agent 0.1.0 → 0.9.0
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 +2 -0
- data/.rspec +0 -0
- data/Gemfile +0 -1
- data/Gemfile.lock +12 -12
- data/README.md +51 -36
- data/Rakefile +5 -0
- data/agent.gemspec +1 -0
- data/autotest/discover.rb +9 -1
- data/benchmark/multi_ruby_bench.sh +87 -0
- data/benchmark/sieve.rb +238 -0
- data/examples/agent-workers.rb +51 -0
- data/examples/producer-consumer.rb +15 -0
- data/lib/agent.rb +1 -15
- data/lib/agent/all.rb +22 -0
- data/lib/agent/blocking_once.rb +26 -0
- data/lib/agent/channel.rb +88 -41
- data/lib/agent/error.rb +15 -0
- data/lib/agent/errors.rb +14 -0
- data/lib/agent/go.rb +9 -0
- data/lib/agent/kernel/channel.rb +7 -0
- data/lib/agent/kernel/go.rb +7 -0
- data/lib/agent/kernel/select.rb +7 -0
- data/lib/agent/notifier.rb +34 -0
- data/lib/agent/once.rb +32 -0
- data/lib/agent/pop.rb +70 -0
- data/lib/agent/push.rb +70 -0
- data/lib/agent/queue.rb +133 -0
- data/lib/agent/queue/buffered.rb +68 -0
- data/lib/agent/queue/unbuffered.rb +88 -0
- data/lib/agent/queues.rb +50 -0
- data/lib/agent/selector.rb +119 -0
- data/lib/agent/uuid.rb +36 -0
- data/lib/agent/version.rb +1 -1
- data/lib/agent/wait_group.rb +43 -0
- data/spec/blocking_once_spec.rb +122 -0
- data/spec/channel_spec.rb +153 -82
- data/spec/error_spec.rb +15 -0
- data/spec/examples/channel_of_channels_spec.rb +17 -14
- data/spec/examples/producer_consumer_spec.rb +26 -16
- data/spec/examples/sieve_spec.rb +43 -37
- data/spec/go_spec.rb +17 -0
- data/spec/notifier_spec.rb +42 -0
- data/spec/once_spec.rb +91 -0
- data/spec/pop_spec.rb +97 -0
- data/spec/push_spec.rb +95 -0
- data/spec/queue_spec.rb +335 -37
- data/spec/queues_spec.rb +28 -0
- data/spec/selector_spec.rb +398 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/uuid_spec.rb +13 -0
- data/spec/wait_group_spec.rb +45 -0
- metadata +94 -54
- data/lib/agent/transport/queue.rb +0 -82
- data/spec/helper.rb +0 -5
data/spec/error_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Agent::Error do
|
4
|
+
before do
|
5
|
+
@error = Agent::Error.new("msg")
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should create an error" do
|
9
|
+
@error.to_s.should == "msg"
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should match the error's message" do
|
13
|
+
@error.should be_message("msg")
|
14
|
+
end
|
15
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require "
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe "Channel of Channels" do
|
4
4
|
|
@@ -7,24 +7,26 @@ describe "Channel of Channels" do
|
|
7
7
|
it "should be able to pass channels as first class citizens" do
|
8
8
|
server = Proc.new do |reqs|
|
9
9
|
2.times do |n|
|
10
|
-
res = Request.new(n,
|
10
|
+
res = Request.new(n, channel!(Integer))
|
11
11
|
|
12
12
|
reqs << res
|
13
|
-
res.resultChan.receive.should == n+1
|
13
|
+
res.resultChan.receive[0].should == n+1
|
14
|
+
res.resultChan.close
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
17
18
|
worker = Proc.new do |reqs|
|
18
19
|
loop do
|
19
|
-
req = reqs.receive
|
20
|
+
req, ok = reqs.receive
|
21
|
+
break unless ok
|
20
22
|
req.resultChan << req.args+1
|
21
23
|
end
|
22
24
|
end
|
23
25
|
|
24
|
-
clientRequests =
|
26
|
+
clientRequests = channel!(Request)
|
25
27
|
|
26
|
-
s = go(clientRequests, &server)
|
27
|
-
c = go(clientRequests, &worker)
|
28
|
+
s = go!(clientRequests, &server)
|
29
|
+
c = go!(clientRequests, &worker)
|
28
30
|
|
29
31
|
s.join
|
30
32
|
clientRequests.close
|
@@ -33,24 +35,25 @@ describe "Channel of Channels" do
|
|
33
35
|
it "should work with multiple workers" do
|
34
36
|
worker = Proc.new do |reqs|
|
35
37
|
loop do
|
36
|
-
req = reqs.receive
|
38
|
+
req, _ = reqs.receive
|
37
39
|
req.resultChan << req.args+1
|
38
40
|
end
|
39
41
|
end
|
40
42
|
|
41
|
-
clientRequests =
|
43
|
+
clientRequests = channel!(Request)
|
42
44
|
|
43
45
|
# start multiple workers
|
44
|
-
go(clientRequests, &worker)
|
45
|
-
go(clientRequests, &worker)
|
46
|
+
go!(clientRequests, &worker)
|
47
|
+
go!(clientRequests, &worker)
|
46
48
|
|
47
49
|
# start server
|
48
|
-
s = go clientRequests do |reqs|
|
50
|
+
s = go! clientRequests do |reqs|
|
49
51
|
2.times do |n|
|
50
|
-
res = Request.new(n,
|
52
|
+
res = Request.new(n, channel!(Integer))
|
51
53
|
|
52
54
|
reqs << res
|
53
|
-
res.resultChan.receive.should == n+1
|
55
|
+
res.resultChan.receive[0].should == n+1
|
56
|
+
res.resultChan.close
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
require "
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe "Producer-Consumer" do
|
4
4
|
it "should synchronize by communication" do
|
@@ -32,31 +32,42 @@ describe "Producer-Consumer" do
|
|
32
32
|
# }
|
33
33
|
|
34
34
|
producer = Proc.new do |c, n, s|
|
35
|
+
# print "producer: starting\n"
|
36
|
+
|
35
37
|
n.times do |i|
|
38
|
+
# print "producer: #{i+1} of #{n}\n"
|
36
39
|
c << i
|
37
|
-
#
|
40
|
+
# print "producer sent: #{i}\n"
|
38
41
|
end
|
39
42
|
|
43
|
+
# print "producer: finished\n"
|
44
|
+
|
40
45
|
s << "producer finished"
|
41
46
|
end
|
42
47
|
|
43
48
|
consumer = Proc.new do |c, n, s|
|
49
|
+
# print "consumer: starting\n"
|
44
50
|
n.times do |i|
|
51
|
+
# print "consumer: #{i+1} of #{n}\n"
|
45
52
|
msg = c.receive
|
46
|
-
#
|
53
|
+
# print "consumer got: #{msg}\n"
|
47
54
|
end
|
48
55
|
|
56
|
+
# print "consumer: finished\n"
|
57
|
+
|
49
58
|
s << "consumer finished"
|
50
59
|
end
|
51
60
|
|
52
|
-
c =
|
53
|
-
s =
|
61
|
+
c = channel!(Integer)
|
62
|
+
s = channel!(String)
|
54
63
|
|
55
|
-
go(c, 3, s, &producer)
|
56
|
-
|
64
|
+
go!(c, 3, s, &producer)
|
65
|
+
sleep 0.1
|
66
|
+
go!(c, 3, s, &consumer)
|
57
67
|
|
58
|
-
s.pop.
|
59
|
-
|
68
|
+
messages = [s.pop[0], s.pop[0]]
|
69
|
+
messages.should include("producer finished")
|
70
|
+
messages.should include("consumer finished")
|
60
71
|
|
61
72
|
c.close
|
62
73
|
s.close
|
@@ -69,15 +80,14 @@ describe "Producer-Consumer" do
|
|
69
80
|
end
|
70
81
|
|
71
82
|
Generator = Struct.new(:name, :pipe)
|
72
|
-
c =
|
83
|
+
c = channel!(Integer)
|
73
84
|
g = Generator.new(:incr, c)
|
74
85
|
|
75
|
-
go(g, &producer)
|
86
|
+
go!(g, &producer)
|
76
87
|
|
77
|
-
c.receive.should == 1
|
78
|
-
c.receive.should == 2
|
79
|
-
|
80
|
-
c.
|
81
|
-
c.receive.should == 3
|
88
|
+
c.receive[0].should == 1
|
89
|
+
c.receive[0].should == 2
|
90
|
+
c.receive[0].should == 3
|
91
|
+
c.close
|
82
92
|
end
|
83
93
|
end
|
data/spec/examples/sieve_spec.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require "
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe "sieve of Eratosthenes" do
|
4
4
|
|
@@ -7,24 +7,22 @@ describe "sieve of Eratosthenes" do
|
|
7
7
|
it "should work using Channel primitives" do
|
8
8
|
|
9
9
|
# send the sequence 2,3,4, ... to returned channel
|
10
|
-
def generate
|
11
|
-
ch =
|
12
|
-
|
13
|
-
go
|
14
|
-
i = 1
|
15
|
-
loop { ch << i+= 1 }
|
16
|
-
end
|
10
|
+
def generate(channels)
|
11
|
+
ch = channel!(Integer)
|
12
|
+
channels << ch
|
13
|
+
go!{ i = 1; loop { ch << i+= 1} }
|
17
14
|
|
18
15
|
return ch
|
19
16
|
end
|
20
17
|
|
21
18
|
# filter out input values divisible by *prime*, send rest to returned channel
|
22
|
-
def filter(in_channel, prime)
|
23
|
-
out =
|
19
|
+
def filter(in_channel, prime, channels)
|
20
|
+
out = channel!(Integer)
|
21
|
+
channels << out
|
24
22
|
|
25
|
-
go do
|
23
|
+
go! do
|
26
24
|
loop do
|
27
|
-
i = in_channel.receive
|
25
|
+
i, _ = in_channel.receive
|
28
26
|
out << i if (i % prime) != 0
|
29
27
|
end
|
30
28
|
end
|
@@ -32,15 +30,16 @@ describe "sieve of Eratosthenes" do
|
|
32
30
|
return out
|
33
31
|
end
|
34
32
|
|
35
|
-
def sieve
|
36
|
-
out =
|
33
|
+
def sieve(channels)
|
34
|
+
out = channel!(Integer)
|
35
|
+
channels << out
|
37
36
|
|
38
|
-
go do
|
39
|
-
ch = generate
|
37
|
+
go! do
|
38
|
+
ch = generate(channels)
|
40
39
|
loop do
|
41
|
-
prime = ch.receive
|
40
|
+
prime, _ = ch.receive
|
42
41
|
out << prime
|
43
|
-
ch = filter(ch, prime)
|
42
|
+
ch = filter(ch, prime, channels)
|
44
43
|
end
|
45
44
|
end
|
46
45
|
|
@@ -50,16 +49,17 @@ describe "sieve of Eratosthenes" do
|
|
50
49
|
# run the sieve
|
51
50
|
n = 20
|
52
51
|
nth = false
|
52
|
+
channels = []
|
53
53
|
|
54
|
-
primes = sieve
|
54
|
+
primes = sieve(channels)
|
55
55
|
result = []
|
56
56
|
|
57
57
|
if nth
|
58
58
|
n.times { primes.receive }
|
59
|
-
puts primes.receive
|
59
|
+
puts primes.receive[0]
|
60
60
|
else
|
61
61
|
loop do
|
62
|
-
p = primes.receive
|
62
|
+
p, _ = primes.receive
|
63
63
|
|
64
64
|
if p <= n
|
65
65
|
result << p
|
@@ -70,15 +70,17 @@ describe "sieve of Eratosthenes" do
|
|
70
70
|
end
|
71
71
|
|
72
72
|
result.should == [2,3,5,7,11,13,17,19]
|
73
|
+
channels.each(&:close)
|
73
74
|
end
|
74
75
|
|
75
76
|
it "should work with Ruby blocks" do
|
76
77
|
|
77
78
|
# send the sequence 2,3,4, ... to returned channel
|
78
|
-
generate = Proc.new do
|
79
|
-
ch =
|
79
|
+
generate = Proc.new do |channels|
|
80
|
+
ch = channel!(Integer)
|
81
|
+
channels << ch
|
80
82
|
|
81
|
-
go do
|
83
|
+
go! do
|
82
84
|
i = 1
|
83
85
|
loop { ch << i+= 1 }
|
84
86
|
end
|
@@ -87,12 +89,13 @@ describe "sieve of Eratosthenes" do
|
|
87
89
|
end
|
88
90
|
|
89
91
|
# filter out input values divisible by *prime*, send rest to returned channel
|
90
|
-
filtr = Proc.new do |in_channel, prime|
|
91
|
-
out =
|
92
|
+
filtr = Proc.new do |in_channel, prime, channels|
|
93
|
+
out = channel!(Integer)
|
94
|
+
channels << out
|
92
95
|
|
93
|
-
go do
|
96
|
+
go! do
|
94
97
|
loop do
|
95
|
-
i = in_channel.receive
|
98
|
+
i, _ = in_channel.receive
|
96
99
|
out << i if (i % prime) != 0
|
97
100
|
end
|
98
101
|
end
|
@@ -100,16 +103,17 @@ describe "sieve of Eratosthenes" do
|
|
100
103
|
out
|
101
104
|
end
|
102
105
|
|
103
|
-
sieve =
|
104
|
-
out =
|
106
|
+
sieve = Proc.new do |channels|
|
107
|
+
out = channel!(Integer)
|
108
|
+
channels << out
|
105
109
|
|
106
|
-
go do
|
107
|
-
ch = generate.call
|
110
|
+
go! do
|
111
|
+
ch = generate.call(channels)
|
108
112
|
|
109
113
|
loop do
|
110
|
-
prime = ch.receive
|
114
|
+
prime, _ = ch.receive
|
111
115
|
out << prime
|
112
|
-
ch = filtr.call(ch, prime)
|
116
|
+
ch = filtr.call(ch, prime, channels)
|
113
117
|
end
|
114
118
|
end
|
115
119
|
|
@@ -119,16 +123,17 @@ describe "sieve of Eratosthenes" do
|
|
119
123
|
# run the sieve
|
120
124
|
n = 20
|
121
125
|
nth = false
|
126
|
+
channels = []
|
122
127
|
|
123
|
-
primes = sieve.call
|
128
|
+
primes = sieve.call(channels)
|
124
129
|
result = []
|
125
130
|
|
126
131
|
if nth
|
127
132
|
n.times { primes.receive }
|
128
|
-
puts primes.receive
|
133
|
+
puts primes.receive[0]
|
129
134
|
else
|
130
135
|
loop do
|
131
|
-
p = primes.receive
|
136
|
+
p, _ = primes.receive
|
132
137
|
|
133
138
|
if p <= n
|
134
139
|
result << p
|
@@ -139,5 +144,6 @@ describe "sieve of Eratosthenes" do
|
|
139
144
|
end
|
140
145
|
|
141
146
|
result.should == [2,3,5,7,11,13,17,19]
|
147
|
+
channels.each(&:close)
|
142
148
|
end
|
143
149
|
end
|
data/spec/go_spec.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Agent.go!" do
|
4
|
+
it "should launch a 'goroutine' that is actually a thread" do
|
5
|
+
Agent.go!{}.should be_a(Thread)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should pass into the thread any arguments passed to it" do
|
9
|
+
b = nil
|
10
|
+
Agent.go!(1){|a| b = a }.join
|
11
|
+
b.should == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should raise an error if no block is passed" do
|
15
|
+
lambda{ Agent.go! }.should raise_error(Agent::Errors::BlockMissing)
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Agent::Notifier do
|
4
|
+
before do
|
5
|
+
@notifier = Agent::Notifier.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should notify using a payload" do
|
9
|
+
@notifier.notify(1)
|
10
|
+
@notifier.payload.should == 1
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should acknowledge notification" do
|
14
|
+
@notifier.should_not be_notified
|
15
|
+
@notifier.notify(1)
|
16
|
+
@notifier.should be_notified
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should only notify once" do
|
20
|
+
@notifier.notify(1)
|
21
|
+
@notifier.notify(2)
|
22
|
+
@notifier.payload.should == 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return nil when notified for the first time" do
|
26
|
+
@notifier.notify(1).should be_nil
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return an error when notified more than once" do
|
30
|
+
@notifier.notify(1)
|
31
|
+
@notifier.notify(2).should be_message("already notified")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should allow waiting on a notification and should signal when it is notified" do
|
35
|
+
ack = channel!(Integer)
|
36
|
+
go!{ @notifier.wait; ack.send(@notifier.payload) }
|
37
|
+
sleep 0.1 # make sure the notifier in the goroutine is waiting
|
38
|
+
@notifier.notify(1)
|
39
|
+
payload, _ = ack.receive
|
40
|
+
payload.should == 1
|
41
|
+
end
|
42
|
+
end
|
data/spec/once_spec.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Agent::Once do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@once = Agent::Once.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should execute the block passed to it" do
|
10
|
+
r = []
|
11
|
+
|
12
|
+
@once.perform do
|
13
|
+
r << 1
|
14
|
+
end
|
15
|
+
|
16
|
+
r.size.should == 1
|
17
|
+
r.first.should == 1
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should only execute the first block passed to it" do
|
21
|
+
r = []
|
22
|
+
|
23
|
+
@once.perform do
|
24
|
+
r << 1
|
25
|
+
end
|
26
|
+
|
27
|
+
@once.perform do
|
28
|
+
r << 2
|
29
|
+
end
|
30
|
+
|
31
|
+
r.size.should == 1
|
32
|
+
r.first.should == 1
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return the value returned from the block" do
|
36
|
+
value, error = @once.perform do
|
37
|
+
1
|
38
|
+
end
|
39
|
+
|
40
|
+
value.should == 1
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should return nil for value and an error if it has already been used" do
|
44
|
+
value, error = @once.perform{ 1 }
|
45
|
+
value.should == 1
|
46
|
+
error.should be_nil
|
47
|
+
|
48
|
+
value, error = @once.perform{ 2 }
|
49
|
+
value.should be_nil
|
50
|
+
error.should_not be_nil
|
51
|
+
error.should be_message("already performed")
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should have minimal contention between threads when they contend for position" do
|
55
|
+
r, s = [], Time.now.to_f
|
56
|
+
|
57
|
+
# Using condition variables to maximize potential contention
|
58
|
+
mutex = Mutex.new
|
59
|
+
condition = ConditionVariable.new
|
60
|
+
|
61
|
+
waiting_channel = channel!(TrueClass, 2)
|
62
|
+
finished_channel = channel!(TrueClass, 2)
|
63
|
+
|
64
|
+
go! do
|
65
|
+
mutex.synchronize{ waiting_channel.send(true); condition.wait(mutex) }
|
66
|
+
@once.perform{ sleep 0.1; r << 1 }
|
67
|
+
finished_channel.send(true)
|
68
|
+
end
|
69
|
+
|
70
|
+
go! do
|
71
|
+
mutex.synchronize{ waiting_channel.send(true); condition.wait(mutex) }
|
72
|
+
@once.perform{ sleep 0.1; r << 1 }
|
73
|
+
finished_channel.send(true)
|
74
|
+
end
|
75
|
+
|
76
|
+
# wait for both the goroutines to be waiting
|
77
|
+
2.times{ waiting_channel.receive }
|
78
|
+
|
79
|
+
mutex.synchronize{ condition.broadcast }
|
80
|
+
|
81
|
+
# wait for the finished channel to be completed
|
82
|
+
2.times{ finished_channel.receive }
|
83
|
+
|
84
|
+
r.size.should == 1
|
85
|
+
# Onlt the first sleep should be performed, so things should quickly
|
86
|
+
(Time.now.to_f - s).should be_within(0.05).of(0.15)
|
87
|
+
|
88
|
+
waiting_channel.close
|
89
|
+
finished_channel.close
|
90
|
+
end
|
91
|
+
end
|