concurrent-ruby-edge 0.3.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +552 -0
  3. data/LICENSE.txt +18 -18
  4. data/README.md +261 -103
  5. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/abstract.rb +2 -0
  6. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/awaits.rb +2 -0
  7. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/buffer.rb +2 -0
  8. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/errors_on_unknown_message.rb +2 -0
  9. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/executes_context.rb +2 -0
  10. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/linking.rb +2 -0
  11. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/pausing.rb +2 -0
  12. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/removes_child.rb +2 -0
  13. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/sets_results.rb +2 -0
  14. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/supervising.rb +2 -0
  15. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour/termination.rb +3 -1
  16. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/behaviour.rb +1 -1
  17. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/context.rb +3 -1
  18. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/core.rb +5 -4
  19. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/default_dead_letter_handler.rb +2 -0
  20. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/envelope.rb +2 -0
  21. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/errors.rb +2 -0
  22. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/internal_delegations.rb +3 -0
  23. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/reference.rb +9 -8
  24. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/root.rb +3 -0
  25. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/ad_hoc.rb +2 -0
  26. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/balancer.rb +2 -0
  27. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/broadcast.rb +1 -0
  28. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils/pool.rb +1 -0
  29. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor.rb +11 -6
  30. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/base.rb +14 -14
  31. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/dropping.rb +1 -0
  32. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/sliding.rb +1 -0
  33. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/unbuffered.rb +1 -1
  34. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/tick.rb +1 -1
  35. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel.rb +3 -2
  36. data/lib/concurrent-ruby-edge/concurrent/edge/cancellation.rb +107 -0
  37. data/lib/concurrent-ruby-edge/concurrent/edge/channel.rb +453 -0
  38. data/lib/concurrent-ruby-edge/concurrent/edge/erlang_actor.rb +1549 -0
  39. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set/node.rb +2 -2
  40. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set.rb +8 -7
  41. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_queue.rb +2 -0
  42. data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/old_channel_integration.rb +2 -0
  43. data/lib/concurrent-ruby-edge/concurrent/edge/processing_actor.rb +184 -0
  44. data/lib/concurrent-ruby-edge/concurrent/edge/promises.rb +174 -0
  45. data/lib/concurrent-ruby-edge/concurrent/edge/throttle.rb +229 -0
  46. data/lib/concurrent-ruby-edge/concurrent/edge/version.rb +3 -0
  47. data/lib/concurrent-ruby-edge/concurrent/edge.rb +21 -0
  48. data/lib/concurrent-ruby-edge/concurrent/executor/wrapping_executor.rb +50 -0
  49. data/lib/concurrent-ruby-edge/concurrent/lazy_register.rb +83 -0
  50. data/lib/{concurrent-edge.rb → concurrent-ruby-edge/concurrent-edge.rb} +5 -4
  51. metadata +71 -67
  52. data/lib/concurrent/edge/atomic_markable_reference.rb +0 -184
  53. data/lib/concurrent/edge/cancellation.rb +0 -138
  54. data/lib/concurrent/edge/lock_free_stack.rb +0 -126
  55. data/lib/concurrent/edge/processing_actor.rb +0 -161
  56. data/lib/concurrent/edge/promises.rb +0 -2111
  57. data/lib/concurrent/edge/throttle.rb +0 -192
  58. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/public_delegations.rb +0 -0
  59. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/type_check.rb +0 -0
  60. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/actor/utils.rb +0 -0
  61. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/buffered.rb +0 -0
  62. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/ticker.rb +0 -0
  63. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer/timer.rb +0 -0
  64. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/buffer.rb +0 -0
  65. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/after_clause.rb +0 -0
  66. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/default_clause.rb +0 -0
  67. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/error_clause.rb +0 -0
  68. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/put_clause.rb +0 -0
  69. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector/take_clause.rb +0 -0
  70. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/channel/selector.rb +0 -0
  71. /data/lib/{concurrent → concurrent-ruby-edge/concurrent}/edge/lock_free_linked_set/window.rb +0 -0
@@ -1,4 +1,4 @@
1
- require 'concurrent/edge/atomic_markable_reference'
1
+ require 'concurrent/atomic/atomic_markable_reference'
2
2
 
3
3
  module Concurrent
4
4
  module Edge
@@ -10,7 +10,7 @@ module Concurrent
10
10
 
11
11
  def initialize(data = nil, successor = nil)
