strand 0.1.0 → 0.2.0.rc0

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 (47) hide show
  1. data/.gitignore +3 -0
  2. data/CHANGELOG +13 -0
  3. data/Gemfile +3 -15
  4. data/LICENSE.txt +1 -0
  5. data/README.rdoc +12 -19
  6. data/Rakefile +10 -57
  7. data/lib/strand.rb +151 -126
  8. data/lib/strand/atc.rb +1 -1
  9. data/lib/strand/em/condition_variable.rb +57 -0
  10. data/lib/strand/em/mutex.rb +63 -0
  11. data/lib/strand/em/queue.rb +84 -0
  12. data/lib/strand/em/thread.rb +305 -0
  13. data/lib/strand/monitor.rb +193 -0
  14. data/lib/strand/version.rb +3 -0
  15. data/spec/spec_helper.rb +9 -5
  16. data/spec/strand/alive.rb +62 -0
  17. data/spec/strand/condition_variable.rb +10 -0
  18. data/spec/strand/condition_variable/broadcast.rb +61 -0
  19. data/spec/strand/condition_variable/signal.rb +62 -0
  20. data/spec/strand/condition_variable/wait.rb +20 -0
  21. data/spec/strand/current.rb +15 -0
  22. data/spec/strand/exit.rb +148 -0
  23. data/spec/strand/join.rb +60 -0
  24. data/spec/strand/local_storage.rb +98 -0
  25. data/spec/strand/mutex.rb +244 -0
  26. data/spec/strand/pass.rb +9 -0
  27. data/spec/strand/queue.rb +124 -0
  28. data/spec/strand/raise.rb +142 -0
  29. data/spec/strand/run.rb +5 -0
  30. data/spec/strand/shared.rb +14 -0
  31. data/spec/strand/sleep.rb +51 -0
  32. data/spec/strand/status.rb +44 -0
  33. data/spec/strand/stop.rb +58 -0
  34. data/spec/strand/strand.rb +32 -0
  35. data/spec/strand/value.rb +39 -0
  36. data/spec/strand/wakeup.rb +60 -0
  37. data/spec/strand_spec.rb +51 -0
  38. data/spec/support/fixtures.rb +305 -0
  39. data/spec/support/scratch.rb +17 -0
  40. data/spec/thread_spec.rb +20 -0
  41. data/strand.gemspec +23 -0
  42. metadata +72 -58
  43. data/Gemfile.lock +0 -40
  44. data/lib/strand/condition_variable.rb +0 -78
  45. data/spec/condition_variable_spec.rb +0 -82
  46. data/test/helper.rb +0 -30
  47. data/test/test_strand.rb +0 -121
