concurrent-ruby 0.4.1 → 0.5.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +31 -33
  3. data/lib/concurrent.rb +11 -3
  4. data/lib/concurrent/actor.rb +29 -29
  5. data/lib/concurrent/agent.rb +98 -16
  6. data/lib/concurrent/atomic.rb +125 -0
  7. data/lib/concurrent/channel.rb +36 -1
  8. data/lib/concurrent/condition.rb +67 -0
  9. data/lib/concurrent/copy_on_notify_observer_set.rb +80 -0
  10. data/lib/concurrent/copy_on_write_observer_set.rb +94 -0
  11. data/lib/concurrent/count_down_latch.rb +60 -0
  12. data/lib/concurrent/dataflow.rb +85 -0
  13. data/lib/concurrent/dereferenceable.rb +69 -31
  14. data/lib/concurrent/event.rb +27 -21
  15. data/lib/concurrent/future.rb +103 -43
  16. data/lib/concurrent/ivar.rb +78 -0
  17. data/lib/concurrent/mvar.rb +154 -0
  18. data/lib/concurrent/obligation.rb +94 -9
  19. data/lib/concurrent/postable.rb +11 -9
  20. data/lib/concurrent/promise.rb +101 -127
  21. data/lib/concurrent/safe_task_executor.rb +28 -0
  22. data/lib/concurrent/scheduled_task.rb +60 -54
  23. data/lib/concurrent/stoppable.rb +2 -2
  24. data/lib/concurrent/supervisor.rb +36 -29
  25. data/lib/concurrent/thread_local_var.rb +117 -0
  26. data/lib/concurrent/timer_task.rb +28 -30
  27. data/lib/concurrent/utilities.rb +1 -1
  28. data/lib/concurrent/version.rb +1 -1
  29. data/spec/concurrent/agent_spec.rb +121 -230
  30. data/spec/concurrent/atomic_spec.rb +201 -0
  31. data/spec/concurrent/condition_spec.rb +171 -0
  32. data/spec/concurrent/copy_on_notify_observer_set_spec.rb +10 -0
  33. data/spec/concurrent/copy_on_write_observer_set_spec.rb +10 -0
  34. data/spec/concurrent/count_down_latch_spec.rb +125 -0
  35. data/spec/concurrent/dataflow_spec.rb +160 -0
  36. data/spec/concurrent/dereferenceable_shared.rb +145 -0
  37. data/spec/concurrent/event_spec.rb +44 -9
  38. data/spec/concurrent/fixed_thread_pool_spec.rb +0 -1
  39. data/spec/concurrent/future_spec.rb +184 -69
  40. data/spec/concurrent/ivar_spec.rb +192 -0
  41. data/spec/concurrent/mvar_spec.rb +380 -0
  42. data/spec/concurrent/obligation_spec.rb +193 -0
  43. data/spec/concurrent/observer_set_shared.rb +233 -0
  44. data/spec/concurrent/postable_shared.rb +3 -7
  45. data/spec/concurrent/promise_spec.rb +270 -192
  46. data/spec/concurrent/safe_task_executor_spec.rb +58 -0
  47. data/spec/concurrent/scheduled_task_spec.rb +142 -38
  48. data/spec/concurrent/thread_local_var_spec.rb +113 -0
  49. data/spec/concurrent/thread_pool_shared.rb +2 -3
  50. data/spec/concurrent/timer_task_spec.rb +31 -1
  51. data/spec/spec_helper.rb +2 -3
  52. data/spec/support/functions.rb +4 -0
  53. data/spec/support/less_than_or_equal_to_matcher.rb +5 -0
  54. metadata +50 -30
  55. data/lib/concurrent/contract.rb +0 -21
  56. data/lib/concurrent/event_machine_defer_proxy.rb +0 -22
  57. data/md/actor.md +0 -404
  58. data/md/agent.md +0 -142
  59. data/md/channel.md +0 -40
  60. data/md/dereferenceable.md +0 -49
  61. data/md/future.md +0 -125
  62. data/md/obligation.md +0 -32
  63. data/md/promise.md +0 -217
  64. data/md/scheduled_task.md +0 -156
  65. data/md/supervisor.md +0 -246
  66. data/md/thread_pool.md +0 -225
  67. data/md/timer_task.md +0 -191
  68. data/spec/concurrent/contract_spec.rb +0 -34
  69. data/spec/concurrent/event_machine_defer_proxy_spec.rb +0 -240