12
12
  super()
13
- @SuccessorReference = AtomicMarkableReference.new(successor || Tail.new)
13
+ @SuccessorReference = AtomicMarkableReference.new(successor || Tail.new)
14
14
  @Data = data
15
15
  @Key = key_for data
16
16
  end
@@ -18,12 +18,13 @@ module Concurrent
18
18
  #
19
19
  # This algorithm is a variation of the Nonblocking Linked Set found in
20
20
  # 'The Art of Multiprocessor Programming' by Herlihy and Shavit.
21
+ # @!macro warn.edge
21
22
  class LockFreeLinkedSet
22
23
  include Enumerable
23
24
 
24
- # @!macro [attach] lock_free_linked_list_method_initialize
25
+ # @!macro lock_free_linked_list_method_initialize
25
26
  #
26
- # @param [Fixnum] initial_size the size of the linked_list to initialize
27
+ # @param [Fixnum] initial_size the size of the linked_list to initialize
27
28
  def initialize(initial_size = 0, val = nil)
28
29
  @head = Head.new
29
30
 
@@ -33,7 +34,7 @@ module Concurrent
33
34
  end
34
35
  end
35
36
 
36
- # @!macro [attach] lock_free_linked_list_method_add
37
+ # @!macro lock_free_linked_list_method_add
37
38
  #
38
39
  # Atomically adds the item to the set if it does not yet exist. Note:
39
40
  # internally the set uses `Object#hash` to compare equality of items,
@@ -61,7 +62,7 @@ module Concurrent
61
62
  end
62
63
  end
63
64
 
64
- # @!macro [attach] lock_free_linked_list_method_<<
65
+ # @!macro lock_free_linked_list_method_<<
65
66
  #
66
67
  # Atomically adds the item to the set if it does not yet exist.
67
68
  #
@@ -73,7 +74,7 @@ module Concurrent
73
74
  self
74
75
  end
75
76
 
76
- # @!macro [attach] lock_free_linked_list_method_contains
77
+ # @!macro lock_free_linked_list_method_contains
77
78
  #
78
79
  # Atomically checks to see if the set contains an item. This method
79
80
  # compares equality based on the `Object#hash` method, meaning that the
@@ -94,7 +95,7 @@ module Concurrent
94
95
  curr == item && !marked
95
96
  end
96
97
 
97
- # @!macro [attach] lock_free_linked_list_method_remove
98
+ # @!macro lock_free_linked_list_method_remove
98
99
  #
99
100
  # Atomically attempts to remove an item, comparing using `Object#hash`.
100
101
  #
@@ -120,7 +121,7 @@ module Concurrent
120
121
  end
121
122
  end
122
123
 
123
- # @!macro [attach] lock_free_linked_list_method_each
124
+ # @!macro lock_free_linked_list_method_each
124
125
  #
125
126
  # An iterator to loop through the set.
126
127
  #
@@ -1,3 +1,5 @@
1
+ require 'concurrent/synchronization/object'
2
+
1
3
  module Concurrent
2
4
 
3
5
  # @!visibility private
@@ -1,3 +1,5 @@
1
+ require 'concurrent/promises'
2
+
1
3
  module Concurrent
2
4
  module Promises
3
5
  module FactoryMethods
