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.
- 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
|