concurrent-ruby 0.2.2 → 0.3.0.pre.1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +45 -42
  3. data/lib/concurrent.rb +5 -6
  4. data/lib/concurrent/agent.rb +29 -33
  5. data/lib/concurrent/cached_thread_pool.rb +26 -105
  6. data/lib/concurrent/channel.rb +94 -0
  7. data/lib/concurrent/event.rb +8 -17
  8. data/lib/concurrent/executor.rb +68 -72
  9. data/lib/concurrent/fixed_thread_pool.rb +15 -83
  10. data/lib/concurrent/functions.rb +7 -22
  11. data/lib/concurrent/future.rb +29 -9
  12. data/lib/concurrent/null_thread_pool.rb +5 -2
  13. data/lib/concurrent/obligation.rb +6 -16
  14. data/lib/concurrent/promise.rb +9 -10
  15. data/lib/concurrent/runnable.rb +103 -0
  16. data/lib/concurrent/supervisor.rb +271 -44
  17. data/lib/concurrent/thread_pool.rb +112 -39
  18. data/lib/concurrent/version.rb +1 -1
  19. data/md/executor.md +9 -3
  20. data/md/goroutine.md +11 -9
  21. data/md/reactor.md +32 -0
  22. data/md/supervisor.md +43 -0
  23. data/spec/concurrent/agent_spec.rb +128 -51
  24. data/spec/concurrent/cached_thread_pool_spec.rb +33 -47
  25. data/spec/concurrent/channel_spec.rb +446 -0
  26. data/spec/concurrent/event_machine_defer_proxy_spec.rb +3 -1
  27. data/spec/concurrent/event_spec.rb +0 -19
  28. data/spec/concurrent/executor_spec.rb +167 -119
  29. data/spec/concurrent/fixed_thread_pool_spec.rb +40 -30
  30. data/spec/concurrent/functions_spec.rb +0 -20
  31. data/spec/concurrent/future_spec.rb +88 -0
  32. data/spec/concurrent/null_thread_pool_spec.rb +23 -2
  33. data/spec/concurrent/obligation_shared.rb +0 -5
  34. data/spec/concurrent/promise_spec.rb +9 -10
  35. data/spec/concurrent/runnable_shared.rb +62 -0
  36. data/spec/concurrent/runnable_spec.rb +233 -0
  37. data/spec/concurrent/supervisor_spec.rb +912 -47
  38. data/spec/concurrent/thread_pool_shared.rb +18 -31
  39. data/spec/spec_helper.rb +10 -3
  40. metadata +17 -23
  41. data/lib/concurrent/defer.rb +0 -65
  42. data/lib/concurrent/reactor.rb +0 -166
  43. data/lib/concurrent/reactor/drb_async_demux.rb +0 -83
  44. data/lib/concurrent/reactor/tcp_sync_demux.rb +0 -131
  45. data/lib/concurrent/utilities.rb +0 -32
  46. data/md/defer.md +0 -174
  47. data/spec/concurrent/defer_spec.rb +0 -199
  48. data/spec/concurrent/reactor/drb_async_demux_spec.rb +0 -196
  49. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +0 -410
  50. data/spec/concurrent/reactor_spec.rb +0 -364
  51. data/spec/concurrent/utilities_spec.rb +0 -74
@@ -5,8 +5,11 @@ module Concurrent
5
5
  class NullThreadPool
6
6
  behavior(:global_thread_pool)
7
7
 
8
- def self.post(*args, &block)
9
- Thread.new(*args, &block).abort_on_exception = false
8
+ def self.post(*args)
9
+ Thread.new(*args) do
10
+ Thread.current.abort_on_exception = false
11
+ yield(*args)
12
+ end
10
13
  return true
11
14
  end
12
15
 
@@ -1,7 +1,8 @@
1
1
  require 'thread'
2
2
  require 'timeout'
3
+ require 'functional'
3
4
 
4
- require 'functional/behavior'
5
+ require 'concurrent/event'
5
6
 