@@ -0,0 +1,28 @@
1
+ module Concurrent
2
+
3
+ # A simple utility class that executes a callable and returns and array of three elements:
4
+ # success - indicating if the callable has been executed without errors
5
+ # value - filled by the callable result if it has been executed without errors, nil otherwise
6
+ # reason - the error risen by the callable if it has been executed with errors, nil otherwise
7
+ class SafeTaskExecutor
8
+ def initialize(task)
9
+ @task = task
10
+ end
11
+
12
+ # @return [Array]
13
+ def execute
14
+ success = false
15
+ value = reason = nil
16
+
17
+ begin
18
+ value = @task.call
19
+ success = true
20
+ rescue => ex
21
+ reason = ex
22
+ success = false
23
+ end
24
+
25
+ [success, value, reason]
26
+ end
27
+ end
28
+ end
@@ -1,96 +1,102 @@
1
1
  require 'observer'
2
2
  require 'concurrent/obligation'
3
+ require 'concurrent/safe_task_executor'
3
4
 
4
5
  module Concurrent
5
6
 
6
7
  class ScheduledTask
7
8
  include Obligation
8
- include Observable
9
+
10
+ SchedulingError = Class.new(ArgumentError)
9
11
 
10
12
  attr_reader :schedule_time
11
13
 
12
14
  def initialize(schedule_time, opts = {}, &block)
13
- now = Time.now
14
-
15
- if ! block_given?
16
- raise ArgumentError.new('no block given')
17
- elsif schedule_time.is_a?(Time)
18
- if schedule_time <= now
19
- raise ArgumentError.new('schedule time must be in the future')
20
- else
21
- @schedule_time = schedule_time.dup
22
- end
23
- elsif schedule_time.to_f <= 0.0
24
- raise ArgumentError.new('seconds must be greater than zero')
25
- else
26
- @schedule_time = now + schedule_time.to_f
27
- end
15
+ raise SchedulingError.new('no block given') unless block_given?
16
+ calculate_schedule_time!(schedule_time) # raise exception if in past
28
17
 
29
- @state = :pending
30
- @schedule_time.freeze
18
+ init_obligation
19
+ @observers = CopyOnWriteObserverSet.new
20
+ @state = :unscheduled
21
+ @intended_schedule_time = schedule_time
22
+ @schedule_time = nil
31
23
  @task = block
32
- init_mutex
33
24
  set_deref_options(opts)
25
+ end
34
26
 
35
- @thread = Thread.new{ work }
36
- @thread.abort_on_exception = false
27
+ # @since 0.5.0
28
+ def execute
29
+ if compare_and_set_state(:pending, :unscheduled)
30
+ @schedule_time = calculate_schedule_time!(@intended_schedule_time).freeze
31
+ Thread.new { work }
32
+ self
33
+ end
34
+ end
35
+
36
+ # @since 0.5.0
37
+ def self.execute(schedule_time, opts = {}, &block)
38
+ return ScheduledTask.new(schedule_time, opts, &block).execute
37
39
  end
38
40
 
39
41
  def cancelled?
40
- return @state == :cancelled
42
+ state == :cancelled
41
43
  end
42
44
 
43
45
  def in_progress?
44
- return @state == :in_progress
46
+ state == :in_progress
45
47
  end
46
48
 
47
49
  def cancel
48
- return false if mutex.locked?
49
- return mutex.synchronize do
50
- if @state == :pending
51
- @state = :cancelled
52
- event.set
53
- true
54
- else
55
- false
56
- end
50
+ if_state(:unscheduled, :pending) do
51
+ @state = :cancelled
52
+ event.set
53
+ true
57
54
  end
58
55
  end
56
+
59
57
  alias_method :stop, :cancel
