concurrent-ruby-edge 0.1.0.pre2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +284 -0
  4. data/lib/concurrent-edge.rb +11 -0
  5. data/lib/concurrent/actor.rb +98 -0
  6. data/lib/concurrent/actor/behaviour.rb +143 -0
  7. data/lib/concurrent/actor/behaviour/abstract.rb +51 -0
  8. data/lib/concurrent/actor/behaviour/awaits.rb +21 -0
  9. data/lib/concurrent/actor/behaviour/buffer.rb +56 -0
  10. data/lib/concurrent/actor/behaviour/errors_on_unknown_message.rb +12 -0
  11. data/lib/concurrent/actor/behaviour/executes_context.rb +17 -0
  12. data/lib/concurrent/actor/behaviour/linking.rb +83 -0
  13. data/lib/concurrent/actor/behaviour/pausing.rb +123 -0
  14. data/lib/concurrent/actor/behaviour/removes_child.rb +16 -0
  15. data/lib/concurrent/actor/behaviour/sets_results.rb +37 -0
  16. data/lib/concurrent/actor/behaviour/supervising.rb +39 -0
  17. data/lib/concurrent/actor/behaviour/terminates_children.rb +14 -0
  18. data/lib/concurrent/actor/behaviour/termination.rb +74 -0
  19. data/lib/concurrent/actor/context.rb +167 -0
  20. data/lib/concurrent/actor/core.rb +220 -0
  21. data/lib/concurrent/actor/default_dead_letter_handler.rb +9 -0
  22. data/lib/concurrent/actor/envelope.rb +41 -0
  23. data/lib/concurrent/actor/errors.rb +27 -0
  24. data/lib/concurrent/actor/internal_delegations.rb +59 -0
  25. data/lib/concurrent/actor/public_delegations.rb +40 -0
  26. data/lib/concurrent/actor/reference.rb +106 -0
  27. data/lib/concurrent/actor/root.rb +37 -0
  28. data/lib/concurrent/actor/type_check.rb +48 -0
  29. data/lib/concurrent/actor/utils.rb +10 -0
  30. data/lib/concurrent/actor/utils/ad_hoc.rb +27 -0
  31. data/lib/concurrent/actor/utils/balancer.rb +43 -0
  32. data/lib/concurrent/actor/utils/broadcast.rb +52 -0
  33. data/lib/concurrent/actor/utils/pool.rb +54 -0
  34. data/lib/concurrent/agent.rb +289 -0
  35. data/lib/concurrent/channel.rb +6 -0
  36. data/lib/concurrent/channel/blocking_ring_buffer.rb +82 -0
  37. data/lib/concurrent/channel/buffered_channel.rb +87 -0
  38. data/lib/concurrent/channel/channel.rb +19 -0
  39. data/lib/concurrent/channel/ring_buffer.rb +65 -0
  40. data/lib/concurrent/channel/unbuffered_channel.rb +39 -0
  41. data/lib/concurrent/channel/waitable_list.rb +48 -0
  42. data/lib/concurrent/edge/atomic_markable_reference.rb +184 -0
  43. data/lib/concurrent/edge/future.rb +1226 -0
  44. data/lib/concurrent/edge/lock_free_stack.rb +85 -0
  45. metadata +110 -0
