concurrent-ruby 0.4.1 → 0.5.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 (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