60
58
 
61
59
  def add_observer(observer, func = :update)
62
- return false unless [:pending, :in_progress].include?(@state)
63
- super
60
+ if_state(:unscheduled, :pending, :in_progress) do
61
+ @observers.add_observer(observer, func)
62
+ end
64
63
  end
65
64
 
66
65
  protected
67
66
 
68
67
  def work
69
- while (diff = @schedule_time.to_f - Time.now.to_f) > 0
70
- sleep( diff > 60 ? 60 : diff )
71
- end
72
-
73
- if @state == :pending
68
+ sleep_until_scheduled_time
69
+
70
+ if compare_and_set_state(:in_progress, :pending)
71
+ success, val, reason = SafeTaskExecutor.new(@task).execute
72
+
74
73
  mutex.synchronize do
75
- @state = :in_progress
76
- begin
77
- @value = @task.call
78
- @state = :fulfilled
79
- rescue => ex
80
- @reason = ex
81
- @state = :rejected
82
- ensure
83
- changed
84
- end
74
+ set_state(success, val, reason)
75
+ event.set
85
76
  end
77
+
78
+ time = Time.now
79
+ @observers.notify_and_delete_observers{ [time, self.value, reason] }
86
80
  end
87
81
 
88
- if self.changed?
89
- notify_observers(Time.now, self.value, @reason)
90
- delete_observers
82
+ end
83
+
84
+ private
85
+
86
+ def sleep_until_scheduled_time
87
+ while (diff = @schedule_time.to_f - Time.now.to_f) > 0
88
+ sleep(diff > 60 ? 60 : diff)
89
+ end
90
+ end
91
+
92
+ def calculate_schedule_time!(schedule_time, now = Time.now)
93
+ if schedule_time.is_a?(Time)
94
+ raise SchedulingError.new('schedule time must be in the future') if schedule_time <= now
95
+ schedule_time.dup
96
+ else
97
+ raise SchedulingError.new('seconds must be greater than zero') if schedule_time.to_f <= 0.0
98
+ now + schedule_time.to_f
91
99
  end
92
- event.set
93
- self.stop
94
100
  end
95
101
  end
96
102
  end
@@ -8,13 +8,13 @@ module Concurrent
8
8
  raise ArgumentError.new('no block given') unless block_given?
9
9
  raise Runnable::LifecycleError.new('#before_stop already set') if @before_stop_proc
10
10
  @before_stop_proc = block
11
- return self
11
+ self
12
12
  end
13
13
 
14
14
  protected
15
15
 
16
16
  def before_stop_proc
17
- return @before_stop_proc
17
+ @before_stop_proc
18
18
  end
19
19
  end
20
20
  end
@@ -20,10 +20,12 @@ module Concurrent
20
20
  WorkerContext = Struct.new(:worker, :type, :restart) do
21
21
  attr_accessor :thread
22
22
  attr_accessor :terminated
23
+
23
24
  def alive?() return thread && thread.alive?; end
25
+
24
26
  def needs_restart?
25
27
  return false if thread && thread.alive?
26
- return false if terminated == true
28
+ return false if terminated
27
29
  case self.restart
28
30
  when :permanent
29
31
  return true
@@ -42,12 +44,12 @@ module Concurrent
42
44
  self.supervisors += 1 if context.type == :supervisor
43
45
  self.workers += 1 if context.type == :worker
44
46
  end
45
- def active() sleeping + running + aborting end;
46
- def sleeping() @status.reduce(0){|x, s| x += (s == 'sleep' ? 1 : 0) } end;
47
- def running() @status.reduce(0){|x, s| x += (s == 'run' ? 1 : 0) } end;
48
- def aborting() @status.reduce(0){|x, s| x += (s == 'aborting' ? 1 : 0) } end;
49
- def stopped() @status.reduce(0){|x, s| x += (s == false ? 1 : 0) } end;
50
- def abend() @status.reduce(0){|x, s| x += (s.nil? ? 1 : 0) } 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
51
53
  end
52
54
 
53
55
  attr_reader :monitor_interval
