concurrent-ruby 0.2.2 → 0.3.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
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