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,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
describe 'dataflow' do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
Future.thread_pool = ImmediateExecutor.new
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'raises an exception when no block given' do
|
12
|
+
expect { Concurrent::dataflow }.to raise_error(ArgumentError)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'accepts zero or more dependencies' do
|
16
|
+
Concurrent::dataflow(){0}
|
17
|
+
Concurrent::dataflow(Future.execute{0}){0}
|
18
|
+
Concurrent::dataflow(Future.execute{0}, Future.execute{0}){0}
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'accepts uncompleted dependencies' do
|
22
|
+
d = Future.new{0}
|
23
|
+
Concurrent::dataflow(d){0}
|
24
|
+
d.execute
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'accepts completed dependencies' do
|
28
|
+
d = Future.new{0}
|
29
|
+
d.execute
|
30
|
+
Concurrent::dataflow(d){0}
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an exception if any dependencies are not IVars' do
|
34
|
+
expect { Concurrent::dataflow(nil) }.to raise_error(ArgumentError)
|
35
|
+
expect { Concurrent::dataflow(Future.execute{0}, nil) }.to raise_error(ArgumentError)
|
36
|
+
expect { Concurrent::dataflow(nil, Future.execute{0}) }.to raise_error(ArgumentError)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns a Future' do
|
40
|
+
Concurrent::dataflow{0}.should be_a(Future)
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'does not schedule the Future' do
|
44
|
+
|
45
|
+
specify 'if no dependencies are completed' do
|
46
|
+
d = Future.new{0}
|
47
|
+
f = Concurrent::dataflow(d){0}
|
48
|
+
f.should be_unscheduled
|
49
|
+
d.execute
|
50
|
+
end
|
51
|
+
|
52
|
+
specify 'if one dependency of two is completed' do
|
53
|
+
d1 = Future.new{0}
|
54
|
+
d2 = Future.new{0}
|
55
|
+
f = Concurrent::dataflow(d1, d2){0}
|
56
|
+
d1.execute
|
57
|
+
f.should be_unscheduled
|
58
|
+
d2.execute
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'schedules the Future when all dependencies are available' do
|
63
|
+
|
64
|
+
specify 'if there is just one' do
|
65
|
+
d = Future.new{0}
|
66
|
+
f = Concurrent::dataflow(d){0}
|
67
|
+
d.execute
|
68
|
+
f.value.should eq 0
|
69
|
+
end
|
70
|
+
|
71
|
+
specify 'if there is more than one' do
|
72
|
+
d1 = Future.new{0}
|
73
|
+
d2 = Future.new{0}
|
74
|
+
f = Concurrent::dataflow(d1, d2){0}
|
75
|
+
d1.execute
|
76
|
+
d2.execute
|
77
|
+
f.value.should eq 0
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'counts already executed dependencies' do
|
82
|
+
|
83
|
+
specify 'if there is just one' do
|
84
|
+
d = Future.new{0}
|
85
|
+
d.execute
|
86
|
+
f = Concurrent::dataflow(d){0}
|
87
|
+
f.value.should eq 0
|
88
|
+
end
|
89
|
+
|
90
|
+
specify 'if there is more than one' do
|
91
|
+
d1 = Future.new{0}
|
92
|
+
d2 = Future.new{0}
|
93
|
+
d1.execute
|
94
|
+
d2.execute
|
95
|
+
f = Concurrent::dataflow(d1, d2){0}
|
96
|
+
f.value.should eq 0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'passes the values of dependencies into the block' do
|
101
|
+
|
102
|
+
specify 'if there is just one' do
|
103
|
+
d = Future.new{14}
|
104
|
+
f = Concurrent::dataflow(d) do |v|
|
105
|
+
v
|
106
|
+
end
|
107
|
+
d.execute
|
108
|
+
f.value.should eq 14
|
109
|
+
end
|
110
|
+
|
111
|
+
specify 'if there is more than one' do
|
112
|
+
d1 = Future.new{14}
|
113
|
+
d2 = Future.new{2}
|
114
|
+
f = Concurrent::dataflow(d1, d2) do |v1, v2|
|
115
|
+
v1 + v2
|
116
|
+
end
|
117
|
+
d1.execute
|
118
|
+
d2.execute
|
119
|
+
f.value.should eq 16
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'module function' do
|
124
|
+
|
125
|
+
it 'can be called as Concurrent.dataflow' do
|
126
|
+
|
127
|
+
def fib_with_dot(n)
|
128
|
+
if n < 2
|
129
|
+
Concurrent.dataflow { n }
|
130
|
+
else
|
131
|
+
n1 = fib_with_dot(n - 1)
|
132
|
+
n2 = fib_with_dot(n - 2)
|
133
|
+
Concurrent.dataflow(n1, n2) { n1.value + n2.value }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
expected = fib_with_dot(14)
|
138
|
+
sleep(0.1)
|
139
|
+
expected.value.should eq 377
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'can be called as Concurrent::dataflow' do
|
143
|
+
|
144
|
+
def fib_with_colons(n)
|
145
|
+
if n < 2
|
146
|
+
Concurrent::dataflow { n }
|
147
|
+
else
|
148
|
+
n1 = fib_with_colons(n - 1)
|
149
|
+
n2 = fib_with_colons(n - 2)
|
150
|
+
Concurrent::dataflow(n1, n2) { n1.value + n2.value }
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
expected = fib_with_colons(14)
|
155
|
+
sleep(0.1)
|
156
|
+
expected.value.should eq 377
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
share_examples_for :dereferenceable do
|
4
|
+
|
5
|
+
it 'defaults :dup_on_deref to false' do
|
6
|
+
value = 'value'
|
7
|
+
value.should_not_receive(:dup).with(any_args)
|
8
|
+
|
9
|
+
subject = dereferenceable_subject(value)
|
10
|
+
subject.value.should eq 'value'
|
11
|
+
|
12
|
+
subject = dereferenceable_subject(value, dup_on_deref: false)
|
13
|
+
subject.value.should eq 'value'
|
14
|
+
|
15
|
+
subject = dereferenceable_subject(value, dup: false)
|
16
|
+
subject.value.should eq 'value'
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'calls #dup when the :dup_on_deref option is true' do
|
20
|
+
value = 'value'
|
21
|
+
|
22
|
+
subject = dereferenceable_subject(value, dup_on_deref: true)
|
23
|
+
subject.value.object_id.should_not eq value.object_id
|
24
|
+
subject.value.should eq 'value'
|
25
|
+
|
26
|
+
subject = dereferenceable_subject(value, dup: true)
|
27
|
+
subject.value.object_id.should_not eq value.object_id
|
28
|
+
subject.value.should eq 'value'
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'defaults :freeze_on_deref to false' do
|
32
|
+
value = 'value'
|
33
|
+
value.should_not_receive(:freeze).with(any_args)
|
34
|
+
|
35
|
+
subject = dereferenceable_subject(value)
|
36
|
+
subject.value.should eq 'value'
|
37
|
+
|
38
|
+
subject = dereferenceable_subject(value, freeze_on_deref: false)
|
39
|
+
subject.value.should eq 'value'
|
40
|
+
|
41
|
+
subject = dereferenceable_subject(value, freeze: false)
|
42
|
+
subject.value.should eq 'value'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'calls #freeze when the :freeze_on_deref option is true' do
|
46
|
+
value = 'value'
|
47
|
+
|
48
|
+
subject = dereferenceable_subject(value, freeze_on_deref: true)
|
49
|
+
subject.value.should be_frozen
|
50
|
+
subject.value.should eq 'value'
|
51
|
+
|
52
|
+
subject = dereferenceable_subject(value, freeze: true)
|
53
|
+
subject.value.should be_frozen
|
54
|
+
subject.value.should eq 'value'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'defaults :copy_on_deref to nil' do
|
58
|
+
value = 'value'
|
59
|
+
|
60
|
+
subject = dereferenceable_subject(value)
|
61
|
+
subject.value.object_id.should == value.object_id
|
62
|
+
subject.value.should eq 'value'
|
63
|
+
|
64
|
+
subject = dereferenceable_subject(value, copy_on_deref: nil)
|
65
|
+
subject.value.object_id.should == value.object_id
|
66
|
+
subject.value.should eq 'value'
|
67
|
+
|
68
|
+
subject = dereferenceable_subject(value, copy: nil)
|
69
|
+
subject.value.object_id.should == value.object_id
|
70
|
+
subject.value.should eq 'value'
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'calls the block when the :copy_on_deref option is passed a proc' do
|
74
|
+
value = 'value'
|
75
|
+
copy = proc{|val| 'copy' }
|
76
|
+
|
77
|
+
subject = dereferenceable_subject(value, copy_on_deref: copy)
|
78
|
+
subject.value.object_id.should_not == value.object_id
|
79
|
+
|
80
|
+
subject = dereferenceable_subject(value, copy: copy)
|
81
|
+
subject.value.object_id.should_not == value.object_id
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'calls the :copy block first followed by #dup followed by #freeze' do
|
85
|
+
value = 'value'
|
86
|
+
copied = 'copied'
|
87
|
+
dup = 'dup'
|
88
|
+
frozen = 'frozen'
|
89
|
+
copy = proc{|val| copied }
|
90
|
+
|
91
|
+
copied.should_receive(:dup).at_least(:once).with(no_args).and_return(dup)
|
92
|
+
dup.should_receive(:freeze).at_least(:once).with(no_args).and_return(frozen)
|
93
|
+
|
94
|
+
subject = dereferenceable_subject(value, dup_on_deref: true, freeze_on_deref: true, copy_on_deref: copy)
|
95
|
+
subject.value.should eq frozen
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'does not call #dup when #dup_on_deref is set and the value is nil' do
|
99
|
+
allow_message_expectations_on_nil
|
100
|
+
result = nil
|
101
|
+
result.should_not_receive(:dup).with(any_args)
|
102
|
+
subject = dereferenceable_subject(result, dup_on_deref: true)
|
103
|
+
subject.value
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'does not call #freeze when #freeze_on_deref is set and the value is nil' do
|
107
|
+
allow_message_expectations_on_nil
|
108
|
+
result = nil
|
109
|
+
result.should_not_receive(:freeze).with(any_args)
|
110
|
+
subject = dereferenceable_subject(result, freeze_on_deref: true)
|
111
|
+
subject.value
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'does not call the #copy_on_deref block when the value is nil' do
|
115
|
+
copier = proc { 42 }
|
116
|
+
subject = dereferenceable_subject(nil, copy_on_deref: copier)
|
117
|
+
subject.value.should be_nil
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'locks when all options are false' do
|
121
|
+
subject = dereferenceable_subject(0)
|
122
|
+
mutex = double('mutex')
|
123
|
+
subject.stub(:mutex).and_return(mutex)
|
124
|
+
mutex.should_receive(:synchronize).at_least(:once)
|
125
|
+
subject.value
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'supports dereference flags with observers' do
|
129
|
+
if dereferenceable_subject(0).respond_to?(:add_observer)
|
130
|
+
|
131
|
+
result = 'result'
|
132
|
+
result.should_receive(:dup).and_return(result)
|
133
|
+
result.should_receive(:freeze).and_return(result)
|
134
|
+
copier = proc { result }
|
135
|
+
|
136
|
+
observer = double('observer')
|
137
|
+
observer.should_receive(:update).with(any_args)
|
138
|
+
|
139
|
+
subject = dereferenceable_observable(dup_on_deref: true, freeze_on_deref: true, copy_on_deref: copier)
|
140
|
+
|
141
|
+
subject.add_observer(observer)
|
142
|
+
execute_dereferenceable(subject)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -118,7 +118,7 @@ module Concurrent
|
|
118
118
|
subject.reset
|
119
119
|
@expected = false
|
120
120
|
Thread.new{ subject.wait(0.5); @expected = true}
|
121
|
-
sleep(
|
121
|
+
sleep(1)
|
122
122
|
@expected.should be_true
|
123
123
|
end
|
124
124
|
|
@@ -128,22 +128,57 @@ module Concurrent
|
|
128
128
|
end
|
129
129
|
|
130
130
|
it 'triggers multiple waiting threads' do
|
131
|
+
latch = CountDownLatch.new(5)
|
131
132
|
subject.reset
|
132
|
-
|
133
|
-
5.times{ Thread.new{ subject.wait; @expected << Thread.current.object_id } }
|
133
|
+
5.times{ Thread.new{ subject.wait; latch.count_down } }
|
134
134
|
subject.set
|
135
|
-
|
136
|
-
@expected.length.should eq 5
|
135
|
+
latch.wait(0.2).should be_true
|
137
136
|
end
|
138
137
|
|
139
138
|
it 'behaves appropriately if wait begins while #set is processing' do
|
140
139
|
subject.reset
|
141
|
-
|
140
|
+
latch = CountDownLatch.new(5)
|
142
141
|
5.times{ Thread.new{ subject.wait(5) } }
|
143
142
|
subject.set
|
144
|
-
5.times{ Thread.new{ subject.wait;
|
145
|
-
|
146
|
-
|
143
|
+
5.times{ Thread.new{ subject.wait; latch.count_down } }
|
144
|
+
latch.wait(0.2).should be_true
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'spurious wake ups' do
|
149
|
+
|
150
|
+
before(:each) do
|
151
|
+
def subject.simulate_spurious_wake_up
|
152
|
+
@mutex.synchronize do
|
153
|
+
@condition.signal
|
154
|
+
@condition.broadcast
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'should resist to spurious wake ups without timeout' do
|
160
|
+
@expected = false
|
161
|
+
Thread.new { subject.wait; @expected = true }
|
162
|
+
|
163
|
+
sleep(0.1)
|
164
|
+
subject.simulate_spurious_wake_up
|
165
|
+
|
166
|
+
sleep(0.1)
|
167
|
+
@expected.should be_false
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'should resist to spurious wake ups with timeout' do
|
171
|
+
@expected = false
|
172
|
+
Thread.new { subject.wait(0.5); @expected = true }
|
173
|
+
|
174
|
+
sleep(0.1)
|
175
|
+
subject.simulate_spurious_wake_up
|
176
|
+
|
177
|
+
sleep(0.1)
|
178
|
+
@expected.should be_false
|
179
|
+
|
180
|
+
sleep(0.4)
|
181
|
+
@expected.should be_true
|
147
182
|
end
|
148
183
|
end
|
149
184
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require_relative 'dereferenceable_shared'
|
2
3
|
require_relative 'obligation_shared'
|
3
4
|
require_relative 'uses_global_thread_pool_shared'
|
4
5
|
|
@@ -6,106 +7,199 @@ module Concurrent
|
|
6
7
|
|
7
8
|
describe Future do
|
8
9
|
|
9
|
-
let!(:
|
10
|
-
|
10
|
+
let!(:value) { 10 }
|
11
|
+
subject { Future.new{ value }.execute.tap{ sleep(0.1) } }
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
let(:pending_subject) do
|
16
|
-
Future.new{ sleep(3); fulfilled_value }
|
13
|
+
before(:each) do
|
14
|
+
Future.thread_pool = FixedThreadPool.new(1)
|
17
15
|
end
|
18
16
|
|
19
|
-
|
20
|
-
Future.new{ fulfilled_value }.tap(){ sleep(0.1) }
|
21
|
-
end
|
17
|
+
context 'behavior' do
|
22
18
|
|
23
|
-
|
24
|
-
Future.new{ raise rejected_reason }.tap(){ sleep(0.1) }
|
25
|
-
end
|
19
|
+
# uses_global_thread_pool
|
26
20
|
|
27
|
-
|
28
|
-
|
21
|
+
let!(:thread_pool_user){ Future }
|
22
|
+
it_should_behave_like Concurrent::UsesGlobalThreadPool
|
23
|
+
|
24
|
+
# obligation
|
25
|
+
|
26
|
+
let!(:fulfilled_value) { 10 }
|
27
|
+
let!(:rejected_reason) { StandardError.new('mojo jojo') }
|
28
|
+
|
29
|
+
let(:pending_subject) do
|
30
|
+
Future.new{ sleep(3); fulfilled_value }.execute
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:fulfilled_subject) do
|
34
|
+
Future.new{ fulfilled_value }.execute.tap{ sleep(0.1) }
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:rejected_subject) do
|
38
|
+
Future.new{ raise rejected_reason }.execute.tap{ sleep(0.1) }
|
39
|
+
end
|
40
|
+
|
41
|
+
it_should_behave_like :obligation
|
42
|
+
|
43
|
+
# dereferenceable
|
44
|
+
|
45
|
+
def dereferenceable_subject(value, opts = {})
|
46
|
+
Future.new(opts){ value }.execute.tap{ sleep(0.1) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def dereferenceable_observable(opts = {})
|
50
|
+
Future.new(opts){ 'value' }
|
51
|
+
end
|
52
|
+
|
53
|
+
def execute_dereferenceable(subject)
|
54
|
+
subject.execute
|
55
|
+
sleep(0.1)
|
56
|
+
end
|
57
|
+
|
58
|
+
it_should_behave_like :dereferenceable
|
29
59
|
end
|
30
60
|
|
31
|
-
|
61
|
+
context 'subclassing' do
|
62
|
+
|
63
|
+
subject{ Future.execute{ 42 } }
|
64
|
+
|
65
|
+
it 'protects #set' do
|
66
|
+
expect{ subject.set(100) }.to raise_error
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'protects #fail' do
|
70
|
+
expect{ subject.fail }.to raise_error
|
71
|
+
end
|
32
72
|
|
33
|
-
|
34
|
-
|
35
|
-
|
73
|
+
it 'protects #complete' do
|
74
|
+
expect{ subject.complete(true, 100, nil) }.to raise_error
|
75
|
+
end
|
36
76
|
end
|
37
77
|
|
38
78
|
context '#initialize' do
|
39
79
|
|
40
|
-
it '
|
41
|
-
Future.
|
80
|
+
it 'sets the state to :unscheduled' do
|
81
|
+
Future.new{ nil }.should be_unscheduled
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'does not spawn a new thread' do
|
85
|
+
Future.thread_pool.should_not_receive(:post).with(any_args)
|
86
|
+
Thread.should_not_receive(:new).with(any_args)
|
42
87
|
Future.new{ nil }
|
43
88
|
end
|
44
89
|
|
45
|
-
it '
|
46
|
-
|
47
|
-
|
90
|
+
it 'raises an exception when no block given' do
|
91
|
+
expect {
|
92
|
+
Future.new.execute
|
93
|
+
}.to raise_error(ArgumentError)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
context 'instance #execute' do
|
98
|
+
|
99
|
+
it 'does nothing unless the state is :unscheduled' do
|
100
|
+
Future.should_not_receive(:thread_pool).with(any_args)
|
101
|
+
future = Future.new{ nil }
|
102
|
+
future.instance_variable_set(:@state, :pending)
|
103
|
+
future.execute
|
104
|
+
future.instance_variable_set(:@state, :rejected)
|
105
|
+
future.execute
|
106
|
+
future.instance_variable_set(:@state, :fulfilled)
|
107
|
+
future.execute
|
48
108
|
end
|
49
109
|
|
50
|
-
it '
|
51
|
-
Future.
|
110
|
+
it 'posts the block given on construction' do
|
111
|
+
Future.thread_pool.should_receive(:post).with(any_args)
|
112
|
+
future = Future.new { nil }
|
113
|
+
future.execute
|
52
114
|
end
|
53
115
|
|
54
|
-
it '
|
55
|
-
Future.new.
|
116
|
+
it 'sets the state to :pending' do
|
117
|
+
future = Future.new { sleep(0.1) }
|
118
|
+
future.execute
|
119
|
+
future.should be_pending
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'returns self' do
|
123
|
+
future = Future.new { nil }
|
124
|
+
future.execute.should be future
|
56
125
|
end
|
57
126
|
end
|
58
127
|
|
59
|
-
context '
|
128
|
+
context 'class #execute' do
|
60
129
|
|
61
130
|
before(:each) do
|
62
131
|
Future.thread_pool = ImmediateExecutor.new
|
63
132
|
end
|
64
133
|
|
65
|
-
it '
|
66
|
-
|
134
|
+
it 'creates a new Future' do
|
135
|
+
future = Future.execute{ nil }
|
136
|
+
future.should be_a(Future)
|
137
|
+
end
|
67
138
|
|
68
|
-
|
69
|
-
|
70
|
-
|
139
|
+
it 'passes the block to the new Future' do
|
140
|
+
@expected = false
|
141
|
+
Future.execute { @expected = true }
|
142
|
+
@expected.should be_true
|
143
|
+
end
|
71
144
|
|
72
|
-
|
145
|
+
it 'calls #execute on the new Future' do
|
146
|
+
future = double('future')
|
147
|
+
Future.stub(:new).with(any_args).and_return(future)
|
148
|
+
future.should_receive(:execute).with(no_args)
|
149
|
+
Future.execute{ nil }
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'fulfillment' do
|
154
|
+
|
155
|
+
before(:each) do
|
156
|
+
Future.thread_pool = ImmediateExecutor.new
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'passes all arguments to handler' do
|
160
|
+
@expected = false
|
161
|
+
Future.new{ @expected = true }.execute
|
162
|
+
@expected.should be_true
|
73
163
|
end
|
74
164
|
|
75
165
|
it 'sets the value to the result of the handler' do
|
76
|
-
|
77
|
-
|
166
|
+
future = Future.new{ 42 }.execute
|
167
|
+
future.value.should eq 42
|
78
168
|
end
|
79
169
|
|
80
170
|
it 'sets the state to :fulfilled when the block completes' do
|
81
|
-
|
82
|
-
|
171
|
+
future = Future.new{ 42 }.execute
|
172
|
+
future.should be_fulfilled
|
83
173
|
end
|
84
174
|
|
85
175
|
it 'sets the value to nil when the handler raises an exception' do
|
86
|
-
|
87
|
-
|
176
|
+
future = Future.new{ raise StandardError }.execute
|
177
|
+
future.value.should be_nil
|
88
178
|
end
|
89
179
|
|
90
180
|
it 'sets the state to :rejected when the handler raises an exception' do
|
91
|
-
|
92
|
-
|
181
|
+
future = Future.new{ raise StandardError }.execute
|
182
|
+
future.should be_rejected
|
93
183
|
end
|
94
184
|
|
95
185
|
context 'aliases' do
|
96
186
|
|
97
187
|
it 'aliases #realized? for #fulfilled?' do
|
98
|
-
|
188
|
+
subject.should be_realized
|
99
189
|
end
|
100
190
|
|
101
191
|
it 'aliases #deref for #value' do
|
102
|
-
|
192
|
+
subject.deref.should eq value
|
103
193
|
end
|
104
194
|
end
|
105
195
|
end
|
106
196
|
|
107
197
|
context 'observation' do
|
108
198
|
|
199
|
+
before(:each) do
|
200
|
+
Future.thread_pool = ImmediateExecutor.new
|
201
|
+
end
|
202
|
+
|
109
203
|
let(:clazz) do
|
110
204
|
Class.new do
|
111
205
|
attr_reader :value
|
@@ -122,72 +216,93 @@ module Concurrent
|
|
122
216
|
let(:observer) { clazz.new }
|
123
217
|
|
124
218
|
it 'notifies all observers on fulfillment' do
|
125
|
-
future = Future.new{
|
219
|
+
future = Future.new{ 42 }
|
126
220
|
future.add_observer(observer)
|
127
|
-
|
128
|
-
future.
|
129
|
-
|
221
|
+
|
222
|
+
future.execute
|
223
|
+
|
130
224
|
observer.value.should == 42
|
131
225
|
observer.reason.should be_nil
|
132
226
|
end
|
133
227
|
|
134
228
|
it 'notifies all observers on rejection' do
|
135
|
-
future = Future.new{
|
229
|
+
future = Future.new{ raise StandardError }
|
136
230
|
future.add_observer(observer)
|
137
|
-
|
138
|
-
future.
|
139
|
-
|
231
|
+
|
232
|
+
future.execute
|
233
|
+
|
140
234
|
observer.value.should be_nil
|
141
235
|
observer.reason.should be_a(StandardError)
|
142
236
|
end
|
143
237
|
|
144
238
|
it 'notifies an observer added after fulfillment' do
|
145
|
-
future = Future.new{ 42 }
|
146
|
-
sleep(0.1)
|
147
|
-
future.value.should == 42
|
239
|
+
future = Future.new{ 42 }.execute
|
148
240
|
future.add_observer(observer)
|
149
|
-
sleep(0.1)
|
150
241
|
observer.value.should == 42
|
151
242
|
end
|
152
243
|
|
153
244
|
it 'notifies an observer added after rejection' do
|
154
|
-
future = Future.new{ raise StandardError }
|
155
|
-
sleep(0.1)
|
156
|
-
future.reason.should be_a(StandardError)
|
245
|
+
future = Future.new{ raise StandardError }.execute
|
157
246
|
future.add_observer(observer)
|
158
|
-
sleep(0.1)
|
159
247
|
observer.reason.should be_a(StandardError)
|
160
248
|
end
|
161
249
|
|
162
250
|
it 'does not notify existing observers when a new observer added after fulfillment' do
|
163
|
-
future = Future.new{ 42 }
|
251
|
+
future = Future.new{ 42 }.execute
|
164
252
|
future.add_observer(observer)
|
165
|
-
|
166
|
-
future.value.should == 42
|
253
|
+
|
167
254
|
observer.count.should == 1
|
168
255
|
|
169
256
|
o2 = clazz.new
|
170
257
|
future.add_observer(o2)
|
171
|
-
sleep(0.1)
|
172
258
|
|
173
259
|
observer.count.should == 1
|
174
260
|
o2.value.should == 42
|
175
261
|
end
|
176
262
|
|
177
263
|
it 'does not notify existing observers when a new observer added after rejection' do
|
178
|
-
future = Future.new{ raise StandardError }
|
264
|
+
future = Future.new{ raise StandardError }.execute
|
179
265
|
future.add_observer(observer)
|
180
|
-
|
181
|
-
future.reason.should be_a(StandardError)
|
266
|
+
|
182
267
|
observer.count.should == 1
|
183
268
|
|
184
269
|
o2 = clazz.new
|
185
270
|
future.add_observer(o2)
|
186
|
-
sleep(0.1)
|
187
271
|
|
188
272
|
observer.count.should == 1
|
189
273
|
o2.reason.should be_a(StandardError)
|
190
274
|
end
|
275
|
+
|
276
|
+
context 'deadlock avoidance' do
|
277
|
+
|
278
|
+
def reentrant_observer(future)
|
279
|
+
obs = Object.new
|
280
|
+
obs.define_singleton_method(:update) do |time, value, reason|
|
281
|
+
@value = future.value
|
282
|
+
end
|
283
|
+
obs.define_singleton_method(:value) { @value }
|
284
|
+
obs
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'should notify observers outside mutex lock' do
|
288
|
+
future = Future.new{ 42 }
|
289
|
+
obs = reentrant_observer(future)
|
290
|
+
|
291
|
+
future.add_observer(obs)
|
292
|
+
future.execute
|
293
|
+
|
294
|
+
obs.value.should eq 42
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'should notify a new observer added after fulfillment outside lock' do
|
298
|
+
future = Future.new{ 42 }.execute
|
299
|
+
obs = reentrant_observer(future)
|
300
|
+
|
301
|
+
future.add_observer(obs)
|
302
|
+
|
303
|
+
obs.value.should eq 42
|
304
|
+
end
|
305
|
+
end
|
191
306
|
end
|
192
307
|
end
|
193
308
|
end
|