@@ -100,12 +102,13 @@ module Concurrent
100
102
  @running = true
101
103
  end
102
104
  monitor
103
- return true
105
+ true
104
106
  end
105
107
 
106
108
  def stop
107
- return true unless @running
108
109
  @mutex.synchronize do
110
+ return true unless @running
111
+
109
112
  @running = false
110
113
  unless @monitor.nil?
111
114
  @monitor.run if @monitor.status == 'sleep'
@@ -122,24 +125,25 @@ module Concurrent
122
125
  end
123
126
  prune_workers
124
127
  end
125
- return true
128
+
129
+ true
126
130
  end
127
131
 
128
132
  def running?
129
- return @running == true
133
+ @mutex.synchronize { @running }
130
134
  end
131
135
 
132
136
  def length
133
- return @workers.length
137
+ @mutex.synchronize { @workers.length }
134
138
  end
135
139
  alias_method :size, :length
136
140
 
137
141
  def current_restart_count
138
- return @restart_times.length
142
+ @restart_times.length
139
143
  end
140
144
 
141
145
  def count
142
- return @mutex.synchronize do
146
+ @mutex.synchronize do
143
147
  @count.status = @workers.collect{|w| w.thread ? w.thread.status : false }
144
148
  @count.dup.freeze
145
149
  end
@@ -147,7 +151,7 @@ module Concurrent
147
151
 
148
152
  def add_worker(worker, opts = {})
149
153
  return nil if worker.nil? || ! behaves_as_worker?(worker)