6
7
  behavior_info(:future,
7
8
  state: 0,
@@ -42,26 +43,15 @@ module Concurrent
42
43
  def pending?() return(@state == :pending); end
43
44
 
44
45
  def value(timeout = nil)
45
- if timeout == 0 || ! pending?
46
- return @value
47
- elsif timeout.nil?
48
- return mutex.synchronize { v = @value }
49
- else
50
- begin
51
- return Timeout::timeout(timeout.to_f) {
52
- mutex.synchronize { v = @value }
53
- }
54
- rescue Timeout::Error => ex
55
- return nil
56
- end
57
- end
46
+ event.wait(timeout) unless timeout == 0 || @state != :pending
47
+ return @value
58
48
  end
59
49
  alias_method :deref, :value
60
50
 
61
51
  protected
62
52
 
63
- def mutex
64
- @mutex ||= Mutex.new
53
+ def event
54
+ @event ||= Event.new
65
55
  end
66
56
  end
67
57
  end
@@ -2,7 +2,6 @@ require 'thread'
2
2
 
3
3
  require 'concurrent/global_thread_pool'
4
4
  require 'concurrent/obligation'
5
- require 'concurrent/utilities'
6
5
 
7
6
  module Concurrent
8
7
 
@@ -80,7 +79,7 @@ module Concurrent
80
79
  # @param block [Proc] the block to call if the rescue is matched
81
80
  #
82
81
  # @return [self] so that additional chaining can occur
83
- def rescue(clazz = Exception, &block)
82
+ def rescue(clazz = nil, &block)
84
83
  return self if fulfilled? || rescued? || ! block_given?
85
84
  @lock.synchronize do
86
85
  rescuer = Rescuer.new(clazz, block)
@@ -140,12 +139,12 @@ module Concurrent
140
139
  # @private
141
140
  def try_rescue(ex, *rescuers) # :nodoc:
142
141
  rescuers = @rescuers if rescuers.empty?
143
- rescuer = rescuers.find{|r| ex.is_a?(r.clazz) }
142
+ rescuer = rescuers.find{|r| r.clazz.nil? || ex.is_a?(r.clazz) }
144
143
  if rescuer
145
144
  rescuer.block.call(ex)
146
145
  @rescued = true
147
146
  end
148
- rescue Exception => e
147
+ rescue Exception => ex
149
148
  # supress
150
149
  end
151
150
 
@@ -157,12 +156,12 @@ module Concurrent
157
156
  loop do
158
157
  current = lock.synchronize{ chain[index] }
159
158
  unless current.rejected?
160
- current.mutex.synchronize do
161
- begin
162
- result = current.on_fulfill(result)
163
- rescue Exception => ex
164
- current.on_reject(ex)
165
- end
159
+ begin
160
+ result = current.on_fulfill(result)
161
+ rescue Exception => ex
162
+ current.on_reject(ex)
163
+ ensure
164
+ event.set
166
165
  end
167
166
  end
168
167
  index += 1
@@ -0,0 +1,103 @@
1
+ require 'thread'
2
+ require 'functional'
3
+
4
+ behavior_info(:runnable,
5
+ run: 0,
6
+ stop: 0,
7
+ running?: 0)
8
+
9
+ module Concurrent
10
+
11
+ module Running
12
+
13
+ class Context
14
+ attr_reader :runner, :thread
15
+ def initialize(runner)
16
+ @runner = runner
17
+ @thread = Thread.new(runner) do |runner|
18
+ Thread.abort_on_exception = false
19
+ runner.run
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.included(base)
25
+
26
+ def run!
27
+ return mutex.synchronize do
28
+ raise LifecycleError.new('already running') if @running
29
+ Context.new(self)
30
+ end
31
+ end
32
+
33
+ protected
34
+
35
+ def mutex
36
+ @mutex ||= Mutex.new
37
+ end
38
+
39
+ public
40
+
41
+ class << base
42
+
43
+ def run!(*args, &block)
44
+ runner = self.new(*args, &block)
45
+ return Context.new(runner)
46
+ rescue => ex
47
+ return nil
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ module Runnable
54
+
55
+ behavior(:runnable)
56
+
57
+ LifecycleError = Class.new(StandardError)
58
+
59
+ def self.included(base)
60
+ base.send(:include, Running)
61
+ end
62
+
63
+ def run
64
+ mutex.synchronize do
65
+ raise LifecycleError.new('already running') if @running
66
+ raise LifecycleError.new('#on_task not implemented') unless self.respond_to?(:on_task, true)
67
+ on_run if respond_to?(:on_run, true)
68
+ @running = true
69
+ end
70
+
71
+ loop do
72
+ break unless @running
73
+ on_task
74
+ break unless @running
75
+ Thread.pass
76
+ end
77
+
78
+ after_run if respond_to?(:after_run, true)
79
+ return true
80
+ rescue LifecycleError => ex
81
+ @running = false
82
+ raise ex
83
+ rescue => ex
84
+ @running = false
85
+ return false
86
+ end
87
+
88
+ def stop
89
+ return true unless @running
90
+ mutex.synchronize do
91
+ @running = false
92
+ on_stop if respond_to?(:on_stop, true)
93
+ end
94
+ return true
95
+ rescue => ex
96
+ return false
97
+ end
98
+
99
+ def running?
100
+ return @running == true
101
+ end
102
+ end
103
+ end
@@ -1,71 +1,134 @@
1
1
  require 'thread'
2
2
  require 'functional'
3
3
 
4
- behavior_info(:runnable,
5
- run: 0,
6
- stop: 0,
7
- running?: 0)
4
+ require 'concurrent/runnable'
8
5
 
9
6
  module Concurrent
10
7
 
11
8
  class Supervisor
12
9
 
10
+ behavior(:runnable)
11
+
13
12
  DEFAULT_MONITOR_INTERVAL = 1
13
+ RESTART_STRATEGIES = [:one_for_one, :one_for_all, :rest_for_one]
14
+ DEFAULT_MAX_RESTART = 5
15
+ DEFAULT_MAX_TIME = 60
14
16
 
15
- behavior(:runnable)
17
+ CHILD_TYPES = [:worker, :supervisor]
18
+ CHILD_RESTART_OPTIONS = [:permanent, :transient, :temporary]
16
19
 
17
- WorkerContext = Struct.new(:worker, :thread)
20
+ MaxRestartFrequencyError = Class.new(StandardError)
21
+
22
+ WorkerContext = Struct.new(:worker, :type, :restart) do
23
+ attr_accessor :thread
24
+ attr_accessor :terminated
25
+ def alive?() return thread && thread.alive?; end
26
+ def needs_restart?
27
+ return false if thread && thread.alive?
28
+ return false if terminated == true
29
+ case self.restart
30
+ when :permanent
31
+ return true
32
+ when :transient
33
+ return thread.nil? || thread.status.nil?
34
+ else #when :temporary
35
+ return false
36
+ end
37
+ end
38
+ end
39
+
40
+ WorkerCounts = Struct.new(:specs, :supervisors, :workers) do
41
+ attr_accessor :status
42
+ def add(context)
43
+ self.specs += 1
44
+ self.supervisors += 1 if context.type == :supervisor
45
+ self.workers += 1 if context.type == :worker
46
+ end
47
+ def active() sleeping + running + aborting end;
48
+ def sleeping() @status.reduce(0){|x, s| x += (s == 'sleep' ? 1 : 0) } end;
49
+ def running() @status.reduce(0){|x, s| x += (s == 'run' ? 1 : 0) } end;
50
+ def aborting() @status.reduce(0){|x, s| x += (s == 'aborting' ? 1 : 0) } end;
51
+ def stopped() @status.reduce(0){|x, s| x += (s == false ? 1 : 0) } end;
52
+ def abend() @status.reduce(0){|x, s| x += (s.nil? ? 1 : 0) } end;
53
+ end
18
54
 
19
55
  attr_reader :monitor_interval
56
+ attr_reader :restart_strategy
57
+ attr_reader :max_restart
58
+ attr_reader :max_time
59
+
60
+ alias_method :strategy, :restart_strategy
61
+ alias_method :max_r, :max_restart
62
+ alias_method :max_t, :max_time
20
63
 
21
64
  def initialize(opts = {})
65
+ @restart_strategy = opts[:restart_strategy] || opts[:strategy] || :one_for_one
66
+ @monitor_interval = (opts[:monitor_interval] || DEFAULT_MONITOR_INTERVAL).to_f
67
+ @max_restart = (opts[:max_restart] || opts[:max_r] || DEFAULT_MAX_RESTART).to_i
68
+ @max_time = (opts[:max_time] || opts[:max_t] || DEFAULT_MAX_TIME).to_i
69
+
70
+ raise ArgumentError.new(":#{@restart_strategy} is not a valid restart strategy") unless RESTART_STRATEGIES.include?(@restart_strategy)
71
+ raise ArgumentError.new(':monitor_interval must be greater than zero') unless @monitor_interval > 0.0
72
+ raise ArgumentError.new(':max_restart must be greater than zero') unless @max_restart > 0
73
+ raise ArgumentError.new(':max_time must be greater than zero') unless @max_time > 0
74
+
75
+ @running = false
22
76
  @mutex = Mutex.new
23
77
  @workers = []
24
- @running = false
25
78
  @monitor = nil
26
- @monitor_interval = opts[:monitor] || opts[:monitor_interval] || DEFAULT_MONITOR_INTERVAL
79
+
80
+ @count = WorkerCounts.new(0, 0, 0)
81
+ @restart_times = []
82
+
27
83
  add_worker(opts[:worker]) unless opts[:worker].nil?
84
+ add_workers(opts[:workers]) unless opts[:workers].nil?
28
85
  end
29
86
 
30
87
  def run!
31
- raise StandardError.new('already running') if running?
32
88
  @mutex.synchronize do
89
+ raise StandardError.new('already running') if @running
33
90
  @running = true
34
- @monitor = Thread.new{ monitor }
35
- @monitor.abort_on_exception = false
91
+ @monitor = Thread.new do
92
+ Thread.current.abort_on_exception = false
93
+ monitor
94
+ end
36
95
  end
37
96
  Thread.pass
38
97
  end
39
98
 
40
99
  def run
41
- raise StandardError.new('already running') if running?
42
- @running = true
100
+ @mutex.synchronize do
101
+ raise StandardError.new('already running') if @running
102
+ @running = true
103
+ end
43
104
  monitor
105
+ return true
44
106
  end
45
107
 
46
108
  def stop
47
- return true unless running?
48
- @running = false
109
+ return true unless @running
49
110
  @mutex.synchronize do
50
- Thread.kill(@monitor) unless @monitor.nil?
51
- @monitor = nil
52
-
53
- until @workers.empty?
54
- context = @workers.pop
55
- begin
56
- context.worker.stop
57
- Thread.pass
58
- rescue Exception => ex
59
- # suppress
60
- ensure
61
- Thread.kill(context.thread) unless context.thread.nil?
111
+ @running = false
112
+ unless @monitor.nil?
113
+ @monitor.run if @monitor.status == 'sleep'
114
+ if @monitor.join(0.1).nil?
115
+ @monitor.kill
62
116
  end
117
+ @monitor = nil
118
+ end
119
+ @restart_times.clear
120
+
121
+ @workers.length.times do |i|
122
+ context = @workers[-1-i]
123
+ terminate_worker(context)
63
124
  end
125
+ prune_workers
64
126
  end
127
+ return true
65
128
  end
66
129
 
67
130
  def running?
68
- return @running
131
+ return @running == true
69
132
  end
70
133
 
71
134
  def length
@@ -73,33 +136,197 @@ module Concurrent
73
136
  end
74
137
  alias_method :size, :length
75
138
 
76
- def add_worker(worker)
77
- if worker.nil? || running? || ! worker.behaves_as?(:runnable)
78
- return false
79
- else
80
- @mutex.synchronize {
81
- @workers << WorkerContext.new(worker)
82
- }
83
- return true
139
+ def current_restart_count
140
+ return @restart_times.length
141
+ end
142
+
143
+ def count
144
+ return @mutex.synchronize do
145
+ @count.status = @workers.collect{|w| w.thread ? w.thread.status : false }
146
+ @count.dup.freeze
147
+ end
148
+ end
149
+
150
+ def add_worker(worker, opts = {})
151
+ return nil if worker.nil? || ! worker.behaves_as?(:runnable)
152
+ return @mutex.synchronize {
153
+ restart = opts[:restart] || :permanent
154
+ type = opts[:type] || (worker.is_a?(Supervisor) ? :supervisor : nil) || :worker
155
+ raise ArgumentError.new(":#{restart} is not a valid restart option") unless CHILD_RESTART_OPTIONS.include?(restart)
156
+ raise ArgumentError.new(":#{type} is not a valid child type") unless CHILD_TYPES.include?(type)
157
+ context = WorkerContext.new(worker, type, restart)
158
+ @workers << context
159
+ @count.add(context)
160
+ worker.run if running?
161
+ context.object_id
162
+ }
163
+ end
164
+ alias_method :add_child, :add_worker
165
+
166
+ def add_workers(workers, opts = {})
167
+ return workers.collect do |worker|
168
+ add_worker(worker, opts)
169
+ end
170
+ end
171
+ alias_method :add_children, :add_workers
172
+
173
+ def remove_worker(worker_id)
174
+ return @mutex.synchronize do
175
+ index, context = find_worker(worker_id)
176
+ break(nil) if context.nil?
177
+ break(false) if context.alive?
178
+ @workers.delete_at(index)
179
+ context.worker
180
+ end
181
+ end
182
+ alias_method :remove_child, :remove_worker
183
+
184
+ def stop_worker(worker_id)
185
+ return true unless running?
186
+ return @mutex.synchronize do
187
+ index, context = find_worker(worker_id)
188
+ break(nil) if index.nil?
189
+ context.terminated = true
190
+ terminate_worker(context)
191
+ @workers.delete_at(index) if @workers[index].restart == :temporary
192
+ true
193
+ end
194
+ end
195
+ alias_method :stop_child, :stop_worker
196
+
197
+ def start_worker(worker_id)
198
+ return false unless running?
199
+ return @mutex.synchronize do
200
+ index, context = find_worker(worker_id)
201
+ break(nil) if context.nil?
202
+ context.terminated = false
203
+ run_worker(context) unless context.alive?
204
+ true
84
205
  end
85
206
  end
207
+ alias_method :start_child, :start_worker
208
+
209
+ def restart_worker(worker_id)
210
+ return false unless running?
211
+ return @mutex.synchronize do
212
+ index, context = find_worker(worker_id)
213
+ break(nil) if context.nil?
214
+ break(false) if context.restart == :temporary
215
+ context.terminated = false
216
+ terminate_worker(context)
217
+ run_worker(context)
218
+ true
219
+ end
220
+ end
221
+ alias_method :restart_child, :restart_worker
86
222
 
87
223
  private
88
224
 
89
225
  def monitor
226
+ @workers.each{|context| run_worker(context)}
90
227
  loop do
228
+ sleep(@monitor_interval)
229
+ break unless running?
91
230
  @mutex.synchronize do
92
- @workers.each do |context|
93
- unless context.thread && context.thread.alive?
94
- context.thread = Thread.new{ context.worker.run }
95
- context.thread.abort_on_exception = false
96
- end
97
- end
231
+ prune_workers
232
+ self.send(@restart_strategy)
98
233
  end
99
234
  break unless running?
100
- sleep(@monitor_interval)
101
- break unless running?
102
235
  end
236
+ rescue MaxRestartFrequencyError => ex
237
+ stop
238
+ end
239
+
240
+ def run_worker(context)
241
+ context.thread = Thread.new do
242
+ Thread.current.abort_on_exception = false
243
+ context.worker.run
244
+ end
245
+ return context
103
246
  end
247
+
248
+ def terminate_worker(context)
249
+ if context.alive?
250
+ context.worker.stop
251
+ Thread.pass
252
+ end
253
+ rescue Exception => ex
254
+ begin
255
+ Thread.kill(context.thread)
256
+ rescue
257
+ # suppress
258
+ end
259
+ ensure
260
+ context.thread = nil
261
+ end
262
+
263
+ def prune_workers
264
+ @workers.delete_if{|w| w.restart == :temporary && ! w.alive? }
265
+ end
266
+
267
+ def find_worker(worker_id)
268
+ index = @workers.find_index{|worker| worker.object_id == worker_id}
269
+ if index.nil?
270
+ return [nil, nil]
271
+ else
272
+ return [index, @workers[index]]
273
+ end
274
+ end
275
+
276
+ def exceeded_max_restart_frequency?
277
+ @restart_times.unshift(Time.now.to_i)
278
+ diff = delta(@restart_times.first, @restart_times.last)
279
+ if @restart_times.length >= @max_restart && diff <= @max_time
280
+ return true
281
+ elsif diff >= @max_time
282
+ @restart_times.pop
283
+ end
284
+ return false
285
+ end
286
+
287
+ #----------------------------------------------------------------
288
+ # restart strategies
289
+
290
+ def one_for_one
291
+ @workers.each do |context|
292
+ if context.needs_restart?
293
+ raise MaxRestartFrequencyError if exceeded_max_restart_frequency?
294
+ run_worker(context)
295
+ end
296
+ end
297
+ end
298
+
299
+ def one_for_all
300
+ restart = false
301
+
302
+ restart = @workers.each do |context|
303
+ if context.needs_restart?
304
+ raise MaxRestartFrequencyError if exceeded_max_restart_frequency?
305
+ break(true)
306
+ end
307
+ end
308
+
309
+ if restart
310
+ @workers.each do |context|
311
+ terminate_worker(context)
312
+ end
313
+ @workers.each{|context| run_worker(context)}
314
+ end
315
+ end
316
+
317
+ def rest_for_one
318
+ restart = false
319
+
320
+ @workers.each do |context|
321
+ if restart
322
+ terminate_worker(context)
323
+ elsif context.needs_restart?
324
+ raise MaxRestartFrequencyError if exceeded_max_restart_frequency?
325
+ restart = true
326
+ end
327
+ end
328
+
329
+ one_for_one if restart
104
330
  end
105
331
  end
332
+ end