@@ -0,0 +1,52 @@
1
+ require 'set'
2
+
3
+ module Concurrent
4
+ module Actor
5
+ module Utils
6
+
7
+ # Allows to build pub/sub easily.
8
+ # @example news
9
+ # news_channel = Concurrent::Actor::Utils::Broadcast.spawn :news
10
+ #
11
+ # 2.times do |i|
12
+ # Concurrent::Actor::Utils::AdHoc.spawn "listener-#{i}" do
13
+ # news_channel << :subscribe
14
+ # -> message { puts message }
15
+ # end
16
+ # end
17
+ #
18
+ # news_channel << 'Ruby rocks!'
19
+ # # prints: 'Ruby rocks!' twice
20
+ class Broadcast < RestartingContext
21
+
22
+ def initialize
23
+ @receivers = Set.new
24
+ end
25
+
26
+ def on_message(message)
27
+ case message
28
+ when :subscribe
29
+ if envelope.sender.is_a? Reference
30
+ @receivers.add envelope.sender
31
+ true
32
+ else
33
+ false
34
+ end
35
+ when :unsubscribe
36
+ !!@receivers.delete(envelope.sender)
37
+ when :subscribed?
38
+ @receivers.include? envelope.sender
39
+ else
40
+ filtered_receivers.each { |r| r << message }
41
+ end
42
+ end
43
+
44
+ # override to define different behaviour, filtering etc
45
+ def filtered_receivers
46
+ @receivers
47
+ end
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,54 @@
1
+ require 'concurrent/actor/utils/balancer'
2
+
3
+ module Concurrent
4
+ module Actor
5
+ module Utils
6
+
7
+ # Allows to create a pool of workers and distribute work between them
8
+ # @param [Integer] size number of workers
9
+ # @yield [balancer, index] a block spawning an worker instance. called +size+ times.
10
+ # The worker should be descendant of AbstractWorker and supervised, see example.
11
+ # @yieldparam [Balancer] balancer to pass to the worker
12
+ # @yieldparam [Integer] index of the worker, usually used in its name
13
+ # @yieldreturn [Reference] the reference of newly created worker
14
+ # @example
15
+ # class Worker < Concurrent::Actor::Utils::AbstractWorker
16
+ # def work(message)
17
+ # p message * 5
18
+ # end
19
+ # end
20
+ #
21
+ # pool = Concurrent::Actor::Utils::Pool.spawn! 'pool', 5 do |balancer, index|
22
+ # Worker.spawn name: "worker-#{index}", supervise: true, args: [balancer]
23
+ # end
24
+ #
25
+ # pool << 'asd' << 2
26
+ # # prints:
27
+ # # "asdasdasdasdasd"
28
+ # # 10
29
+ class Pool < RestartingContext
30
+ def initialize(size, &worker_initializer)
31
+ @balancer = Balancer.spawn name: :balancer, supervise: true
32
+ @workers = Array.new(size, &worker_initializer)
33
+ @workers.each do |worker|
34
+ Type! worker, Reference
35
+ @balancer << [:subscribe, worker]
36
+ end
37
+ end
38
+
39
+ def on_message(message)
40
+ command, *rest = message
41
+ return if [:restarted, :reset, :resumed, :terminated].include? command # ignore events from supervised actors
42
+
43
+ envelope_to_redirect = if envelope.future
44
+ envelope
45
+ else
46
+ Envelope.new(envelope.message, Concurrent.future, envelope.sender, envelope.address)
47
+ end
48
+ envelope_to_redirect.future.on_completion! { @balancer << :subscribe } # TODO check safety of @balancer reading
49
+ redirect @balancer, envelope_to_redirect
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,289 @@
1
+ require 'thread'
2
+ require 'concurrent/collection/copy_on_write_observer_set'
3
+ require 'concurrent/concern/dereferenceable'
4
+ require 'concurrent/concern/observable'
5
+ require 'concurrent/concern/logging'
6
+ require 'concurrent/executor/executor'
7
+ require 'concurrent/concern/deprecation'
8
+
9
+ module Concurrent
10
+
11
+ # `Agent`s are inspired by [Clojure's](http://clojure.org/) [agent](http://clojure.org/agents) function. An `Agent` is a single atomic value that represents an identity. The current value of the `Agent` can be requested at any time (`deref`). Each `Agent` has a work queue and operates on the global thread pool (see below). Consumers can `post` code blocks to the `Agent`. The code block (function) will receive the current value of the `Agent` as its sole parameter. The return value of the block will become the new value of the `Agent`. `Agent`s support two error handling modes: fail and continue. A good example of an `Agent` is a shared incrementing counter, such as the score in a video game.
12
+ #
13
+ # An `Agent` must be initialize with an initial value. This value is always accessible via the `value` (or `deref`) methods. Code blocks sent to the `Agent` will be processed in the order received. As each block is processed the current value is updated with the result from the block. This update is an atomic operation so a `deref` will never block and will always return the current value.
14
+ #
15
+ # When an `Agent` is created it may be given an optional `validate` block and zero or more `rescue` blocks. When a new value is calculated the value will be checked against the validator, if present. If the validator returns `true` the new value will be accepted. If it returns `false` it will be rejected. If a block raises an exception during execution the list of `rescue` blocks will be seacrhed in order until one matching the current exception is found. That `rescue` block will then be called an passed the exception object. If no matching `rescue` block is found, or none were configured, then the exception will be suppressed.
16
+ #
17
+ # `Agent`s also implement Ruby's [Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html). Code that observes an `Agent` will receive a callback with the new value any time the value is changed.
18
+ #
19
+ # @!macro copy_options
20
+ #
21
+ # @example Simple Example
22
+ #
23
+ # require 'concurrent'
24
+ #
25
+ # score = Concurrent::Agent.new(10)
26
+ # score.value #=> 10
27
+ #
28
+ # score << proc{|current| current + 100 }
29
+ # sleep(0.1)
30
+ # score.value #=> 110
31
+ #
32
+ # score << proc{|current| current * 2 }
33
+ # sleep(0.1)
34
+ # score.value #=> 220
35
+ #
36
+ # score << proc{|current| current - 50 }
37
+ # sleep(0.1)
38
+ # score.value #=> 170
39
+ #
40
+ # @example With Validation and Error Handling
41
+ #
42
+ # score = Concurrent::Agent.new(0).validate{|value| value <= 1024 }.
43
+ # rescue(NoMethodError){|ex| puts "Bam!" }.
44
+ # rescue(ArgumentError){|ex| puts "Pow!" }.
45
+ # rescue{|ex| puts "Boom!" }
46
+ # score.value #=> 0
47
+ #
48
+ # score << proc{|current| current + 2048 }
49
+ # sleep(0.1)
50
+ # score.value #=> 0
51
+ #
52
+ # score << proc{|current| raise ArgumentError }
53
+ # sleep(0.1)
54
+ # #=> puts "Pow!"
55
+ # score.value #=> 0
56
+ #
57
+ # score << proc{|current| current + 100 }
58
+ # sleep(0.1)
59
+ # score.value #=> 100
60
+ #
61
+ # @example With Observation
62
+ #
63
+ # bingo = Class.new{
64
+ # def update(time, score)
65
+ # puts "Bingo! [score: #{score}, time: #{time}]" if score >= 100
66
+ # end
67
+ # }.new
68
+ #
69
+ # score = Concurrent::Agent.new(0)
70
+ # score.add_observer(bingo)
71
+ #
72
+ # score << proc{|current| sleep(0.1); current += 30 }
73
+ # score << proc{|current| sleep(0.1); current += 30 }
74
+ # score << proc{|current| sleep(0.1); current += 30 }
75
+ # score << proc{|current| sleep(0.1); current += 30 }
76
+ #
77
+ # sleep(1)
78
+ # #=> Bingo! [score: 120, time: 2013-07-22 21:26:08 -0400]
79
+ #
80
+ # @!attribute [r] timeout
81
+ # @return [Fixnum] the maximum number of seconds before an update is cancelled
82
+ #
83
+ # @!macro edge_warning
84
+ class Agent
85
+ include Concern::Dereferenceable
86
+ include Concern::Observable
87
+ include Concern::Logging
88
+ include Concern::Deprecation
89
+
90
+ attr_reader :timeout, :io_executor, :fast_executor
91
+
92
+ # Initialize a new Agent with the given initial value and provided options.
93
+ #
94
+ # @param [Object] initial the initial value
95
+ #
96
+ # @!macro [attach] executor_and_deref_options
97
+ #
98
+ # @param [Hash] opts the options used to define the behavior at update and deref
99
+ # and to specify the executor on which to perform actions
100
+ # @option opts [Executor] :executor when set use the given `Executor` instance.
101
+ # Three special values are also supported: `:task` returns the global task pool,
102
+ # `:operation` returns the global operation pool, and `:immediate` returns a new
103
+ # `ImmediateExecutor` object.
104
+ # @!macro deref_options
105
+ def initialize(initial, opts = {})
106
+ @value = initial
107
+ @rescuers = []
108
+ @validator = Proc.new { |result| true }
109
+ self.observers = Collection::CopyOnWriteObserverSet.new
110
+ @serialized_execution = SerializedExecution.new
111
+ @io_executor = Executor.executor_from_options(opts) || Concurrent.global_io_executor
112
+ @fast_executor = Executor.executor_from_options(opts) || Concurrent.global_fast_executor
113
+ init_mutex
114
+ set_deref_options(opts)
115
+ end
116
+
117
+ # Specifies a block fast to be performed when an update fast raises
118
+ # an exception. Rescue blocks will be checked in order they were added. The first
119
+ # block for which the raised exception "is-a" subclass of the given `clazz` will
120
+ # be called. If no `clazz` is given the block will match any caught exception.
121
+ # This behavior is intended to be identical to Ruby's `begin/rescue/end` behavior.
122
+ # Any number of rescue handlers can be added. If no rescue handlers are added then
123
+ # caught exceptions will be suppressed.
124
+ #
125
+ # @param [Exception] clazz the class of exception to catch
126
+ # @yield the block to be called when a matching exception is caught
127
+ # @yieldparam [StandardError] ex the caught exception
128
+ #
129
+ # @example
130
+ # score = Concurrent::Agent.new(0).
131
+ # rescue(NoMethodError){|ex| puts "Bam!" }.
132
+ # rescue(ArgumentError){|ex| puts "Pow!" }.
133
+ # rescue{|ex| puts "Boom!" }
134
+ #
135
+ # score << proc{|current| raise ArgumentError }
136
+ # sleep(0.1)
137
+ # #=> puts "Pow!"
138
+ def rescue(clazz = StandardError, &block)
139
+ unless block.nil?
140
+ mutex.synchronize do
141
+ @rescuers << Rescuer.new(clazz, block)
142
+ end
143
+ end
144
+ self
145
+ end
146
+
147
+ alias_method :catch, :rescue
148
+ alias_method :on_error, :rescue
149
+
150
+ # A block fast to be performed after every update to validate if the new
151
+ # value is valid. If the new value is not valid then the current value is not
152
+ # updated. If no validator is provided then all updates are considered valid.
153
+ #
154
+ # @yield the block to be called after every update fast to determine if
155
+ # the result is valid
156
+ # @yieldparam [Object] value the result of the last update fast
157
+ # @yieldreturn [Boolean] true if the value is valid else false
158
+ def validate(&block)
159
+
160
+ unless block.nil?
161
+ begin
162
+ mutex.lock
163
+ @validator = block
164
+ ensure
165
+ mutex.unlock
166
+ end
167
+ end
168
+ self
169
+ end
170
+
171
+ alias_method :validates, :validate
172
+ alias_method :validate_with, :validate
173
+ alias_method :validates_with, :validate
174
+
175
+ # Update the current value with the result of the given block fast,
176
+ # block should not do blocking calls, use #post_off for blocking calls
177
+ #
178
+ # @yield the fast to be performed with the current value in order to calculate
179
+ # the new value
180
+ # @yieldparam [Object] value the current value
181
+ # @yieldreturn [Object] the new value
182
+ # @return [true, nil] nil when no block is given
183
+ def post(&block)
184
+ post_on(@fast_executor, &block)
185
+ end
186
+
187
+ # Update the current value with the result of the given block fast,
188
+ # block can do blocking calls
189
+ #
190
+ # @param [Fixnum, nil] timeout [DEPRECATED] maximum number of seconds before an update is cancelled
191
+ #
192
+ # @yield the fast to be performed with the current value in order to calculate
193
+ # the new value
194
+ # @yieldparam [Object] value the current value
195
+ # @yieldreturn [Object] the new value
196
+ # @return [true, nil] nil when no block is given
197
+ def post_off(timeout = nil, &block)
198
+ task = if timeout
199
+ deprecated 'post_off with option timeout options is deprecated and will be removed'
200
+ lambda do |value|
201
+ future = Future.execute do
202
+ block.call(value)
203
+ end
204
+ if future.wait(timeout)
205
+ future.value!
206
+ else
207
+ raise Concurrent::TimeoutError
208
+ end
209
+ end
210
+ else
211
+ block
212
+ end
213
+ post_on(@io_executor, &task)
214
+ end
215
+
216
+ # Update the current value with the result of the given block fast,
217
+ # block should not do blocking calls, use #post_off for blocking calls
218
+ #
219
+ # @yield the fast to be performed with the current value in order to calculate
220
+ # the new value
221
+ # @yieldparam [Object] value the current value
222
+ # @yieldreturn [Object] the new value
223
+ def <<(block)
224
+ post(&block)
225
+ self
226
+ end
227
+
228
+ # Waits/blocks until all the updates sent before this call are done.
229
+ #
230
+ # @param [Numeric] timeout the maximum time in second to wait.
231
+ # @return [Boolean] false on timeout, true otherwise
232
+ def await(timeout = nil)
233
+ done = Event.new
234
+ post { |val| done.set; val }
235
+ done.wait timeout
236
+ end
237
+
238
+ private
239
+
240
+ def post_on(executor, &block)
241
+ return nil if block.nil?
242
+ @serialized_execution.post(executor) { work(&block) }
243
+ true
244
+ end
245
+
246
+ # @!visibility private
247
+ Rescuer = Struct.new(:clazz, :block) # :nodoc:
248
+
249
+ # @!visibility private
250
+ def try_rescue(ex) # :nodoc:
251
+ rescuer = mutex.synchronize do
252
+ @rescuers.find { |r| ex.is_a?(r.clazz) }
253
+ end
254
+ rescuer.block.call(ex) if rescuer
255
+ rescue Exception => ex
256
+ # suppress
257
+ log DEBUG, ex
258
+ end
259
+
260
+ # @!visibility private
261
+ def work(&handler) # :nodoc:
262
+ validator, value = mutex.synchronize { [@validator, @value] }
263
+
264
+ begin
265
+ result = handler.call(value)
266
+ valid = validator.call(result)
267
+ rescue Exception => ex
268
+ exception = ex
269
+ end
270
+
271
+ begin
272
+ mutex.lock
273
+ should_notify = if !exception && valid
274
+ @value = result
275
+ true
276
+ end
277
+ ensure
278
+ mutex.unlock
279
+ end
280
+
281
+ if should_notify
282
+ time = Time.now
283
+ observers.notify_observers { [time, self.value] }
284
+ end
285
+
286
+ try_rescue(exception)
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,6 @@
1
+ require 'concurrent/channel/blocking_ring_buffer'
2
+ require 'concurrent/channel/buffered_channel'
3
+ require 'concurrent/channel/channel'
4
+ require 'concurrent/channel/ring_buffer'
5
+ require 'concurrent/channel/unbuffered_channel'
6
+ require 'concurrent/channel/waitable_list'
@@ -0,0 +1,82 @@
1
+ require 'concurrent/synchronization'
2
+
3
+ module Concurrent
4
+ module Channel
5
+
6
+ # @api Channel
7
+ # @!macro edge_warning
8
+ class BlockingRingBuffer < Synchronization::Object
9
+
10
+ def initialize(capacity)
11
+ super()
12
+ synchronize { ns_initialize capacity}
13
+ end
14
+
15
+ # @return [Integer] the capacity of the buffer
16
+ def capacity
17
+ synchronize { @buffer.capacity }
18
+ end
19
+
20
+ # @return [Integer] the number of elements currently in the buffer
21
+ def count
22
+ synchronize { @buffer.count }
23
+ end
24
+
25
+ # @return [Boolean] true if buffer is empty, false otherwise
26
+ def empty?
27
+ synchronize { @buffer.empty? }
28
+ end
29
+
30
+ # @return [Boolean] true if buffer is full, false otherwise
31
+ def full?
32
+ synchronize { @buffer.full? }
33
+ end
34
+
35
+ # @param [Object] value the value to be inserted
36
+ # @return [Boolean] true if value has been inserted, false otherwise
37
+ def put(value)
38
+ synchronize do
39
+ wait_while_full
40
+ @buffer.offer(value)
41
+ ns_signal
42
+ true
43
+ end
44
+ end
45
+
46
+ # @return [Object] the first available value and removes it from the buffer.
47
+ # If buffer is empty it blocks until an element is available
48
+ def take
49
+ synchronize do
50
+ wait_while_empty
51
+ result = @buffer.poll
52
+ ns_signal
53
+ result
54
+ end
55
+ end
56
+
57
+ # @return [Object] the first available value and without removing it from
58
+ # the buffer. If buffer is empty returns nil
59
+ def peek
60
+ synchronize { @buffer.peek }
61
+ end
62
+
63
+ protected
64
+
65
+ def ns_initialize(capacity)
66
+ @buffer = RingBuffer.new(capacity)
67
+ @first = @last = 0
68
+ @count = 0
69
+ end
70
+
71
+ private
72
+
73
+ def wait_while_full
74
+ ns_wait_until { !@buffer.full? }
75
+ end
76
+
77
+ def wait_while_empty
78
+ ns_wait_until { !@buffer.empty? }
79
+ end
80
+ end
81
+ end
82
+ end