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.
- checksums.yaml +4 -4
- data/README.md +31 -33
- data/lib/concurrent.rb +11 -3
- data/lib/concurrent/actor.rb +29 -29
- data/lib/concurrent/agent.rb +98 -16
- data/lib/concurrent/atomic.rb +125 -0
- data/lib/concurrent/channel.rb +36 -1
- data/lib/concurrent/condition.rb +67 -0
- data/lib/concurrent/copy_on_notify_observer_set.rb +80 -0
- data/lib/concurrent/copy_on_write_observer_set.rb +94 -0
- data/lib/concurrent/count_down_latch.rb +60 -0
- data/lib/concurrent/dataflow.rb +85 -0
- data/lib/concurrent/dereferenceable.rb +69 -31
- data/lib/concurrent/event.rb +27 -21
- data/lib/concurrent/future.rb +103 -43
- data/lib/concurrent/ivar.rb +78 -0
- data/lib/concurrent/mvar.rb +154 -0
- data/lib/concurrent/obligation.rb +94 -9
- data/lib/concurrent/postable.rb +11 -9
- data/lib/concurrent/promise.rb +101 -127
- data/lib/concurrent/safe_task_executor.rb +28 -0
- data/lib/concurrent/scheduled_task.rb +60 -54
- data/lib/concurrent/stoppable.rb +2 -2
- data/lib/concurrent/supervisor.rb +36 -29
- data/lib/concurrent/thread_local_var.rb +117 -0
- data/lib/concurrent/timer_task.rb +28 -30
- data/lib/concurrent/utilities.rb +1 -1
- data/lib/concurrent/version.rb +1 -1
- data/spec/concurrent/agent_spec.rb +121 -230
- data/spec/concurrent/atomic_spec.rb +201 -0
- data/spec/concurrent/condition_spec.rb +171 -0
- data/spec/concurrent/copy_on_notify_observer_set_spec.rb +10 -0
- data/spec/concurrent/copy_on_write_observer_set_spec.rb +10 -0
- data/spec/concurrent/count_down_latch_spec.rb +125 -0
- data/spec/concurrent/dataflow_spec.rb +160 -0
- data/spec/concurrent/dereferenceable_shared.rb +145 -0
- data/spec/concurrent/event_spec.rb +44 -9
- data/spec/concurrent/fixed_thread_pool_spec.rb +0 -1
- data/spec/concurrent/future_spec.rb +184 -69
- data/spec/concurrent/ivar_spec.rb +192 -0
- data/spec/concurrent/mvar_spec.rb +380 -0
- data/spec/concurrent/obligation_spec.rb +193 -0
- data/spec/concurrent/observer_set_shared.rb +233 -0
- data/spec/concurrent/postable_shared.rb +3 -7
- data/spec/concurrent/promise_spec.rb +270 -192
- data/spec/concurrent/safe_task_executor_spec.rb +58 -0
- data/spec/concurrent/scheduled_task_spec.rb +142 -38
- data/spec/concurrent/thread_local_var_spec.rb +113 -0
- data/spec/concurrent/thread_pool_shared.rb +2 -3
- data/spec/concurrent/timer_task_spec.rb +31 -1
- data/spec/spec_helper.rb +2 -3
- data/spec/support/functions.rb +4 -0
- data/spec/support/less_than_or_equal_to_matcher.rb +5 -0
- metadata +50 -30
- data/lib/concurrent/contract.rb +0 -21
- data/lib/concurrent/event_machine_defer_proxy.rb +0 -22
- data/md/actor.md +0 -404
- data/md/agent.md +0 -142
- data/md/channel.md +0 -40
- data/md/dereferenceable.md +0 -49
- data/md/future.md +0 -125
- data/md/obligation.md +0 -32
- data/md/promise.md +0 -217
- data/md/scheduled_task.md +0 -156
- data/md/supervisor.md +0 -246
- data/md/thread_pool.md +0 -225
- data/md/timer_task.md +0 -191
- data/spec/concurrent/contract_spec.rb +0 -34
- 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
|
-
|
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
|
-
|
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
|
-
|
30
|
-
@
|
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
|
-
|
36
|
-
|
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
|
-
|
42
|
+
state == :cancelled
|
41
43
|
end
|
42
44
|
|
43
45
|
def in_progress?
|
44
|
-
|
46
|
+
state == :in_progress
|
45
47
|
end
|
46
48
|
|
47
49
|
def cancel
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
63
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
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
|
data/lib/concurrent/stoppable.rb
CHANGED
@@ -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
|
-
|
11
|
+
self
|
12
12
|
end
|
13
13
|
|
14
14
|
protected
|
15
15
|
|
16
16
|
def before_stop_proc
|
17
|
-
|
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
|
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
|
-
|
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
|
-
|
128
|
+
|
129
|
+
true
|
126
130
|
end
|
127
131
|
|
128
132
|
def running?
|
129
|
-
|
133
|
+
@mutex.synchronize { @running }
|
130
134
|
end
|
131
135
|
|
132
136
|
def length
|
133
|
-
|
137
|
+
@mutex.synchronize { @workers.length }
|
134
138
|
end
|
135
139
|
alias_method :size, :length
|
136
140
|
|
137
141
|
def current_restart_count
|
138
|
-
|
142
|
+
@restart_times.length
|
139
143
|
end
|
140
144
|
|
141
145
|
def count
|
142
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
184
|
-
|
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
|
-
|
197
|
-
|
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
|
-
|
209
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
282
|
+
[nil, nil]
|
276
283
|
else
|
277
|
-
|
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
|
-
|
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
|