agent 0.1.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- 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/lib/agent/uuid.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
# Borrowed from Celluloid. Using thread locals instead of extending the base
|
4
|
+
# Thread class, though.
|
5
|
+
module Agent
|
6
|
+
module UUID
|
7
|
+
values = SecureRandom.hex(9).match(/(.{8})(.{4})(.{3})(.{3})/)
|
8
|
+
PREFIX = "#{values[1]}_#{values[2]}_4#{values[3]}_8#{values[4]}".freeze
|
9
|
+
BLOCK_SIZE = 0x10000
|
10
|
+
|
11
|
+
@counter = 0
|
12
|
+
@counter_mutex = Mutex.new
|
13
|
+
|
14
|
+
def self.generate
|
15
|
+
thread = Thread.current
|
16
|
+
|
17
|
+
unless thread[:__agent_uuid_limit__]
|
18
|
+
@counter_mutex.synchronize do
|
19
|
+
block_base = @counter
|
20
|
+
@counter += BLOCK_SIZE
|
21
|
+
thread[:__agent_uuid_counter__] = block_base
|
22
|
+
thread[:__agent_uuid_limit__] = @counter - 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
counter = thread[:__agent_uuid_counter__]
|
27
|
+
if thread[:__agent_uuid_counter__] >= thread[:__agent_uuid_limit__]
|
28
|
+
thread[:__agent_uuid_counter__] = thread[:__agent_uuid_limit__] = nil
|
29
|
+
else
|
30
|
+
thread[:__agent_uuid_counter__] += 1
|
31
|
+
end
|
32
|
+
|
33
|
+
"#{PREFIX}_#{sprintf("%012x", counter)}".freeze
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/agent/version.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "agent/errors"
|
2
|
+
|
3
|
+
module Agent
|
4
|
+
class WaitGroup
|
5
|
+
attr_reader :count
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@count = 0
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@cvar = ConditionVariable.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def wait
|
14
|
+
@mutex.synchronize do
|
15
|
+
while @count > 0
|
16
|
+
@cvar.wait(@mutex)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(delta)
|
22
|
+
@mutex.synchronize do
|
23
|
+
modify_count(delta)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def done
|
28
|
+
@mutex.synchronize do
|
29
|
+
modify_count(-1)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
# Expects to be called inside of the mutex
|
36
|
+
def modify_count(delta)
|
37
|
+
@count += delta
|
38
|
+
raise Errors::NegativeWaitGroupCount if @count < 0
|
39
|
+
@cvar.signal if @count == 0
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Agent::BlockingOnce do
|
4
|
+
|
5
|
+
before do
|
6
|
+
@blocking_once = Agent::BlockingOnce.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should execute the block passed to it" do
|
10
|
+
r = []
|
11
|
+
|
12
|
+
@blocking_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
|
+
@blocking_once.perform do
|
24
|
+
r << 1
|
25
|
+
end
|
26
|
+
|
27
|
+
@blocking_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 = @blocking_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 = @blocking_once.perform{ 1 }
|
45
|
+
value.should == 1
|
46
|
+
error.should be_nil
|
47
|
+
|
48
|
+
value, error = @blocking_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 roll back and allow the block to be executed again" do
|
55
|
+
s = Time.now.to_f
|
56
|
+
|
57
|
+
finished_channel = channel!(TrueClass, 2)
|
58
|
+
|
59
|
+
go! do
|
60
|
+
@blocking_once.perform do
|
61
|
+
sleep 0.1
|
62
|
+
finished_channel.send(true)
|
63
|
+
raise Agent::Errors::Rollback
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
sleep 0.1 # make sure the first @blocking_once calls #perform
|
68
|
+
|
69
|
+
go! do
|
70
|
+
@blocking_once.perform do
|
71
|
+
sleep 0.1
|
72
|
+
finished_channel.send(true)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
2.times { finished_channel.receive }
|
77
|
+
|
78
|
+
finished_channel.close
|
79
|
+
|
80
|
+
# Three sleeps at 0.1 == 0.3, so if it's less than 0.3...
|
81
|
+
(Time.now.to_f - s).should < 0.3
|
82
|
+
end
|
83
|
+
|
84
|
+
it "should have minimal contention between threads when they contend for position" do
|
85
|
+
r, s = [], Time.now.to_f
|
86
|
+
|
87
|
+
# Using condition variables to maximize potential contention
|
88
|
+
mutex = Mutex.new
|
89
|
+
condition = ConditionVariable.new
|
90
|
+
|
91
|
+
waiting_channel = channel!(TrueClass, 2)
|
92
|
+
finished_channel = channel!(TrueClass, 2)
|
93
|
+
|
94
|
+
go! do
|
95
|
+
mutex.synchronize{ waiting_channel.send(true); condition.wait(mutex) }
|
96
|
+
@blocking_once.perform{ sleep 0.1; r << 1 }
|
97
|
+
finished_channel.send(true)
|
98
|
+
end
|
99
|
+
|
100
|
+
go! do
|
101
|
+
mutex.synchronize{ waiting_channel.send(true); condition.wait(mutex) }
|
102
|
+
@blocking_once.perform{ sleep 0.1; r << 1 }
|
103
|
+
finished_channel.send(true)
|
104
|
+
end
|
105
|
+
|
106
|
+
# wait for both the goroutines to be waiting
|
107
|
+
2.times{ waiting_channel.receive }
|
108
|
+
|
109
|
+
mutex.synchronize{ condition.broadcast }
|
110
|
+
|
111
|
+
# wait for the finished channel to be completed
|
112
|
+
2.times{ finished_channel.receive }
|
113
|
+
|
114
|
+
r.size.should == 1
|
115
|
+
# Onlt the first sleep should be performed, so things should quickly
|
116
|
+
(Time.now.to_f - s).should be_within(0.05).of(0.15)
|
117
|
+
|
118
|
+
waiting_channel.close
|
119
|
+
finished_channel.close
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/spec/channel_spec.rb
CHANGED
@@ -1,38 +1,17 @@
|
|
1
|
-
require "
|
1
|
+
require "spec_helper"
|
2
2
|
|
3
3
|
describe Agent::Channel do
|
4
|
-
# http://golang.org/doc/go_spec.html#Channel_types
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
lambda { Channel.new(:type => String) }.should raise_error(Channel::NoName)
|
11
|
-
c.name.should == "spec"
|
12
|
-
end
|
13
|
-
|
14
|
-
it "should respond to close" do
|
15
|
-
lambda { c.close }.should_not raise_error
|
16
|
-
c.closed?.should be_true
|
17
|
-
end
|
18
|
-
|
19
|
-
it "should respond to closed?" do
|
20
|
-
c.closed?.should be_false
|
21
|
-
c.close
|
22
|
-
c.closed?.should be_true
|
23
|
-
end
|
24
|
-
|
25
|
-
context "deadlock" do
|
26
|
-
it "should deadlock on single thread" do
|
27
|
-
c = Channel.new(:name => "deadlock", :type => String)
|
28
|
-
lambda { c.receive }.should raise_error
|
5
|
+
context "naming" do
|
6
|
+
it "should not be required" do
|
7
|
+
c = nil
|
8
|
+
lambda { c = channel!(String) }.should_not raise_error
|
29
9
|
c.close
|
30
10
|
end
|
31
11
|
|
32
|
-
it "
|
33
|
-
c =
|
34
|
-
|
35
|
-
lambda { c.receive }.should_not raise_error
|
12
|
+
it "be able to be set" do
|
13
|
+
c = channel!(String, :name => "gibberish")
|
14
|
+
c.name.should == "gibberish"
|
36
15
|
c.close
|
37
16
|
end
|
38
17
|
end
|
@@ -43,102 +22,194 @@ describe Agent::Channel do
|
|
43
22
|
# type. The value of an uninitialized channel is nil.
|
44
23
|
|
45
24
|
it "should support send only" do
|
46
|
-
c =
|
25
|
+
c = channel!(String, 3, :direction => :send)
|
47
26
|
|
48
27
|
lambda { c << "hello" }.should_not raise_error
|
49
28
|
lambda { c.push "hello" }.should_not raise_error
|
50
29
|
lambda { c.send "hello" }.should_not raise_error
|
51
30
|
|
52
|
-
|
53
|
-
|
31
|
+
c.direction.should == :send
|
32
|
+
|
33
|
+
lambda { c.pop }.should raise_error Agent::Errors::InvalidDirection
|
34
|
+
lambda { c.receive }.should raise_error Agent::Errors::InvalidDirection
|
54
35
|
|
55
36
|
c.close
|
56
37
|
end
|
57
38
|
|
58
39
|
it "should support receive only" do
|
59
|
-
c =
|
40
|
+
c = channel!(String, :direction => :receive)
|
41
|
+
|
42
|
+
lambda { c << "hello" }.should raise_error Agent::Errors::InvalidDirection
|
43
|
+
lambda { c.push "hello" }.should raise_error Agent::Errors::InvalidDirection
|
44
|
+
lambda { c.send "hello" }.should raise_error Agent::Errors::InvalidDirection
|
60
45
|
|
61
|
-
|
62
|
-
lambda { c.push "hello" }.should raise_error Channel::InvalidDirection
|
63
|
-
lambda { c.send "hello" }.should raise_error Channel::InvalidDirection
|
46
|
+
c.direction.should == :receive
|
64
47
|
|
65
48
|
# timeout blocking receive calls
|
66
|
-
|
67
|
-
|
49
|
+
timed_out = false
|
50
|
+
select! do |s|
|
51
|
+
s.case(c, :receive){}
|
52
|
+
s.timeout(0.1){ timed_out = true }
|
53
|
+
end
|
54
|
+
timed_out.should == true
|
55
|
+
c.close
|
68
56
|
end
|
69
57
|
|
70
58
|
it "should default to bi-directional communication" do
|
59
|
+
c = channel!(String, 1)
|
71
60
|
lambda { c.send "hello" }.should_not raise_error
|
72
61
|
lambda { c.receive }.should_not raise_error
|
62
|
+
|
63
|
+
c.direction.should == :bidirectional
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should be able to be dup'd as a uni-directional channel" do
|
67
|
+
c = channel!(String, 1)
|
68
|
+
|
69
|
+
send_only = c.as_send_only
|
70
|
+
send_only.direction.should == :send
|
71
|
+
|
72
|
+
receive_only = c.as_receive_only
|
73
|
+
receive_only.direction.should == :receive
|
74
|
+
|
75
|
+
send_only.send("nifty")
|
76
|
+
receive_only.receive[0].should == "nifty"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "closing" do
|
81
|
+
before do
|
82
|
+
@c = channel!(String)
|
83
|
+
end
|
84
|
+
|
85
|
+
it "not raise an error the first time it is called" do
|
86
|
+
lambda { @c.close }.should_not raise_error
|
87
|
+
@c.closed?.should be_true
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should raise an error the second time it is called" do
|
91
|
+
@c.close
|
92
|
+
lambda { @c.close }.should raise_error(Agent::Errors::ChannelClosed)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should respond to closed?" do
|
96
|
+
@c.closed?.should be_false
|
97
|
+
@c.close
|
98
|
+
@c.closed?.should be_true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should return that a receive was a failure when a channel is closed while being read from" do
|
102
|
+
go!{ sleep 0.01; @c.close }
|
103
|
+
_, ok = @c.receive
|
104
|
+
ok.should be_false
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should raise an error when sending to a channel that has already been closed" do
|
108
|
+
@c.close
|
109
|
+
lambda { @c.send("a") }.should raise_error(Agent::Errors::ChannelClosed)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should raise an error when receiving from a channel that has already been closed" do
|
113
|
+
@c.close
|
114
|
+
lambda { @c.receive }.should raise_error(Agent::Errors::ChannelClosed)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "deadlock" do
|
119
|
+
before do
|
120
|
+
@c = channel!(String)
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should deadlock on single thread", :vm => :ruby do
|
124
|
+
lambda { @c.receive }.should raise_error
|
125
|
+
end
|
126
|
+
|
127
|
+
it "should not deadlock with multiple threads" do
|
128
|
+
go!{ sleep(0.1); @c.push "hi" }
|
129
|
+
lambda { @c.receive }.should_not raise_error
|
73
130
|
end
|
74
131
|
end
|
75
132
|
|
76
133
|
context "typed" do
|
77
134
|
it "should create a typed channel" do
|
78
|
-
lambda {
|
79
|
-
|
135
|
+
lambda { channel! }.should raise_error Agent::Errors::Untyped
|
136
|
+
c = nil
|
137
|
+
lambda { c = channel!(Integer) }.should_not raise_error
|
138
|
+
c.close
|
80
139
|
end
|
81
140
|
|
82
141
|
it "should reject messages of invalid type" do
|
83
|
-
|
142
|
+
c = channel!(String)
|
143
|
+
go!{ c.receive }
|
144
|
+
lambda { c.send 1 }.should raise_error(Agent::Errors::InvalidType)
|
84
145
|
lambda { c.send "hello" }.should_not raise_error
|
85
|
-
c.
|
146
|
+
c.close
|
86
147
|
end
|
87
148
|
end
|
88
149
|
|
89
|
-
context "
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
context "channels of channels" do
|
95
|
-
# One of the most important properties of Go is that a channel is a first-class
|
96
|
-
# value that can be allocated and passed around like any other. A common use of
|
97
|
-
# this property is to implement safe, parallel demultiplexing.
|
98
|
-
# - http://golang.org/doc/effective_go.html#chan_of_chan
|
150
|
+
context "buffering" do
|
151
|
+
# The capacity, in number of elements, sets the size of the buffer in the channel.
|
152
|
+
# If the capacity is greater than zero, the channel is buffered: provided the
|
153
|
+
# buffer is not full, sends can succeed without blocking. If the capacity is zero
|
154
|
+
# or absent, the communication succeeds only when both a sender and receiver are ready.
|
99
155
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
end
|
156
|
+
it "should default to unbuffered" do
|
157
|
+
n = Time.now
|
158
|
+
c = channel!(String)
|
104
159
|
|
105
|
-
|
106
|
-
|
160
|
+
go!{ sleep(0.15); c.send("hello") }
|
161
|
+
c.receive[0].should == "hello"
|
107
162
|
|
108
|
-
|
109
|
-
cm.receive.should == "hello"
|
110
|
-
end
|
163
|
+
(Time.now - n).should be_within(0.05).of(0.15)
|
111
164
|
end
|
112
165
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
# buffer is not full, sends can succeed without blocking. If the capacity is zero
|
117
|
-
# or absent, the communication succeeds only when both a sender and receiver are ready.
|
166
|
+
it "should support buffered" do
|
167
|
+
c = channel!(String, 2)
|
168
|
+
r = []
|
118
169
|
|
119
|
-
|
120
|
-
|
170
|
+
c.send "hello 1"
|
171
|
+
c.send "hello 2"
|
121
172
|
|
122
|
-
|
123
|
-
c
|
124
|
-
|
173
|
+
select! do |s|
|
174
|
+
s.case(c, :send, "hello 3")
|
175
|
+
s.timeout(0.1)
|
176
|
+
end
|
125
177
|
|
126
|
-
|
178
|
+
c.receive[0].should == "hello 1"
|
179
|
+
c.receive[0].should == "hello 2"
|
180
|
+
select! do |s|
|
181
|
+
s.case(c, :receive){|v| r.push(v) }
|
182
|
+
s.timeout(0.1)
|
127
183
|
end
|
128
184
|
|
129
|
-
|
130
|
-
|
185
|
+
c.close
|
186
|
+
end
|
187
|
+
end
|
131
188
|
|
132
|
-
|
133
|
-
|
134
|
-
|
189
|
+
context "channels of channels" do
|
190
|
+
before do
|
191
|
+
@c = channel!(String, 1)
|
192
|
+
end
|
193
|
+
|
194
|
+
after do
|
195
|
+
@c.close unless @c.closed?
|
196
|
+
end
|
135
197
|
|
136
|
-
|
137
|
-
|
138
|
-
|
198
|
+
# One of the most important properties of Go is that a channel is a first-class
|
199
|
+
# value that can be allocated and passed around like any other. A common use of
|
200
|
+
# this property is to implement safe, parallel demultiplexing.
|
201
|
+
# - http://golang.org/doc/effective_go.html#chan_of_chan
|
139
202
|
|
140
|
-
|
141
|
-
|
203
|
+
it "should be a first class, serializable value" do
|
204
|
+
lambda { Marshal.dump(@c) }.should_not raise_error
|
205
|
+
lambda { Marshal.load(Marshal.dump(@c)).is_a?(Agent::Channel) }.should_not raise_error
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should be able to pass as a value on a different channel" do
|
209
|
+
@c.send "hello"
|
210
|
+
|
211
|
+
cm = Marshal.load(Marshal.dump(@c))
|
212
|
+
cm.receive[0].should == "hello"
|
142
213
|
end
|
143
214
|
end
|
144
215
|
|