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.
- data/.gitignore +3 -0
- data/CHANGELOG +13 -0
- data/Gemfile +3 -15
- data/LICENSE.txt +1 -0
- data/README.rdoc +12 -19
- data/Rakefile +10 -57
- data/lib/strand.rb +151 -126
- data/lib/strand/atc.rb +1 -1
- data/lib/strand/em/condition_variable.rb +57 -0
- data/lib/strand/em/mutex.rb +63 -0
- data/lib/strand/em/queue.rb +84 -0
- data/lib/strand/em/thread.rb +305 -0
- data/lib/strand/monitor.rb +193 -0
- data/lib/strand/version.rb +3 -0
- data/spec/spec_helper.rb +9 -5
- data/spec/strand/alive.rb +62 -0
- data/spec/strand/condition_variable.rb +10 -0
- data/spec/strand/condition_variable/broadcast.rb +61 -0
- data/spec/strand/condition_variable/signal.rb +62 -0
- data/spec/strand/condition_variable/wait.rb +20 -0
- data/spec/strand/current.rb +15 -0
- data/spec/strand/exit.rb +148 -0
- data/spec/strand/join.rb +60 -0
- data/spec/strand/local_storage.rb +98 -0
- data/spec/strand/mutex.rb +244 -0
- data/spec/strand/pass.rb +9 -0
- data/spec/strand/queue.rb +124 -0
- data/spec/strand/raise.rb +142 -0
- data/spec/strand/run.rb +5 -0
- data/spec/strand/shared.rb +14 -0
- data/spec/strand/sleep.rb +51 -0
- data/spec/strand/status.rb +44 -0
- data/spec/strand/stop.rb +58 -0
- data/spec/strand/strand.rb +32 -0
- data/spec/strand/value.rb +39 -0
- data/spec/strand/wakeup.rb +60 -0
- data/spec/strand_spec.rb +51 -0
- data/spec/support/fixtures.rb +305 -0
- data/spec/support/scratch.rb +17 -0
- data/spec/thread_spec.rb +20 -0
- data/strand.gemspec +23 -0
- metadata +72 -58
- data/Gemfile.lock +0 -40
- data/lib/strand/condition_variable.rb +0 -78
- data/spec/condition_variable_spec.rb +0 -82
- data/test/helper.rb +0 -30
- data/test/test_strand.rb +0 -121
data/spec/strand/pass.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
shared_examples_for "a queue" do
|
2
|
+
|
3
|
+
# These specs are derived from rubyspec for ruby's standard Queue class
|
4
|
+
|
5
|
+
context :enqueue do
|
6
|
+
|
7
|
+
it "adds an element to the Queue" do
|
8
|
+
q = Strand::Queue.new
|
9
|
+
q.size.should == 0
|
10
|
+
q << Object.new
|
11
|
+
q.size.should == 1
|
12
|
+
q.push(Object.new)
|
13
|
+
q.size.should == 2
|
14
|
+
q.enq(Object.new)
|
15
|
+
q.size.should == 3
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
context :dequeue do
|
21
|
+
it "removes an item from the Queue" do
|
22
|
+
q = Strand::Queue.new
|
23
|
+
q << Object.new
|
24
|
+
q.size.should == 1
|
25
|
+
q.pop
|
26
|
+
q.size.should == 0
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns items in the order they were added" do
|
30
|
+
q = Strand::Queue.new
|
31
|
+
q << 1
|
32
|
+
q << 2
|
33
|
+
q.deq.should == 1
|
34
|
+
q.shift.should == 2
|
35
|
+
end
|
36
|
+
|
37
|
+
it "blocks until there are items in the queue" do
|
38
|
+
q = Strand::Queue.new
|
39
|
+
v = 0
|
40
|
+
|
41
|
+
s = Strand.new do
|
42
|
+
q.pop
|
43
|
+
v = 1
|
44
|
+
end
|
45
|
+
|
46
|
+
v.should == 0
|
47
|
+
q << Object.new
|
48
|
+
s.join()
|
49
|
+
v.should == 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "raises a StrandError if Queue is empty" do
|
53
|
+
q = Strand::Queue.new
|
54
|
+
lambda { q.pop(true) }.should raise_error(strand_exception)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context :length do
|
59
|
+
it "returns the number of elements" do
|
60
|
+
q = Strand::Queue.new
|
61
|
+
q.length.should == 0
|
62
|
+
q << Object.new
|
63
|
+
q << Object.new
|
64
|
+
q.length.should == 2
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context :empty do
|
69
|
+
it "returns true on an empty Queue" do
|
70
|
+
q = Strand::Queue.new
|
71
|
+
q.empty?.should be_true
|
72
|
+
end
|
73
|
+
|
74
|
+
it "returns false when Queue is not empty" do
|
75
|
+
q = Strand::Queue.new
|
76
|
+
q << Object.new
|
77
|
+
q.empty?.should be_false
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context :num_waiting do
|
82
|
+
it "reports the number of Strands waiting on the Queue" do
|
83
|
+
q = Strand::Queue.new
|
84
|
+
fibers = []
|
85
|
+
|
86
|
+
5.times do |i|
|
87
|
+
q.num_waiting.should == i
|
88
|
+
f = Strand.new { q.deq }
|
89
|
+
Strand.pass until f.status and f.status == 'sleep'
|
90
|
+
fibers << f
|
91
|
+
end
|
92
|
+
|
93
|
+
fibers.each { q.enq Object.new }
|
94
|
+
|
95
|
+
fibers.each { |f| Strand.pass while f.alive? }
|
96
|
+
|
97
|
+
q.num_waiting.should == 0
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context :doc_example do
|
102
|
+
it "handles the doc example" do
|
103
|
+
queue = Strand::Queue.new
|
104
|
+
|
105
|
+
producer = Strand.new do
|
106
|
+
5.times do |i|
|
107
|
+
Strand.sleep rand(i/4) # simulate expense
|
108
|
+
queue << i
|
109
|
+
puts "#{i} produced"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
consumer = Strand.new do
|
114
|
+
5.times do |i|
|
115
|
+
value = queue.pop
|
116
|
+
Strand.sleep rand(i/8) # simulate expense
|
117
|
+
puts "consumed #{value}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
consumer.join
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
shared_examples_for "Strand#raise" do
|
2
|
+
context :raise do
|
3
|
+
it "ignores dead strands" do
|
4
|
+
t = Strand.new { :dead }
|
5
|
+
Strand.pass while t.alive?
|
6
|
+
lambda {t.raise("Kill the strand")}.should_not raise_error
|
7
|
+
lambda {t.value}.should_not raise_error
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context "raise on a sleeping strand" do
|
12
|
+
before :each do
|
13
|
+
ScratchPad.clear
|
14
|
+
@str = StrandSpecs.sleeping_strand
|
15
|
+
Strand.pass while @str.status and @str.status != "sleep"
|
16
|
+
end
|
17
|
+
|
18
|
+
after :each do
|
19
|
+
@str.kill
|
20
|
+
end
|
21
|
+
|
22
|
+
it "raises a RuntimeError if no exception class is given" do
|
23
|
+
@str.raise
|
24
|
+
Strand.pass while @str.status
|
25
|
+
ScratchPad.recorded.should be_kind_of(RuntimeError)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "raises the given exception" do
|
29
|
+
@str.raise Exception
|
30
|
+
Strand.pass while @str.status
|
31
|
+
ScratchPad.recorded.should be_kind_of(Exception)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "raises the given exception with the given message" do
|
35
|
+
@str.raise Exception, "get to work"
|
36
|
+
Strand.pass while @str.status
|
37
|
+
ScratchPad.recorded.should be_kind_of(Exception)
|
38
|
+
ScratchPad.recorded.message.should == "get to work"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "is captured and raised by Strand#value" do
|
42
|
+
t = Strand.new do
|
43
|
+
Strand.sleep
|
44
|
+
end
|
45
|
+
|
46
|
+
StrandSpecs.spin_until_sleeping(t)
|
47
|
+
|
48
|
+
t.raise
|
49
|
+
lambda { t.value }.should raise_error(RuntimeError)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "raises a RuntimeError when called with no arguments" do
|
53
|
+
t = Strand.new do
|
54
|
+
begin
|
55
|
+
1/0
|
56
|
+
rescue ZeroDivisionError
|
57
|
+
Strand.sleep 3
|
58
|
+
end
|
59
|
+
end
|
60
|
+
begin
|
61
|
+
raise RangeError
|
62
|
+
rescue
|
63
|
+
StrandSpecs.spin_until_sleeping(t)
|
64
|
+
t.raise
|
65
|
+
end
|
66
|
+
lambda {t.value}.should raise_error(RuntimeError)
|
67
|
+
t.kill
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
#TODO do these make sense?
|
72
|
+
quarantine! do
|
73
|
+
|
74
|
+
context "Strand#raise on a running strand" do
|
75
|
+
before :each do
|
76
|
+
ScratchPad.clear
|
77
|
+
StrandSpecs.clear_state
|
78
|
+
|
79
|
+
@str = StrandSpecs.running_strand
|
80
|
+
Strand.pass until StrandSpecs.state == :running
|
81
|
+
end
|
82
|
+
|
83
|
+
after :each do
|
84
|
+
@str.kill
|
85
|
+
end
|
86
|
+
|
87
|
+
it "raises a RuntimeError if no exception class is given" do
|
88
|
+
@str.raise
|
89
|
+
Strand.pass while @str.status
|
90
|
+
ScratchPad.recorded.should be_kind_of(RuntimeError)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "raises the given exception" do
|
94
|
+
@str.raise Exception
|
95
|
+
Strand.pass while @str.status
|
96
|
+
ScratchPad.recorded.should be_kind_of(Exception)
|
97
|
+
end
|
98
|
+
|
99
|
+
it "raises the given exception with the given message" do
|
100
|
+
@str.raise Exception, "get to work"
|
101
|
+
Strand.pass while @str.status
|
102
|
+
ScratchPad.recorded.should be_kind_of(Exception)
|
103
|
+
ScratchPad.recorded.message.should == "get to work"
|
104
|
+
end
|
105
|
+
|
106
|
+
it "can go unhandled" do
|
107
|
+
t = Strand.new do
|
108
|
+
loop {}
|
109
|
+
end
|
110
|
+
|
111
|
+
t.raise
|
112
|
+
lambda {t.value}.should raise_error(RuntimeError)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "raise the given argument even when there is an active exception" do
|
116
|
+
raised = false
|
117
|
+
t = Strand.new do
|
118
|
+
begin
|
119
|
+
1/0
|
120
|
+
rescue ZeroDivisionError
|
121
|
+
raised = true
|
122
|
+
loop { }
|
123
|
+
end
|
124
|
+
end
|
125
|
+
begin
|
126
|
+
raise "Create an active exception for the current strand too"
|
127
|
+
rescue
|
128
|
+
Strand.pass until raised || !t.alive?
|
129
|
+
t.raise RangeError
|
130
|
+
lambda {t.value}.should raise_error(RangeError)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
136
|
+
#TODO find out what the spec for :kernel_raise is
|
137
|
+
quarantine! do
|
138
|
+
context "Strand#raise on same strand" do
|
139
|
+
it_behaves_like :kernel_raise, :raise, Strand.current
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
data/spec/strand/run.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
|
2
|
+
require 'strand/strand'
|
3
|
+
require 'strand/local_storage'
|
4
|
+
require 'strand/mutex'
|
5
|
+
require 'strand/queue'
|
6
|
+
require 'strand/condition_variable'
|
7
|
+
|
8
|
+
shared_examples_for Strand do
|
9
|
+
it_behaves_like "a strand"
|
10
|
+
it_behaves_like "strand local storage"
|
11
|
+
it_behaves_like "a mutex"
|
12
|
+
it_behaves_like "a queue"
|
13
|
+
it_behaves_like "a condition variable"
|
14
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
shared_examples_for "Strand#sleep" do
|
3
|
+
context "sleep" do
|
4
|
+
it "pauses execution for approximately the duration requested" do
|
5
|
+
duration = 0.1
|
6
|
+
start = Time.now
|
7
|
+
Strand.sleep duration
|
8
|
+
(Time.now - start).should be_within(0.1).of(duration)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns the rounded number of seconds asleep" do
|
12
|
+
Strand.sleep(0.01).should be_kind_of(Integer)
|
13
|
+
end
|
14
|
+
it "raises a TypeError when passed a non-numeric duration" do
|
15
|
+
# Kernel.sleep raises error for nil, Strand.sleep will sleep forever
|
16
|
+
#lambda { Strand.sleep(nil) }.should raise_error(TypeError)
|
17
|
+
lambda { Strand.sleep('now') }.should raise_error(TypeError)
|
18
|
+
lambda { Strand.sleep('2') }.should raise_error(TypeError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "pauses execution indefinitely if not given a duration" do
|
22
|
+
lock = Strand::Queue.new
|
23
|
+
t = Strand.new do
|
24
|
+
lock << :ready
|
25
|
+
Strand.sleep
|
26
|
+
5
|
27
|
+
end
|
28
|
+
lock.shift.should == :ready
|
29
|
+
# wait until the thread has gone to sleep
|
30
|
+
Strand.pass while t.status and t.status != "sleep"
|
31
|
+
t.run
|
32
|
+
t.value.should == 5
|
33
|
+
end
|
34
|
+
|
35
|
+
# Strand.sleep handles nil differently to kernel.sleep
|
36
|
+
it "pauses execution indefinitely if given a nil duration" do
|
37
|
+
lock = Strand::Queue.new
|
38
|
+
t = Strand.new do
|
39
|
+
lock << :ready
|
40
|
+
Strand.sleep(nil)
|
41
|
+
5
|
42
|
+
end
|
43
|
+
lock.shift.should == :ready
|
44
|
+
# wait until the thread has gone to sleep
|
45
|
+
Strand.pass while t.status and t.status != "sleep"
|
46
|
+
t.run
|
47
|
+
t.value.should == 5
|
48
|
+
end
|
49
|
+
it "needs to be reviewed for spec completeness"
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
shared_examples_for "Strand#status" do
|
2
|
+
context :status do
|
3
|
+
it "can check it's own status" do
|
4
|
+
StrandSpecs.status_of_current_strand.status.should == 'run'
|
5
|
+
end
|
6
|
+
|
7
|
+
quarantine! do
|
8
|
+
# There's no way to interact with a running strand from another strand
|
9
|
+
it "describes a running strand" do
|
10
|
+
StrandSpecs.status_of_running_strand.status.should == 'run'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it "describes a sleeping strand" do
|
15
|
+
StrandSpecs.status_of_sleeping_strand.status.should == 'sleep'
|
16
|
+
end
|
17
|
+
|
18
|
+
it "describes a blocked strand" do
|
19
|
+
StrandSpecs.status_of_blocked_strand.status.should == 'sleep'
|
20
|
+
end
|
21
|
+
|
22
|
+
it "describes a completed strand" do
|
23
|
+
StrandSpecs.status_of_completed_strand.status.should == false
|
24
|
+
end
|
25
|
+
|
26
|
+
it "describes a killed strand" do
|
27
|
+
StrandSpecs.status_of_killed_strand.status.should == false
|
28
|
+
end
|
29
|
+
|
30
|
+
it "describes a strand with an uncaught exception" do
|
31
|
+
StrandSpecs.status_of_strand_with_uncaught_exception.status.should == nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it "describes a dying sleeping strand" do
|
35
|
+
StrandSpecs.status_of_dying_sleeping_strand.status.should == 'sleep'
|
36
|
+
end
|
37
|
+
|
38
|
+
quarantine! do
|
39
|
+
it "reports aborting on a killed strand" do
|
40
|
+
StrandSpecs.status_of_aborting_strand.status.should == 'aborting'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/spec/strand/stop.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
|
2
|
+
shared_examples_for "Strand#stop" do
|
3
|
+
|
4
|
+
context :stop do
|
5
|
+
it "causes the current strand to sleep indefinitely" do
|
6
|
+
t = Strand.new { Strand.stop; 5 }
|
7
|
+
Strand.pass while t.status and t.status != 'sleep'
|
8
|
+
t.status.should == 'sleep'
|
9
|
+
t.run
|
10
|
+
t.value.should == 5
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "stop?" do
|
15
|
+
it "can check it's own status" do
|
16
|
+
StrandSpecs.status_of_current_strand.stop?.should == false
|
17
|
+
end
|
18
|
+
quarantine! do
|
19
|
+
#Can't really have a running strand
|
20
|
+
it "describes a running strand" do
|
21
|
+
StrandSpecs.status_of_running_strand.stop?.should == false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
it "describes a sleeping strand" do
|
25
|
+
StrandSpecs.status_of_sleeping_strand.stop?.should == true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "describes a blocked strand" do
|
29
|
+
StrandSpecs.status_of_blocked_strand.stop?.should == true
|
30
|
+
end
|
31
|
+
|
32
|
+
it "describes a completed strand" do
|
33
|
+
StrandSpecs.status_of_completed_strand.stop?.should == true
|
34
|
+
end
|
35
|
+
|
36
|
+
it "describes a killed strand" do
|
37
|
+
StrandSpecs.status_of_killed_strand.stop?.should == true
|
38
|
+
end
|
39
|
+
|
40
|
+
it "describes a strand with an uncaught exception" do
|
41
|
+
StrandSpecs.status_of_strand_with_uncaught_exception.stop?.should == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "describes a dying running strand" do
|
45
|
+
StrandSpecs.status_of_dying_running_strand.stop?.should == false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "describes a dying sleeping strand" do
|
49
|
+
StrandSpecs.status_of_dying_sleeping_strand.stop?.should == true
|
50
|
+
end
|
51
|
+
|
52
|
+
quarantine! do
|
53
|
+
it "reports aborting on a killed strand" do
|
54
|
+
StrandSpecs.status_of_aborting_strand.stop?.should == false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|