donkey 0.1.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/LICENSE +20 -0
- data/README.textile +162 -0
- data/Rakefile +57 -0
- data/VERSION.yml +4 -0
- data/lib/ass.rb +125 -0
- data/lib/ass/actor.rb +50 -0
- data/lib/ass/amqp.rb +19 -0
- data/lib/ass/callback_factory.rb +95 -0
- data/lib/ass/client.rb +19 -0
- data/lib/ass/peeper.rb +38 -0
- data/lib/ass/rpc.rb +180 -0
- data/lib/ass/serializers.rb +26 -0
- data/lib/ass/server.rb +181 -0
- data/lib/ass/topic.rb +90 -0
- data/spec/actor_spec.rb +83 -0
- data/spec/ass_spec.rb +425 -0
- data/spec/client_spec.rb +50 -0
- data/spec/rpc_spec.rb +73 -0
- data/test/ass_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +85 -0
data/spec/actor_spec.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "lib/ass"
|
3
|
+
require "spec"
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
require 'eventmachine'
|
7
|
+
EM.threadpool_size = 1
|
8
|
+
|
9
|
+
describe "Actor" do
|
10
|
+
before do
|
11
|
+
q = Queue.new
|
12
|
+
@server = nil
|
13
|
+
@thread = Thread.new {ASS.start {
|
14
|
+
q << :ready
|
15
|
+
}}
|
16
|
+
@thread.abort_on_exception = true
|
17
|
+
q.pop.should == :ready
|
18
|
+
end
|
19
|
+
|
20
|
+
after do
|
21
|
+
ASS.stop
|
22
|
+
@thread.join
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should respond by dispatch to methods" do
|
26
|
+
q = Queue.new
|
27
|
+
actor = ASS.actor("spec") {
|
28
|
+
define_method(:foo) do |i|
|
29
|
+
r = [:foo,i]
|
30
|
+
q << r
|
31
|
+
r
|
32
|
+
end
|
33
|
+
|
34
|
+
define_method(:bar) do |i|
|
35
|
+
r = [:bar,i]
|
36
|
+
q << r
|
37
|
+
r
|
38
|
+
end
|
39
|
+
}
|
40
|
+
#a.cast("spec",:foo,1)
|
41
|
+
actor.cast("spec",:foo,1)
|
42
|
+
q.pop.should == [:foo,1]
|
43
|
+
actor.cast("spec",:bar,2)
|
44
|
+
q.pop.should == [:bar,2]
|
45
|
+
|
46
|
+
actor.call("spec",:bar,3) # calling to self
|
47
|
+
q.pop.should == [:bar,3] # input
|
48
|
+
q.pop.should == [:bar,[:bar,3]] # output
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should handle error by calling on_error" do
|
52
|
+
q = Queue.new
|
53
|
+
actor = ASS.actor("spec") {
|
54
|
+
define_method(:on_error) do |e,data|
|
55
|
+
q << [e,data]
|
56
|
+
end
|
57
|
+
}
|
58
|
+
actor.cast("spec",:foo,1)
|
59
|
+
e, data = q.pop
|
60
|
+
e.should be_a(NoMethodError)
|
61
|
+
data.should == 1
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should be able to make service calls" do
|
65
|
+
q = Queue.new
|
66
|
+
a1 = ASS.actor("spec") {
|
67
|
+
define_method(:foo) do |i|
|
68
|
+
q << [:foo,i]
|
69
|
+
cast("spec2",:bar,i+1)
|
70
|
+
end
|
71
|
+
}
|
72
|
+
a2 = ASS.actor("spec2") do
|
73
|
+
define_method(:bar) do |i|
|
74
|
+
q << [:bar,i]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
a1.cast("spec",:foo,1)
|
78
|
+
q.pop.should == [:foo,1]
|
79
|
+
q.pop.should == [:bar,2]
|
80
|
+
end
|
81
|
+
|
82
|
+
|
83
|
+
end
|
data/spec/ass_spec.rb
ADDED
@@ -0,0 +1,425 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "lib/ass"
|
3
|
+
require "spec"
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
require 'eventmachine'
|
7
|
+
EM.threadpool_size = 1
|
8
|
+
|
9
|
+
require 'mocha'
|
10
|
+
Spec::Runner.configure do |config|
|
11
|
+
config.mock_with :mocha
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
describe "ASS" do
|
16
|
+
def server(*args,&block)
|
17
|
+
name = args.grep(String).first || "spec"
|
18
|
+
opts = args.grep(Hash).first || {}
|
19
|
+
ASS.server(name,opts) {
|
20
|
+
define_method(:on_call,&block)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(data,opts={},meta=nil)
|
25
|
+
ASS.call("spec",nil,data,{
|
26
|
+
:reply_to => "spec"
|
27
|
+
}.merge(opts),meta)
|
28
|
+
end
|
29
|
+
|
30
|
+
def cast(data,opts={},meta=nil)
|
31
|
+
ASS.cast("spec",nil,data,opts,meta)
|
32
|
+
end
|
33
|
+
|
34
|
+
def start_ass
|
35
|
+
q = Queue.new
|
36
|
+
@thread = Thread.new {ASS.start(:logging => (ENV["trace"]=="true")) {
|
37
|
+
q << :ready
|
38
|
+
}}
|
39
|
+
@thread.abort_on_exception = true
|
40
|
+
q.pop.should == :ready
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop_ass
|
44
|
+
ASS.stop
|
45
|
+
@thread.join
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should start and stop" do
|
49
|
+
30.times do
|
50
|
+
begin
|
51
|
+
q = Queue.new
|
52
|
+
s = nil
|
53
|
+
thread = Thread.new {
|
54
|
+
ASS.start {
|
55
|
+
s = server("spec") do |i|
|
56
|
+
q << :ok
|
57
|
+
end
|
58
|
+
q << :ready
|
59
|
+
}
|
60
|
+
q << :done
|
61
|
+
}
|
62
|
+
thread.abort_on_exception = true
|
63
|
+
q.pop.should == :ready
|
64
|
+
cast(0)
|
65
|
+
q.pop.should == :ok
|
66
|
+
ASS.stop
|
67
|
+
q.pop.should == :done
|
68
|
+
rescue => e
|
69
|
+
ASS.stop
|
70
|
+
raise e
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should stop ASS if server does not respond to on_error" do
|
76
|
+
begin
|
77
|
+
old_stderr = $stderr
|
78
|
+
error_output = StringIO.new
|
79
|
+
$stderr = error_output
|
80
|
+
q = Queue.new
|
81
|
+
s = nil
|
82
|
+
t = Thread.new {
|
83
|
+
ASS.start {
|
84
|
+
s = server("spec") do |i|
|
85
|
+
raise "ouch" if i == :die
|
86
|
+
q << :ok
|
87
|
+
i
|
88
|
+
end
|
89
|
+
q << :ready
|
90
|
+
}
|
91
|
+
q << :died
|
92
|
+
}
|
93
|
+
q.pop.should == :ready
|
94
|
+
cast(1)
|
95
|
+
q.pop.should == :ok
|
96
|
+
call(:die)
|
97
|
+
q.pop.should == :died
|
98
|
+
$stderr.string.should_not be_empty
|
99
|
+
EM.reactor_running?.should == false
|
100
|
+
rescue => e
|
101
|
+
ASS.stop
|
102
|
+
raise e
|
103
|
+
ensure
|
104
|
+
$stderr = old_stderr
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
it "should resend a message if server did not ack a message" do
|
110
|
+
begin
|
111
|
+
old_stderr = $stderr
|
112
|
+
error_output = StringIO.new
|
113
|
+
$stderr = error_output
|
114
|
+
q = Queue.new
|
115
|
+
s = nil
|
116
|
+
t1 = Thread.new {
|
117
|
+
ASS.start {
|
118
|
+
s = ASS.server("spec").react(:ack => true) do
|
119
|
+
define_method(:on_call) do |i|
|
120
|
+
raise "ouch" if i == :die
|
121
|
+
q << [header,i]
|
122
|
+
i
|
123
|
+
end
|
124
|
+
end
|
125
|
+
q << :ready
|
126
|
+
}
|
127
|
+
q << :died
|
128
|
+
}
|
129
|
+
q.pop.should == :ready
|
130
|
+
cast(1)
|
131
|
+
header, msg = q.pop
|
132
|
+
msg.should == 1
|
133
|
+
header.redelivered != true
|
134
|
+
cast(:die)
|
135
|
+
q.pop.should == :died
|
136
|
+
t1.join
|
137
|
+
$stderr.string.should_not be_empty
|
138
|
+
EM.reactor_running?.should == false
|
139
|
+
|
140
|
+
# restart server to get have the message resent
|
141
|
+
t2 = Thread.new {
|
142
|
+
ASS.start {
|
143
|
+
s = server { |msg|
|
144
|
+
q << [header,msg]
|
145
|
+
}
|
146
|
+
}
|
147
|
+
}
|
148
|
+
header, msg = q.pop
|
149
|
+
msg.should == :die
|
150
|
+
header.redelivered == true
|
151
|
+
ASS.stop
|
152
|
+
t2.join
|
153
|
+
ensure
|
154
|
+
$stderr = old_stderr
|
155
|
+
ASS.stop
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "serialization formats" do
|
160
|
+
def test_format(format)
|
161
|
+
q = Queue.new
|
162
|
+
@thread = Thread.new {ASS.start(:format => format
|
163
|
+
#:logging => true
|
164
|
+
) {
|
165
|
+
q << :ready
|
166
|
+
}}
|
167
|
+
@thread.abort_on_exception = true
|
168
|
+
q.pop.should == :ready
|
169
|
+
|
170
|
+
server { |*args|
|
171
|
+
# do nothing
|
172
|
+
q << :got_msg
|
173
|
+
}
|
174
|
+
cast(:whatever)
|
175
|
+
q.pop.should == :got_msg
|
176
|
+
|
177
|
+
ASS.stop
|
178
|
+
@thread.join
|
179
|
+
end
|
180
|
+
|
181
|
+
it "uses Marshal" do
|
182
|
+
ASS::Marshal.expects(:dump).returns("whatever")
|
183
|
+
ASS::Marshal.expects(:load).returns({})
|
184
|
+
test_format(ASS::Marshal)
|
185
|
+
end
|
186
|
+
|
187
|
+
it "uses JSON" do
|
188
|
+
ASS::JSON.expects(:dump).returns("whatever")
|
189
|
+
ASS::JSON.expects(:load).returns({})
|
190
|
+
test_format(ASS::JSON)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
describe "server" do
|
195
|
+
before do
|
196
|
+
q = Queue.new
|
197
|
+
@server = nil
|
198
|
+
@thread = Thread.new {ASS.start {
|
199
|
+
q << :ready
|
200
|
+
}}
|
201
|
+
@thread.abort_on_exception = true
|
202
|
+
q.pop.should == :ready
|
203
|
+
end
|
204
|
+
|
205
|
+
after do
|
206
|
+
ASS.stop
|
207
|
+
@thread.join
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should process message with on_call" do
|
211
|
+
input = Queue.new
|
212
|
+
output = Queue.new
|
213
|
+
s = server("spec") do |i|
|
214
|
+
input << i if i == 0
|
215
|
+
output << i if i == 1 # this is the response to self
|
216
|
+
i + 1
|
217
|
+
end
|
218
|
+
# note that when calling self we have the
|
219
|
+
# wierdness of the response sending back to
|
220
|
+
# self.
|
221
|
+
10.times { call(0) }
|
222
|
+
10.times.map { input.pop }.uniq.should == [0]
|
223
|
+
10.times.map { output.pop }.uniq.should == [1]
|
224
|
+
end
|
225
|
+
|
226
|
+
|
227
|
+
it "should handle error with on_error" do
|
228
|
+
errors = Queue.new
|
229
|
+
msgs = Queue.new
|
230
|
+
s = ASS.server("spec") {
|
231
|
+
def on_call(i)
|
232
|
+
raise "aieee"
|
233
|
+
end
|
234
|
+
|
235
|
+
define_method(:on_error) do |e,msg|
|
236
|
+
msgs << msg
|
237
|
+
errors << e
|
238
|
+
end
|
239
|
+
}
|
240
|
+
10.times { call(0) }
|
241
|
+
10.times {
|
242
|
+
msgs.pop.should == 0
|
243
|
+
#errors.pop.is_a?(Exception).should == true
|
244
|
+
errors.pop.should be_a(RuntimeError)
|
245
|
+
}
|
246
|
+
end
|
247
|
+
|
248
|
+
it "should have access to magic service methods" do
|
249
|
+
q = Queue.new
|
250
|
+
s = server { |i|
|
251
|
+
q << [header,method,data,meta]
|
252
|
+
}
|
253
|
+
cast(1,{},:meta)
|
254
|
+
header,method,data,meta = q.pop
|
255
|
+
header.should be_an(MQ::Header)
|
256
|
+
method.should be_nil
|
257
|
+
data.should == 1
|
258
|
+
meta.should == :meta
|
259
|
+
end
|
260
|
+
|
261
|
+
|
262
|
+
it "should use a new callback instance to process each request" do
|
263
|
+
q = Queue.new
|
264
|
+
s = server { |i|
|
265
|
+
q << self
|
266
|
+
i
|
267
|
+
}
|
268
|
+
2000.times {|i|
|
269
|
+
begin
|
270
|
+
cast(1)
|
271
|
+
rescue => e
|
272
|
+
p "broken at #{i}"
|
273
|
+
raise e
|
274
|
+
end
|
275
|
+
}
|
276
|
+
# we need to hold on to the saved pointers,
|
277
|
+
# otherwise objects would get garbage
|
278
|
+
# collected and their object_ids
|
279
|
+
# reused. This is the case with C-ruby.
|
280
|
+
saved_pointers = []
|
281
|
+
ids = (0...2000).map {
|
282
|
+
o = q.pop
|
283
|
+
saved_pointers << o
|
284
|
+
o.object_id
|
285
|
+
}
|
286
|
+
#pp s.objs
|
287
|
+
ids.uniq.length.should == ids.length
|
288
|
+
end
|
289
|
+
|
290
|
+
it "should receive messages in order from a connection" do
|
291
|
+
q = Queue.new
|
292
|
+
s = server do |i|
|
293
|
+
q << i
|
294
|
+
i
|
295
|
+
end
|
296
|
+
100.times { |i|
|
297
|
+
cast(i)
|
298
|
+
}
|
299
|
+
100.times { |i|
|
300
|
+
q.pop == i
|
301
|
+
}
|
302
|
+
end
|
303
|
+
|
304
|
+
it "should resend message" do
|
305
|
+
pending
|
306
|
+
i = 0
|
307
|
+
q = Queue.new
|
308
|
+
s = server do |data|
|
309
|
+
q << [header,method,data,meta]
|
310
|
+
# requeue the first 100 messages
|
311
|
+
i += 1
|
312
|
+
if i <= 100
|
313
|
+
resend
|
314
|
+
end
|
315
|
+
true
|
316
|
+
end
|
317
|
+
100.times { |i|
|
318
|
+
cast(i,{ :message_id => i }, i)
|
319
|
+
}
|
320
|
+
# the first 100 should be the same message as the messages to 200
|
321
|
+
|
322
|
+
msgs100 = 100.times.map { q.pop }
|
323
|
+
msgs200 = 100.times.map { q.pop }
|
324
|
+
msgs100.zip(msgs200) { |m1,m2|
|
325
|
+
header1,method1,data1,meta1 = m1
|
326
|
+
header2,method2,data2,meta2 = m2
|
327
|
+
|
328
|
+
# everything should be the same except the delivery_tag
|
329
|
+
header2.delivery_tag.should_not == header1.delivery_tag
|
330
|
+
# requeue is different from AMQP's redelivery
|
331
|
+
## it should resend the message as another one.
|
332
|
+
header1.redelivered.should_not == true
|
333
|
+
header2.redelivered.should_not == true
|
334
|
+
|
335
|
+
header2.message_id.should == header2.message_id
|
336
|
+
method2.should == method1
|
337
|
+
data2.should == data1
|
338
|
+
meta1.should == meta2
|
339
|
+
}
|
340
|
+
end
|
341
|
+
|
342
|
+
it "unsubscribes from queue" do
|
343
|
+
pending
|
344
|
+
q1 = Queue.new
|
345
|
+
s1 = server do |data|
|
346
|
+
q1 << data
|
347
|
+
end
|
348
|
+
(1..10).map { |i| cast(i) }
|
349
|
+
(1..10).map { q1.pop }.should == (1..10).to_a
|
350
|
+
# s1.stop { q1 << :unsubscribed } # lame, this somehow causes blocking.
|
351
|
+
# q1.pop.should == :unsubscribed
|
352
|
+
sleep(1)
|
353
|
+
(1..10).map { |i| cast(i) } # these should be queued until another server comes up
|
354
|
+
q1.should be_empty
|
355
|
+
q2 = Queue.new
|
356
|
+
s2 = server do |data|
|
357
|
+
q2 << data
|
358
|
+
end
|
359
|
+
(1..10).map { q2.pop }.should == (1..10).to_a
|
360
|
+
end
|
361
|
+
|
362
|
+
end
|
363
|
+
|
364
|
+
describe "funnel and tunnel" do
|
365
|
+
before do
|
366
|
+
start_ass
|
367
|
+
end
|
368
|
+
|
369
|
+
after do
|
370
|
+
stop_ass
|
371
|
+
end
|
372
|
+
|
373
|
+
def funnel(i,match)
|
374
|
+
q = Queue.new
|
375
|
+
ASS::Topic.funnel("spec-tunnel","spec-funnel-#{i}",match) {
|
376
|
+
define_method(:on_event) do |key,data|
|
377
|
+
q << [key,data]
|
378
|
+
end
|
379
|
+
}
|
380
|
+
q
|
381
|
+
end
|
382
|
+
|
383
|
+
def tunnel
|
384
|
+
ASS::Topic.tunnel("spec-tunnel")
|
385
|
+
end
|
386
|
+
|
387
|
+
def event(key,data)
|
388
|
+
ASS::Topic.event("spec-tunnel",key,data)
|
389
|
+
end
|
390
|
+
|
391
|
+
it "event notification from tunnel to funnel" do
|
392
|
+
tunnel
|
393
|
+
|
394
|
+
all = funnel(0,"*")
|
395
|
+
evens = funnel(1,"even")
|
396
|
+
odds = funnel(2,"odd")
|
397
|
+
|
398
|
+
sleep(0.1) # yield
|
399
|
+
|
400
|
+
10.times { |i|
|
401
|
+
key = i.even? ? "even" : "odd"
|
402
|
+
event(key,i)
|
403
|
+
}
|
404
|
+
|
405
|
+
all = 10.times.map { all.pop }
|
406
|
+
all.each do |(key,data)|
|
407
|
+
key.match(/^even|odd$/).should_not be_nil
|
408
|
+
end
|
409
|
+
|
410
|
+
evens = 5.times.map { evens.pop }
|
411
|
+
evens.each do |(key,data)|
|
412
|
+
key.should == "even"
|
413
|
+
data.should be_even
|
414
|
+
end
|
415
|
+
|
416
|
+
odds = 5.times.map { odds.pop }
|
417
|
+
odds.each do |(key,data)|
|
418
|
+
key.should == "odd"
|
419
|
+
data.should be_odd
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
end
|
424
|
+
|
425
|
+
end
|