@@ -0,0 +1,184 @@
1
+ require 'concurrent/synchronization/object'
2
+ require 'concurrent/promises'
3
+ require 'concurrent/edge/channel'
4
+
5
+ module Concurrent
6
+
7
+ # A new implementation of actor which also simulates the process, therefore it can be used
8
+ # in the same way as Erlang's actors but **without** occupying thread. A tens of thousands
9
+ # ProcessingActors can run at the same time sharing a thread pool.
10
+ # @example
11
+ # # Runs on a pool, does not consume 50_000 threads
12
+ # actors = 50_000.times.map do |i|
13
+ # Concurrent::ProcessingActor.act(i) { |a, i| a.receive.then_on(:fast, i) { |m, i| m + i } }
14
+ # end
15
+ #
16
+ # actors.each { |a| a.tell 1 }
17
+ # values = actors.map(&:termination).map(&:value)
18
+ # values[0,5] # => [1, 2, 3, 4, 5]
19
+ # values[-5, 5] # => [49996, 49997, 49998, 49999, 50000]
20
+ # @!macro warn.edge
21
+ class ProcessingActor < Synchronization::Object
22
+
23
+ # TODO (pitr-ch 29-Jan-2019): simplify as much as possible, maybe even do not delegate to mailbox, no ask linking etc
24
+ # TODO (pitr-ch 03-Feb-2019): remove completely
25
+
26
+ safe_initialization!
27
+
28
+ # @return [Promises::Channel] actor's mailbox.
29
+ def mailbox
30
+ @Mailbox
31
+ end
32
+
33
+ # @return [Promises::Future(Object)] a future which is resolved when the actor ends its processing.
34
+ # It can either be fulfilled with a value when actor ends normally or rejected with
35
+ # a reason (exception) when actor fails.
36
+ def termination
37
+ @Terminated.with_hidden_resolvable
38
+ end
39
+
40
+ # Creates an actor.
41
+ # @see #act_listening Behaves the same way, but does not take mailbox as a first argument.
42
+ # @return [ProcessingActor]
43
+ # @example
44
+ # actor = Concurrent::ProcessingActor.act do |actor|
45
+ # actor.receive.then do |message|
46
+ # # the actor ends normally with message
47
+ # message
48
+ # end
49
+ # end
50
+ #
51
+ # actor.tell :a_message
52
+ # # => <#Concurrent::ProcessingActor:0x7fff11280560 termination:pending>
53
+ # actor.termination.value! # => :a_message
54
+ def self.act(*args, &process)
55
+ act_listening Promises::Channel.new, *args, &process
56
+ end
57
+
58
+ # Creates an actor listening to a specified channel (mailbox).
59
+ # @param [Object] args Arguments passed to the process.
60
+ # @param [Promises::Channel] channel which serves as mailing box. The channel can have limited
61
+ # size to achieve backpressure.
62
+ # @yield [actor, *args] to the process to get back a future which represents the actors execution.
63
+ # @yieldparam [ProcessingActor] actor
64
+ # @yieldparam [Object] *args
65
+ # @yieldreturn [Promises::Future(Object)] a future representing next step of execution
66
+ # @return [ProcessingActor]
67
+ def self.act_listening(channel, *args, &process)
68
+ ProcessingActor.new channel, *args, &process
69
+ end
70
+
71
+ # # Receives a message when available, used in the actor's process.
72
+ # # @return [Promises::Future(Object)] a future which will be fulfilled with a message from
73
+ # # mailbox when it is available.
74
+ # def receive(*channels)
75
+ # channels = [@Mailbox] if channels.empty?
76
+ # Promises::Channel.select(*channels)
77
+ # # TODO (pitr-ch 27-Dec-2016): support patterns
78
+ # # - put any received message aside if it does not match
79
+ # # - on each receive call check the messages put aside
80
+ # # - track where the message came from, cannot later receive m form other channel only because it matches
81
+ # end
82
+
83
+ def receive(channel = mailbox)
84
+ channel.pop_op
85
+ end
86
+
87
+ # Tells a message to the actor. May block current thread if the mailbox is full.
88
+ # {#tell_op} is a better option since it does not block. It's usually used to integrate with
89
+ # threading code.
90
+ # @example
91
+ # Thread.new(actor) do |actor|
92
+ # # ...
93
+ # actor.tell! :a_message # blocks until the message is told
94
+ # # (there is a space for it in the channel)
95
+ # # ...
96
+ # end
97
+ # @param [Object] message
98
+ # @return [self]
99
+ def tell!(message)
100
+ @Mailbox.push(message)
101
+ self
102
+ end
103
+
104
+ # Tells a message to the actor.
105
+ # @param [Object] message
106
+ # @return [Promises::Future(ProcessingActor)] a future which will be fulfilled with the actor
107
+ # when the message is pushed to mailbox.
108
+ def tell_op(message)
109
+ @Mailbox.push_op(message).then(self) { |_ch, actor| actor }
110
+ end
111
+
112
+ # # Simplifies common pattern when a message sender also requires an answer to the message
113
+ # # from the actor. It appends a resolvable_future for the answer after the message.
114
+ # # @todo has to be nice also on the receive side, cannot make structure like this [message = [...], answer]
115
+ # # all receives should receive something friendly
116
+ # # @param [Object] message
117
+ # # @param [Promises::ResolvableFuture] answer
118
+ # # @return [Promises::Future] a future which will be fulfilled with the answer to the message
119
+ # # @example
120
+ # # add_once_actor = Concurrent::ProcessingActor.act do |actor|
121
+ # # actor.receive.then do |(a, b), answer|
122
+ # # result = a + b
123
+ # # answer.fulfill result
124
+ # # # terminate with result value
125
+ # # result
126
+ # # end
127
+ # # end
128
+ # # # => <#Concurrent::ProcessingActor:0x7fcd1315f6e8 termination:pending>
129
+ # #
130
+ # # add_once_actor.ask([1, 2]).value! # => 3
131
+ # # # fails the actor already added once
132
+ # # add_once_actor.ask(%w(ab cd)).reason
133
+ # # # => #<RuntimeError: actor terminated normally before answering with a value: 3>
134
+ # # add_once_actor.termination.value! # => 3
135
+ # def ask(message, answer = Promises.resolvable_future)
136
+ # raise 'to be removed'
137
+ #
138
+ # # TODO (pitr-ch 12-Dec-2018): REMOVE, the process ends up as another future not a value, no nice way to do ask in the actor
139
+ # tell [message, answer]
140
+ # # do not leave answers unanswered when actor terminates.
141
+ # Promises.any(
142
+ # Promises.fulfilled_future(:answer).zip(answer),
143
+ # Promises.fulfilled_future(:termination).zip(@Terminated)
144
+ # ).chain do |fulfilled, (which, value), (_, reason)|
145
+ # # TODO (pitr-ch 20-Jan-2017): we have to know which future was resolved
146
+ # # TODO (pitr-ch 20-Jan-2017): make the combinator programmable, so anyone can create what is needed
147
+ # # FIXME (pitr-ch 19-Jan-2017): ensure no callbacks are accumulated on @Terminated
148
+ # if which == :termination
149
+ # raise reason.nil? ? format('actor terminated normally before answering with a value: %s', value) : reason
150
+ # else
151
+ # fulfilled ? value : raise(reason)
152
+ # end
153
+ # end
154
+ # end
155
+
156
+ # actor.ask2 { |a| [:count, a] }
157
+ def ask_op(answer = Promises.resolvable_future, &message_provider)
158
+ # TODO (pitr-ch 12-Dec-2018): is it ok to let the answers be unanswered when the actor terminates
159
+ tell_op(message_provider.call(answer)).then(answer) { |_, a| a }
160
+
161
+ # answer.chain { |v| [true, v] } | @Terminated.then
162
+ end
163
+
164
+ # @return [String] string representation.
165
+ def to_s
166
+ format '%s termination: %s>', super[0..-2], termination.state
167
+ end
168
+
169
+ alias_method :inspect, :to_s
170
+
171
+ def to_ary
172
+ [@Mailbox, @Terminated]
173
+ end
174
+
175
+ private
176
+
177
+ def initialize(channel, *args, &process)
178
+ @Mailbox = channel
179
+ @Terminated = Promises.future(self, *args, &process).run
180
+ super()
181
+ end
182
+
183
+ end
184
+ end
@@ -0,0 +1,174 @@
1
+ # TODO try stealing pool, each thread has it's own queue
2
+
3
+ require 'concurrent/promises'
4
+
5
+ module Concurrent
6
+ module Promises
7
+
8
+ class Future < AbstractEventFuture
9
+
10
+ # @!macro warn.edge
11
+ module ActorIntegration
12
+ # Asks the actor with its value.
13
+ # @return [Future] new future with the response form the actor
14
+ def then_ask(actor)
15
+ self.then(actor) { |v, a| a.ask_op(v) }.flat
16
+ end
17
+ end
18
+
19
+ include ActorIntegration
20
+
21
+ # @!macro warn.edge
22
+ module FlatShortcuts
23
+
24
+ # @return [Future]
25
+ def then_flat_future(*args, &block)
26
+ self.then(*args, &block).flat_future
27
+ end
28
+
29
+ alias_method :then_flat, :then_flat_future
30
+
31
+ # @return [Future]
32
+ def then_flat_future_on(executor, *args, &block)
33
+ self.then_on(executor, *args, &block).flat_future
34
+ end
35
+
36
+ alias_method :then_flat_on, :then_flat_future_on
37
+
38
+ # @return [Event]
39
+ def then_flat_event(*args, &block)
40
+ self.then(*args, &block).flat_event
41
+ end
42
+
43
+ # @return [Event]
44
+ def then_flat_event_on(executor, *args, &block)
45
+ self.then_on(executor, *args, &block).flat_event
46
+ end
47
+ end
48
+
49
+ include FlatShortcuts
50
+ end
51
+
52
+ class Future < AbstractEventFuture
53
+ # @!macro warn.edge
54
+ module NewChannelIntegration
55
+
56
+ # @param [Channel] channel to push to.
57
+ # @return [Future] a future which is fulfilled after the message is pushed to the channel.
58
+ # May take a moment if the channel is full.
59
+ def then_channel_push(channel)
60
+ self.then(channel) { |value, ch| ch.push_op value }.flat_future
61
+ end
62
+
63
+ end
64
+
65
+ include NewChannelIntegration
66
+ end
67
+
68
+ module FactoryMethods
69
+ # @!macro promises.shortcut.on
70
+ # @return [Future]
71
+ # @!macro warn.edge
72
+ def zip_futures_over(enumerable, &future_factory)
73
+ zip_futures_over_on default_executor, enumerable, &future_factory
74
+ end
75
+
76
+ # Creates new future which is resolved after all the futures created by future_factory from
77
+ # enumerable elements are resolved. Simplified it does:
78
+ # `zip(*enumerable.map { |e| future e, &future_factory })`
79
+ # @example
80
+ # # `#succ` calls are executed in parallel
81
+ # zip_futures_over_on(:io, [1, 2], &:succ).value! # => [2, 3]
82
+ #
83
+ # @!macro promises.param.default_executor
84
+ # @param [Enumerable] enumerable
85
+ # @yield a task to be executed in future
86
+ # @yieldparam [Object] element from enumerable
87
+ # @yieldreturn [Object] a value of the future
88
+ # @return [Future]
89
+ # @!macro warn.edge
90
+ def zip_futures_over_on(default_executor, enumerable, &future_factory)
91
+ # ZipFuturesPromise.new_blocked_by(futures_and_or_events, default_executor).future
92
+ zip_futures_on(default_executor, *enumerable.map { |e| future e, &future_factory })
93
+ end
94
+ end
95
+
96
+ module Resolvable
97
+ include InternalStates
98
+
99
+ # Reserves the event or future, if reserved others are prevented from resolving it.
100
+ # Advanced feature.
101
+ # Be careful about the order of reservation to avoid deadlocks,
102
+ # the method blocks if the future or event is already reserved
103
+ # until it is released or resolved.
104
+ #
105
+ # @example
106
+ # f = Concurrent::Promises.resolvable_future
107
+ # reserved = f.reserve
108
+ # Thread.new { f.resolve true, :val, nil } # fails
109
+ # f.resolve true, :val, nil, true if reserved # must be called only if reserved
110
+ # @return [true, false] on successful reservation
111
+ def reserve
112
+ while true
113
+ return true if compare_and_set_internal_state(PENDING, RESERVED)
114
+ return false if resolved?
115
+ # FIXME (pitr-ch 17-Jan-2019): sleep until given up or resolved instead of busy wait
116
+ Thread.pass
117
+ end
118
+ end
119
+
120
+ # @return [true, false] on successful release of the reservation
121
+ def release
122
+ compare_and_set_internal_state(RESERVED, PENDING)
123
+ end
124
+
125
+ # @return [Comparable] an item to sort the resolvable events or futures
126
+ # by to get the right global locking order of resolvable events or futures
127
+ # @see .atomic_resolution
128
+ def self.locking_order_by(resolvable)
129
+ resolvable.object_id
130
+ end
131
+
132
+ # Resolves all passed events and futures to the given resolutions
133
+ # if possible (all are unresolved) or none.
134
+ #
135
+ # @param [Hash{Resolvable=>resolve_arguments}, Array<Array(Resolvable, resolve_arguments)>] resolvable_map
136
+ # collection of resolvable events and futures which should be resolved all at once
137
+ # and what should they be resolved to, examples:
138
+ # ```ruby
139
+ # { a_resolvable_future1 => [true, :val, nil],
140
+ # a_resolvable_future2 => [false, nil, :err],
141
+ # a_resolvable_event => [] }
142
+ # ```
143
+ # or
144
+ # ```ruby
145
+ # [[a_resolvable_future1, [true, :val, nil]],
146
+ # [a_resolvable_future2, [false, nil, :err]],
147
+ # [a_resolvable_event, []]]
148
+ # ```
149
+ # @return [true, false] if success
150
+ def self.atomic_resolution(resolvable_map)
151
+ # atomic_resolution event => [], future => [true, :v, nil]
152
+ sorted = resolvable_map.to_a.sort_by { |resolvable, _| locking_order_by resolvable }
153
+
154
+ reserved = 0
155
+ while reserved < sorted.size && sorted[reserved].first.reserve
156
+ reserved += 1
157
+ end
158
+
159
+ if reserved == sorted.size
160
+ sorted.each { |resolvable, args| resolvable.resolve(*args, true, true) }
161
+ true
162
+ else
163
+ while reserved > 0
164
+ reserved -= 1
165
+ raise 'has to be reserved' unless sorted[reserved].first.release
166
+ end
167
+ false
168
+ end
169
+ end
170
+ end
171
+
172
+
173
+ end
174
+ end
@@ -0,0 +1,229 @@
1
+ require 'concurrent/edge/lock_free_queue'
2
+ require 'concurrent/promises'
3
+ require 'concurrent/synchronization/object'
4
+
5
+ module Concurrent
6
+ # A tool managing concurrency level of tasks.
7
+ # The maximum capacity is set in constructor.
8
+ # Each acquire will lower the available capacity and release will increase it.
9
+ # When there is no available capacity the current thread may either be blocked or
10
+ # an event is returned which will be resolved when capacity becomes available.
11
+ #
12
+ # The more common usage of the Throttle is with a proxy executor
13
+ # `a_throttle.on(Concurrent.global_io_executor)`.
14
+ # Anything executed on the proxy executor will be throttled and
15
+ # execute on the given executor. There can be more than one proxy executors.
16
+ # All abstractions which execute tasks have option to specify executor,
17
+ # therefore the proxy executor can be injected to any abstraction
18
+ # throttling its concurrency level.
19
+ #
20
+ # {include:file:docs-source/throttle.out.md}
21
+ #
22
+ # @!macro warn.edge
23
+ class Throttle < Synchronization::Object
24
+ safe_initialization!
25
+
26
+ attr_atomic(:capacity)
27
+ private :capacity, :capacity=, :swap_capacity, :compare_and_set_capacity, :update_capacity
28
+
29
+ # @return [Integer] The available capacity.
30
+ def available_capacity
31
+ current_capacity = capacity
32
+ current_capacity >= 0 ? current_capacity : 0
33
+ end
34
+
35
+ # Create throttle.
36
+ # @param [Integer] capacity How many tasks using this throttle can run at the same time.
37
+ def initialize(capacity)
38
+ super()
39
+ @MaxCapacity = capacity
40
+ @Queue = LockFreeQueue.new
41
+ @executor_cache = [nil, nil]
42
+ self.capacity = capacity
43
+ end
44
+
45
+ # @return [Integer] The maximum capacity.
46
+ def max_capacity
47
+ @MaxCapacity
48
+ end
49
+
50
+ # Blocks current thread until there is capacity available in the throttle.
51
+ # The acquired capacity has to be returned to the throttle by calling {#release}.
52
+ # If block is passed then the block is called after the capacity is acquired and
53
+ # it is automatically released after the block is executed.
54
+ #
55
+ # @param [Numeric] timeout the maximum time in second to wait.
56
+ # @yield [] block to execute after the capacity is acquired
57
+ # @return [Object, self, true, false]
58
+ # * When no timeout and no block it returns self
59
+ # * When no timeout and with block it returns the result of the block
60
+ # * When with timeout and no block it returns true when acquired and false when timed out
61
+ # * When with timeout and with block it returns the result of the block of nil on timing out
62
+ # @see #release
63
+ def acquire(timeout = nil, &block)
64
+ event = acquire_or_event
65
+ if event
66
+ within_timeout = event.wait(timeout)
67
+ # release immediately when acquired later after the timeout since it is unused
68
+ event.on_resolution!(self, &:release) unless within_timeout
69
+ else
70
+ within_timeout = true
71
+ end
72
+
73
+ called = false
74
+ if timeout
75
+ if block
76
+ if within_timeout
77
+ called = true
78
+ block.call
79
+ else
80
+ nil
81
+ end
82
+ else
83
+ within_timeout
84
+ end
85
+ else
86
+ if block
87
+ called = true
88
+ block.call
89
+ else
90
+ self
91
+ end
92
+ end
93
+ ensure
94
+ release if called
95
+ end
96
+
97
+ # Tries to acquire capacity from the throttle.
98
+ # Returns true when there is capacity available.
99
+ # The acquired capacity has to be returned to the throttle by calling {#release}.
100
+ # @return [true, false]
101
+ # @see #release
102
+ def try_acquire
103
+ while true
104
+ current_capacity = capacity
105
+ if current_capacity > 0
106
+ return true if compare_and_set_capacity(
107
+ current_capacity, current_capacity - 1)
108
+ else
109
+ return false
110
+ end
111
+ end
112
+ end
113
+
114
+ # Releases previously acquired capacity back to Throttle.
115
+ # Has to be called exactly once for each acquired capacity.
116
+ # @return [self]
117
+ # @see #acquire_operation, #acquire, #try_acquire
118
+ def release
119
+ while true
120
+ current_capacity = capacity
121
+ if compare_and_set_capacity current_capacity, current_capacity + 1
122
+ if current_capacity < 0
123
+ # release called after trigger which pushed a trigger, busy wait is ok
124
+ Thread.pass until (trigger = @Queue.pop)
125
+ trigger.resolve
126
+ end
127
+ return self
128
+ end
129
+ end
130
+ end
131
+
132
+ # @return [String] Short string representation.
133
+ def to_s
134
+ format '%s capacity available %d of %d>', super[0..-2], capacity, @MaxCapacity
135
+ end
136
+
137
+ alias_method :inspect, :to_s
138
+
139
+ # @!visibility private
140
+ def acquire_or_event
141
+ while true
142
+ current_capacity = capacity
143
+ if compare_and_set_capacity current_capacity, current_capacity - 1
144
+ if current_capacity > 0
145
+ return nil
146
+ else
147
+ event = Promises.resolvable_event
148
+ @Queue.push event
149
+ return event
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ include Promises::FactoryMethods
156
+
157
+ # @param [ExecutorService] executor
158
+ # @return [ExecutorService] An executor which wraps given executor and allows to post tasks only
159
+ # as available capacity in the throttle allows.
160
+ # @example throttling future
161
+ # a_future.then_on(a_throttle.on(:io)) { a_throttled_task }
162
+ def on(executor = Promises::FactoryMethods.default_executor)
163
+ current_executor, current_cache = @executor_cache
164
+ return current_cache if current_executor == executor && current_cache
165
+
166
+ if current_executor.nil?
167
+ # cache first proxy
168
+ proxy_executor = ProxyExecutor.new(self, Concurrent.executor(executor))
169
+ @executor_cache = [executor, proxy_executor]
170
+ return proxy_executor
171
+ else
172
+ # do not cache more than 1 executor
173
+ ProxyExecutor.new(self, Concurrent.executor(executor))
174
+ end
175
+ end
176
+
177
+ # Uses executor provided by {#on} therefore
178
+ # all events and futures created using factory methods on this object will be throttled.
179
+ # Overrides {Promises::FactoryMethods#default_executor}.
180
+ #
181
+ # @return [ExecutorService]
182
+ # @see Promises::FactoryMethods#default_executor
183
+ def default_executor
184
+ on(super)
185
+ end
186
+
187
+ class ProxyExecutor < Synchronization::Object
188
+ safe_initialization!
189
+
190
+ include ExecutorService
191
+
192
+ def initialize(throttle, executor)
193
+ super()
194
+ @Throttle = throttle
195
+ @Executor = executor
196
+ end
197
+
198
+ def post(*args, &task)
199
+ if (event = @Throttle.acquire_or_event)
200
+ event.on_resolution! { inner_post(*args, &task) }
201
+ else
202
+ inner_post(*args, &task)
203
+ end
204
+ end
205
+
206
+ def can_overflow?
207
+ @Executor.can_overflow?
208
+ end
209
+
210
+ def serialized?
211
+ @Executor.serialized?
212
+ end
213
+
214
+ private
215
+
216
+ def inner_post(*arguments, &task)
217
+ @Executor.post(*arguments) do |*args|
218
+ begin
219
+ task.call(*args)
220
+ ensure
221
+ @Throttle.release
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ private_constant :ProxyExecutor
228
+ end
229
+ end
@@ -0,0 +1,3 @@
1
+ module Concurrent
2
+ EDGE_VERSION = '0.7.0'
3
+ end