150
- return @mutex.synchronize {
154
+ @mutex.synchronize {
151
155
  restart = opts[:restart] || :permanent
152
156
  type = opts[:type] || (worker.is_a?(Supervisor) ? :supervisor : nil) || :worker
153
157
  raise ArgumentError.new(":#{restart} is not a valid restart option") unless CHILD_RESTART_OPTIONS.include?(restart)
@@ -155,21 +159,21 @@ module Concurrent
155
159
  context = WorkerContext.new(worker, type, restart)
156
160
  @workers << context
157
161
  @count.add(context)
158
- worker.run if running?
162
+ worker.run if @running
159
163
  context.object_id
160
164
  }
161
165
  end
162
166
  alias_method :add_child, :add_worker
163
167
 
164
168
  def add_workers(workers, opts = {})
165
- return workers.collect do |worker|
169
+ workers.collect do |worker|
166
170
  add_worker(worker, opts)
167
171
  end
168
172
  end
169
173
  alias_method :add_children, :add_workers
170
174
 
171
175
  def remove_worker(worker_id)
172
- return @mutex.synchronize do
176
+ @mutex.synchronize do
173
177
  index, context = find_worker(worker_id)
174
178
  break(nil) if context.nil?
175
179
  break(false) if context.alive?
@@ -180,8 +184,9 @@ module Concurrent
180
184
  alias_method :remove_child, :remove_worker
181
185
 
182
186
  def stop_worker(worker_id)
183
- return true unless running?
184
- return @mutex.synchronize do
187
+ @mutex.synchronize do
188
+ return true unless @running
189
+
185
190
  index, context = find_worker(worker_id)
186
191
  break(nil) if index.nil?
187
192
  context.terminated = true
@@ -193,8 +198,9 @@ module Concurrent
193
198
  alias_method :stop_child, :stop_worker
194
199
 
195
200
  def start_worker(worker_id)
196
- return false unless running?
197
- return @mutex.synchronize do
201
+ @mutex.synchronize do
202
+ return false unless @running
203
+
198
204
  index, context = find_worker(worker_id)
199
205
  break(nil) if context.nil?
200
206
  context.terminated = false
@@ -205,8 +211,9 @@ module Concurrent
205
211
  alias_method :start_child, :start_worker
206
212
 
207
213
  def restart_worker(worker_id)
208
- return false unless running?
209
- return @mutex.synchronize do
214
+ @mutex.synchronize do
215
+ return false unless @running
216
+
210
217
  index, context = find_worker(worker_id)
211
218
  break(nil) if context.nil?
212
219
  break(false) if context.restart == :temporary
@@ -221,7 +228,7 @@ module Concurrent
221
228
  private
222
229
 
223
230
  def behaves_as_worker?(obj)
224
- return WORKER_API.each do |method, arity|
231
+ WORKER_API.each do |method, arity|
225
232
  break(false) unless obj.respond_to?(method) && obj.method(method).arity == arity
226
233
  true
227
234
  end
@@ -247,7 +254,7 @@ module Concurrent
247
254
  Thread.current.abort_on_exception = false
248
255
  context.worker.run
249
256
  end
250
- return context
257
+ context
251
258
  end
252
259
 
253
260
  def terminate_worker(context)
@@ -272,9 +279,9 @@ module Concurrent
272
279
  def find_worker(worker_id)
273
280
  index = @workers.find_index{|worker| worker.object_id == worker_id}
274
281
  if index.nil?
275
- return [nil, nil]
282
+ [nil, nil]
276
283
  else
277
- return [index, @workers[index]]
284
+ [index, @workers[index]]
278
285
  end
279
286
  end
280
287
 
@@ -286,7 +293,7 @@ module Concurrent
286
293
  elsif diff >= @max_time
287
294
  @restart_times.pop
288
295
  end
289
- return false
296
+ false
290
297
  end
291
298
 
292
299
  #----------------------------------------------------------------
@@ -0,0 +1,117 @@
1
+ module Concurrent
2
+
3
+ module ThreadLocalSymbolAllocator
4
+
5
+ COUNTER = AtomicFixnum.new
6
+
7
+ protected
8
+
9
+ def allocate_symbol
10
+ # Warning: this symbol may never be deallocated
11
+ @symbol = :"thread_local_symbol_#{COUNTER.increment}"
12
+ end
13
+
14
+ end
15
+
16
+ module ThreadLocalOldStorage
17
+
18
+ include ThreadLocalSymbolAllocator
19
+
20
+ protected
21
+
22
+ def allocate_storage
23
+ allocate_symbol
24
+ end
25
+
26
+ def get
27
+ Thread.current[@symbol]
28
+ end
29
+
30
+ def set(value)
31
+ Thread.current[@symbol] = value
32
+ end
33
+
34
+ end
35
+
36
+ module ThreadLocalNewStorage
37
+
38
+ include ThreadLocalSymbolAllocator
39
+
40
+ protected
41
+
42
+ def allocate_storage
43
+ allocate_symbol
44
+ end
45
+
46
+ def get
47
+ Thread.current.thread_variable_get(@symbol)
48
+ end
49
+
50
+ def set(value)
51
+ Thread.current.thread_variable_set(@symbol, value)
52
+ end
53
+
54
+ end
55
+
56
+ module ThreadLocalJavaStorage
57
+
58
+ protected
59
+
60
+ def allocate_storage
61
+ @var = java.lang.ThreadLocal.new
62
+ end
63
+
64
+ def get
65
+ @var.get
66
+ end
67
+
68
+ def set(value)
69
+ @var.set(value)
70
+ end
71
+
72
+ end
73
+
74
+ class ThreadLocalVar
75
+
76
+ NIL_SENTINEL = Object.new
77
+
78
+ if defined? java.lang
79
+ include ThreadLocalJavaStorage
80
+ elsif Thread.current.respond_to?(:thread_variable_set)
81
+ include ThreadLocalNewStorage
82
+ else
83
+ include ThreadLocalOldStorage
84
+ end
85
+
86
+ def initialize(default = nil)
87
+ @default = default
88
+ allocate_storage
89
+ end
90
+
91
+ def value
92
+ value = get
93
+
94
+ if value.nil?
95
+ @default
96
+ elsif value == NIL_SENTINEL
97
+ nil
98
+ else
99
+ value
100
+ end
101
+ end
102
+
103
+ def value=(value)
104
+ if value.nil?
105
+ stored_value = NIL_SENTINEL
106
+ else
107
+ stored_value = value
108
+ end
109
+
110
+ set stored_value
111
+
112
+ value
113
+ end
114
+
115
+ end
116
+
117
+ end