@@ -0,0 +1,148 @@
1
+
2
+ shared_examples_for "Strand#exit" do
3
+
4
+ context :exit do
5
+ it "kills sleeping strand" do
6
+ sleeping_strand = Strand.new do
7
+ Strand.sleep
8
+ ScratchPad.record :after_sleep
9
+ end
10
+ sleeping_strand.exit
11
+ sleeping_strand.join
12
+ ScratchPad.recorded.should == nil
13
+ end
14
+
15
+ it "kills current strand" do
16
+ strand = Strand.new do
17
+ Strand.current.kill
18
+ ScratchPad.record :after_sleep
19
+ end
20
+ strand.join
21
+ ScratchPad.recorded.should == nil
22
+ end
23
+
24
+ it "runs ensure clause" do
25
+ strand = StrandSpecs.dying_strand_ensures(:kill) { ScratchPad.record :in_ensure_clause }
26
+ strand.join
27
+ ScratchPad.recorded.should == :in_ensure_clause
28
+ end
29
+
30
+ quarantine! do
31
+ # >1.9.2 has this as undefined
32
+ it "runs nested ensure clauses" do
33
+ ScratchPad.record []
34
+ outer = Strand.new do
35
+ begin
36
+ inner = Strand.new do
37
+ begin
38
+ Strand.sleep
39
+ ensure
40
+ ScratchPad << :inner_ensure_clause
41
+ end
42
+ end
43
+ Strand.sleep
44
+ ensure
45
+ ScratchPad << :outer_ensure_clause
46
+ Strand.pass while inner.status and inner.status != "sleep"
47
+ # exit is private for thread, but not for em::thread
48
+ inner.terminate
49
+ inner.join
50
+ end
51
+ end
52
+ outer.terminate
53
+ outer.join
54
+ ScratchPad.recorded.should include(:inner_ensure_clause)
55
+ ScratchPad.recorded.should include(:outer_ensure_clause)
56
+ end
57
+ end
58
+ it "does not set $!" do
59
+ strand = StrandSpecs.dying_strand_ensures(:kill) { ScratchPad.record $! }
60
+ strand.join
61
+ ScratchPad.recorded.should == nil
62
+ end
63
+
64
+ it "cannot be rescued" do
65
+ strand = Strand.new do
66
+ begin
67
+ Strand.current.kill
68
+ rescue Exception
69
+ ScratchPad.record :in_rescue
70
+ end
71
+ ScratchPad.record :end_of_strand_block
72
+ end
73
+
74
+ strand.join
75
+ ScratchPad.recorded.should == nil
76
+ end
77
+
78
+ it "killing dying running does nothing" do
79
+ # Not applicable for Strands (there can be no "running" status)
80
+ end
81
+
82
+ quarantine! do
83
+
84
+ it "propogates inner exception to Strand.join if there is an outer ensure clause" do
85
+ strand = StrandSpecs.dying_strand_with_outer_ensure(:kill) { }
86
+ lambda { strand.join }.should raise_error(RuntimeError, "In dying strand")
87
+ end
88
+
89
+ it "runs all outer ensure clauses even if inner ensure clause raises exception" do
90
+ strand = StrandSpecs.join_dying_strand_with_outer_ensure(:kill) { ScratchPad.record :in_outer_ensure_clause }
91
+ ScratchPad.recorded.should == :in_outer_ensure_clause
92
+ end
93
+
94
+ it "sets $! in outer ensure clause if inner ensure clause raises exception" do
95
+ strand = StrandSpecs.join_dying_strand_with_outer_ensure(:kill) { ScratchPad.record $! }
96
+ ScratchPad.recorded.to_s.should == "In dying strand"
97
+ end
98
+ end
99
+
100
+ it "can be rescued by outer rescue clause when inner ensure clause raises exception" do
101
+ strand = Strand.new do
102
+ begin
103
+ begin
104
+ Strand.current.send(:kill)
105
+ ensure
106
+ raise "In dying strand"
107
+ end
108
+ rescue Exception
109
+ ScratchPad.record $!
110
+ end
111
+ :end_of_strand_block
112
+ end
113
+
114
+ strand.value.should == :end_of_strand_block
115
+ ScratchPad.recorded.to_s.should == "In dying strand"
116
+ end
117
+
118
+ it "is deferred if ensure clause does Strand.stop" do
119
+ StrandSpecs.wakeup_dying_sleeping_strand(:kill) { Strand.stop; ScratchPad.record :after_sleep }
120
+ ScratchPad.recorded.should == :after_sleep
121
+ end
122
+
123
+ # Hangs on 1.8.6.114 OS X, possibly also on Linux
124
+ # FIX: There is no such thing as not_compliant_on(:ruby)!!!
125
+ quarantine! do
126
+ not_compliant_on(:ruby) do # Doing a sleep in the ensure block hangs the process
127
+ it "is deferred if ensure clause sleeps" do
128
+ StrandSpecs.wakeup_dying_sleeping_strand(:kill) { sleep; ScratchPad.record :after_sleep }
129
+ ScratchPad.recorded.should == :after_sleep
130
+ end
131
+ end
132
+
133
+ # This case occurred in JRuby where native strands are used to provide
134
+ # the same behavior as MRI green strands. Key to this issue was the fact
135
+ # that the strand which called #exit in its block was also being explicitly
136
+ # sent #join from outside the strand. The 100.times provides a certain
137
+ # probability that the deadlock will occur. It was sufficient to reliably
138
+ # reproduce the deadlock in JRuby.
139
+ it "does not deadlock when called from within the strand while being joined from without" do
140
+ 100.times do
141
+ t = Strand.new { Strand.stop; Strand.current.send(:kill) }
142
+ t.wakeup.should == t
143
+ t.join.should == t
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,60 @@
1
+ shared_examples_for "Strand#join" do
2
+
3
+ context :join do
4
+ it "returns the strand when it is finished" do
5
+ t = Strand.new {}
6
+ t.join.should equal(t)
7
+ end
8
+
9
+ it "returns the strand when it is finished when given a timeout" do
10
+ t = Strand.new {}
11
+ t.join
12
+ t.join(0).should equal(t)
13
+ end
14
+
15
+ it "returns nil if it is not finished when given a timeout" do
16
+ c = Strand::Queue.new
17
+ t = Strand.new { c.shift }
18
+ begin
19
+ t.join(0).should == nil
20
+ ensure
21
+ c << true
22
+ end
23
+ t.join.should == t
24
+ end
25
+
26
+ it "accepts a floating point timeout length" do
27
+ c = Strand::Queue.new
28
+ t = Strand.new { c.shift }
29
+ begin
30
+ t.join(0.01).should == nil
31
+ ensure
32
+ c << true
33
+ end
34
+ t.join.should == t
35
+ end
36
+
37
+ it "raises any exceptions encountered in the strand body" do
38
+ t = Strand.new { raise NotImplementedError.new("Just kidding") }
39
+ lambda { t.join }.should raise_error(NotImplementedError)
40
+ end
41
+
42
+ it "returns the dead strand" do
43
+ t = Strand.new { Strand.current.kill }
44
+ t.join.should equal(t)
45
+ end
46
+
47
+ quarantine! do
48
+ # This was pre 1.9 behaviour
49
+ it "returns the dead strand even if an uncaught exception is thrown from ensure block" do
50
+ t = StrandSpecs.dying_strand_ensures { raise "In dying strand" }
51
+ t.join.should equal(t)
52
+ end
53
+ end
54
+
55
+ it "raises any uncaught exception encountered in ensure block" do
56
+ t = StrandSpecs.dying_strand_ensures { raise NotImplementedError.new("Just kidding") }
57
+ lambda { t.join }.should raise_error(NotImplementedError)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,98 @@
1
+
2
+ shared_examples_for "strand local storage" do
3
+
4
+ context "[]" do
5
+ it "gives access to strand local values" do
6
+ th = Strand.new do
7
+ Strand.current[:value] = 5
8
+ end
9
+ th.join
10
+ th[:value].should == 5
11
+ Strand.current[:value].should == nil
12
+ end
13
+
14
+ it "is not shared across strands" do
15
+ t1 = Strand.new do
16
+ Strand.current[:value] = 1
17
+ end
18
+ t2 = Strand.new do
19
+ Strand.current[:value] = 2
20
+ end
21
+ [t1,t2].each {|x| x.join}
22
+ t1[:value].should == 1
23
+ t2[:value].should == 2
24
+ end
25
+
26
+ it "is accessable using strings or symbols" do
27
+ t1 = Strand.new do
28
+ Strand.current[:value] = 1
29
+ end
30
+ t2 = Strand.new do
31
+ Strand.current["value"] = 2
32
+ end
33
+ [t1,t2].each {|x| x.join}
34
+ t1[:value].should == 1
35
+ t1["value"].should == 1
36
+ t2[:value].should == 2
37
+ t2["value"].should == 2
38
+ end
39
+
40
+ it "raises exceptions on the wrong type of keys" do
41
+ lambda { Strand.current[nil] }.should raise_error(TypeError)
42
+ lambda { Strand.current[5] }.should raise_error(TypeError)
43
+ end
44
+ end
45
+
46
+ context "[]=" do
47
+ it "raises exceptions on the wrong type of keys" do
48
+ lambda { Strand.current[nil] = true }.should raise_error(TypeError)
49
+ lambda { Strand.current[5] = true }.should raise_error(TypeError)
50
+ end
51
+ end
52
+
53
+ context "keys" do
54
+ it "returns an array of the names of the thread-local variables as symbols" do
55
+ th = Strand.new do
56
+ Strand.current["cat"] = 'woof'
57
+ Strand.current[:cat] = 'meow'
58
+ Strand.current[:dog] = 'woof'
59
+ end
60
+ th.join
61
+ th.keys.sort_by {|x| x.to_s}.should == [:cat,:dog]
62
+ end
63
+ end
64
+
65
+ context "key?" do
66
+ before :each do
67
+ @th = Strand.new do
68
+ Strand.current[:oliver] = "a"
69
+ end
70
+ @th.join
71
+ end
72
+
73
+ it "tests for existance of strand local variables using symbols or strings" do
74
+ @th.key?(:oliver).should == true
75
+ @th.key?("oliver").should == true
76
+ @th.key?(:stanley).should == false
77
+ @th.key?(:stanley.to_s).should == false
78
+ end
79
+
80
+ quarantine! do
81
+ ruby_version_is ""..."1.9" do
82
+ it "raises exceptions on the wrong type of keys" do
83
+ lambda { Strand.current.key? nil }.should raise_error(TypeError)
84
+ lambda { Strand.current.key? 5 }.should raise_error(ArgumentError)
85
+ end
86
+ end
87
+
88
+ end
89
+
90
+ # Ruby spec says 1.9 should raise TypeError
91
+ it "raises exceptions on the wrong type of keys" do
92
+ lambda { Strand.current.key? nil }.should raise_error(TypeError)
93
+ lambda { Strand.current.key? 5 }.should raise_error(TypeError)
94
+ end
95
+
96
+ end
97
+ end
98
+
@@ -0,0 +1,244 @@
1
+
2
+ # This spec derived from rubyspec for mutex
3
+ shared_examples "a mutex" do
4
+
5
+ context :lock do
6
+
7
+ it "returns self" do
8
+ m = Strand::Mutex.new
9
+ m.lock.should == m
10
+ m.unlock
11
+ end
12
+
13
+ it "waits if the lock is not available" do
14
+ m = Strand::Mutex.new
15
+
16
+ status = nil
17
+ m.lock
18
+
19
+ s = Strand.new do
20
+ m.lock
21
+ status = :after_lock
22
+ end
23
+
24
+ status.should be_nil
25
+ m.unlock
26
+ s.join
27
+ status.should == :after_lock
28
+ end
29
+
30
+ #GG there's no test for this in rubyspec
31
+ # but there is a test below for locked?
32
+ it "acquires a lock previously held by a dead Fiber" do
33
+ m = Strand::Mutex.new
34
+
35
+ m.lock
36
+
37
+ # s1 acquires the lock but does not release it before
38
+ # it dies, something needs to resume s2
39
+ # we
40
+ s1 = Strand.new { m.lock; Strand.pass }
41
+ s2 = Strand.new { m.lock; }
42
+
43
+ m.unlock
44
+ s2.join
45
+ end
46
+
47
+ it "acquires a lock previously held by a killed Fiber" do
48
+ m = Strand::Mutex.new
49
+ m.lock
50
+
51
+ s1 = Strand.new { m.lock; Strand.sleep }
52
+ s2 = Strand.new { m.lock; }
53
+
54
+ m.unlock
55
+ s1.kill
56
+ s2.join
57
+ end
58
+
59
+ it "acquires a lock in a queue behind a killed Fiber" do
60
+
61
+ m = Strand::Mutex.new
62
+ m.lock
63
+ s1 = Strand.new { m.lock }
64
+ s2 = Strand.new { m.lock }
65
+ s3 = Strand.new { m.lock }
66
+
67
+ s2.kill
68
+ m.unlock
69
+ s3.join
70
+
71
+ end
72
+ end
73
+
74
+ context :unlock do
75
+ it "raises StrandError unless Mutex is locked" do
76
+ mutex = Strand::Mutex.new
77
+ lambda { mutex.unlock }.should raise_error(strand_exception)
78
+ end
79
+
80
+ it "raises StrandError unless thread owns Mutex" do
81
+ mutex = Strand::Mutex.new
82
+ wait = Strand::Mutex.new
83
+ wait.lock
84
+
85
+ s = Strand.new do
86
+ mutex.lock
87
+ wait.lock
88
+ end
89
+
90
+ lambda { mutex.unlock }.should raise_error(strand_exception)
91
+
92
+ wait.unlock
93
+ s.join
94
+ end
95
+
96
+ it "raises StrandError if previously locking thread is gone" do
97
+ mutex = Strand::Mutex.new
98
+ s = Strand.new do
99
+ mutex.lock
100
+ end
101
+
102
+ s.join
103
+ #TODO This doesn't make sense, because it would raise error
104
+ # as per above test
105
+ lambda { mutex.unlock }.should raise_error(strand_exception)
106
+ end
107
+ end
108
+
109
+ context :locked do
110
+ it "returns true if locked" do
111
+ m = Strand::Mutex.new
112
+ m.lock
113
+ m.locked?.should be_true
114
+ end
115
+
116
+ it "returns false if unlocked" do
117
+ m = Strand::Mutex.new
118
+ m.locked?.should be_false
119
+ end
120
+
121
+ it "returns the status of the lock" do
122
+
123
+ m1 = Strand::Mutex.new
124
+ m2 = Strand::Mutex.new
125
+
126
+ m2.lock # hold s with only m1 locked
127
+
128
+ s = Strand.new do
129
+ m1.lock
130
+ m2.lock
131
+ end
132
+ Strand.pass while s.status and s.status != "sleep"
133
+ m1.locked?.should be_true
134
+ m2.unlock # release s
135
+ s.join
136
+ #TODO GG implies that I should be able to get m1
137
+ # but there is no test for this case!
138
+ m1.locked?.should be_false
139
+ end
140
+ end
141
+
142
+ context :try_lock do
143
+ it "locks the mutex if it can" do
144
+ m = Strand::Mutex.new
145
+ m.try_lock
146
+
147
+ m.locked?.should be_true
148
+ lambda { m.try_lock.should be_false }.should_not raise_error(strand_exception)
149
+ end
150
+
151
+ it "returns false if lock can not be aquired immediately" do
152
+ m1 = Strand::Mutex.new
153
+ m2 = Strand::Mutex.new
154
+
155
+ m2.lock
156
+ s = Strand.new do
157
+ m1.lock
158
+ m2.lock
159
+ end
160
+ Strand.pass while s.status and s.status != "sleep"
161
+ # s owns m1 so try_lock should return false
162
+ m1.try_lock.should be_false
163
+ m2.unlock
164
+ s.join
165
+ # once th is finished m1 should be released
166
+ m1.try_lock.should be_true
167
+ end
168
+ end
169
+ context :synchronize do
170
+ it "wraps the lock/unlock pair in an ensure" do
171
+ m1 = Strand::Mutex.new
172
+ m2 = Strand::Mutex.new
173
+ m2.lock
174
+
175
+ s = Strand.new do
176
+ lambda do
177
+ m1.synchronize do
178
+ m2.lock
179
+ raise Exception
180
+ end
181
+ end.should raise_error(Exception)
182
+ end
183
+
184
+ Strand.pass while s.status and s.status != "sleep"
185
+ m1.locked?.should be_true
186
+ m2.unlock
187
+ s.join
188
+ m1.locked?.should be_false
189
+ end
190
+ end
191
+
192
+ context :sleep do
193
+ it "raises StrandError if not locked by the current thread" do
194
+ m = Strand::Mutex.new
195
+ lambda { m.sleep }.should raise_error(strand_exception)
196
+ end
197
+
198
+ it "pauses execution for approximately the duration requested" do
199
+ m = Strand::Mutex.new
200
+ m.lock
201
+ duration = 0.1
202
+ start = Time.now
203
+ m.sleep duration
204
+ (Time.now - start).should be_within(0.1).of(duration)
205
+ end
206
+
207
+ it "unlocks the mutex while sleeping" do
208
+ m = Strand::Mutex.new
209
+ s = Strand.new { m.lock; m.sleep }
210
+ Strand.pass while s.status and s.status != "sleep"
211
+ m.locked?.should be_false
212
+ s.run
213
+ s.join
214
+ end
215
+
216
+ it "relocks the mutex when woken" do
217
+ m = Strand::Mutex.new
218
+ m.lock
219
+ m.sleep(0.01)
220
+ m.locked?.should be_true
221
+ end
222
+
223
+ it "relocks the mutex when woken by an exception being raised" do
224
+ m = Strand::Mutex.new
225
+ s = Strand.new do
226
+ m.lock
227
+ begin
228
+ m.sleep
229
+ rescue Exception
230
+ m.locked?
231
+ end
232
+ end
233
+ Strand.pass while s.status and s.status != "sleep"
234
+ s.raise(Exception)
235
+ s.value.should be_true
236
+ end
237
+
238
+ it "returns the rounded number of seconds asleep" do
239
+ m = Strand::Mutex.new
240
+ m.lock
241
+ m.sleep(0.01).should be_kind_of(Integer)
242
+ end
243
+ end
244
+ end