amqp 0.7.0.pre → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/CHANGELOG +8 -2
  4. data/CONTRIBUTORS +22 -0
  5. data/Gemfile +3 -3
  6. data/README.md +20 -11
  7. data/Rakefile +30 -6
  8. data/amqp.gemspec +1 -1
  9. data/bin/cleanify.rb +50 -0
  10. data/examples/amqp/simple.rb +6 -4
  11. data/examples/mq/ack.rb +8 -6
  12. data/examples/mq/automatic_binding_for_default_direct_exchange.rb +65 -0
  13. data/examples/mq/callbacks.rb +9 -1
  14. data/examples/mq/clock.rb +17 -17
  15. data/examples/mq/hashtable.rb +19 -10
  16. data/examples/mq/internal.rb +13 -11
  17. data/examples/mq/logger.rb +38 -36
  18. data/examples/mq/multiclock.rb +16 -7
  19. data/examples/mq/pingpong.rb +16 -7
  20. data/examples/mq/pop.rb +8 -6
  21. data/examples/mq/primes-simple.rb +2 -0
  22. data/examples/mq/primes.rb +7 -5
  23. data/examples/mq/stocks.rb +14 -5
  24. data/lib/amqp.rb +12 -8
  25. data/lib/amqp/buffer.rb +35 -158
  26. data/lib/amqp/client.rb +34 -22
  27. data/lib/amqp/frame.rb +8 -64
  28. data/lib/amqp/protocol.rb +21 -70
  29. data/lib/amqp/server.rb +11 -9
  30. data/lib/amqp/spec.rb +8 -6
  31. data/lib/amqp/version.rb +2 -0
  32. data/lib/ext/blankslate.rb +3 -1
  33. data/lib/ext/em.rb +2 -0
  34. data/lib/ext/emfork.rb +13 -11
  35. data/lib/mq.rb +253 -156
  36. data/lib/mq/collection.rb +6 -88
  37. data/lib/mq/exchange.rb +70 -13
  38. data/lib/mq/header.rb +12 -6
  39. data/lib/mq/logger.rb +9 -7
  40. data/lib/mq/queue.rb +42 -30
  41. data/lib/mq/rpc.rb +6 -4
  42. data/protocol/codegen.rb +20 -18
  43. data/research/api.rb +10 -46
  44. data/research/primes-forked.rb +9 -7
  45. data/research/primes-processes.rb +74 -72
  46. data/research/primes-threaded.rb +9 -7
  47. data/spec/integration/automatic_binding_for_default_direct_exchange_spec.rb +61 -0
  48. data/spec/mq_helper.rb +70 -0
  49. data/spec/spec_helper.rb +84 -29
  50. data/spec/unit/amqp/buffer_spec.rb +178 -0
  51. data/spec/unit/amqp/client_spec.rb +472 -0
  52. data/spec/unit/amqp/frame_spec.rb +60 -0
  53. data/spec/unit/amqp/misc_spec.rb +123 -0
  54. data/spec/unit/amqp/protocol_spec.rb +53 -0
  55. data/spec/unit/mq/channel_close_spec.rb +15 -0
  56. data/spec/unit/mq/collection_spec.rb +129 -0
  57. data/spec/unit/mq/exchange_declaration_spec.rb +524 -0
  58. data/spec/unit/mq/misc_spec.rb +228 -0
  59. data/spec/unit/mq/mq_basic_spec.rb +39 -0
  60. data/spec/unit/mq/queue_declaration_spec.rb +97 -0
  61. data/spec/unit/mq/queue_spec.rb +71 -0
  62. metadata +33 -21
  63. data/Gemfile.lock +0 -16
  64. data/old/README +0 -30
  65. data/old/Rakefile +0 -12
  66. data/old/amqp-0.8.json +0 -606
  67. data/old/amqp_spec.rb +0 -796
  68. data/old/amqpc.rb +0 -695
  69. data/old/codegen.rb +0 -148
  70. data/spec/channel_close_spec.rb +0 -13
  71. data/spec/sync_async_spec.rb +0 -52
@@ -0,0 +1,472 @@
1
+ # encoding: utf-8
2
+
3
+ require "spec_helper"
4
+ require "amqp/client"
5
+
6
+ describe AMQP::Client do
7
+ include AMQP::SpecHelper
8
+ include AMQP
9
+
10
+ em_after { AMQP.cleanup_state }
11
+
12
+ context 'with AMQP.client set to BasicClient (default)' do
13
+ describe 'creating new connection to AMQP broker using .connect:', :broker => true do
14
+ it 'when .connect is called outside EM event loop, raises error' do
15
+ expect { Client.connect }.to raise_error /eventmachine not initialized/
16
+ end
17
+
18
+ context 'when .connect is called inside EM event loop:' do
19
+ include AMQP::SpecHelper
20
+
21
+ it 'calls EM.connect with AMQP::Client as a handler class and AMQP options, and' do
22
+ em do
23
+ opts = AMQP.settings.merge(AMQP_OPTS)
24
+ EM.should_receive(:connect).with(opts[:host], opts[:port], Client, opts)
25
+ Client.connect AMQP_OPTS
26
+ done
27
+ end
28
+ end
29
+
30
+ it 'either hits connection timeout (with :timeout option), or' do
31
+ expect { em do
32
+ Client.connect(:host => 'example.com', :timeout => 0.001)
33
+ done(2)
34
+ end
35
+ }.to raise_error AMQP::Error, /Could not connect to server example.com:5672/
36
+ end
37
+
38
+ it 'raises connection error (with unresolvable AMQP broker address), or' do
39
+ pending 'Need to rewrite to eliminate dependency on DNS loockup mechanism'
40
+ expect { em do
41
+ Client.connect(:host => 'impossible.')
42
+ done(2)
43
+ end
44
+ }.to raise_error EventMachine::ConnectionError, /unable to resolve server address/
45
+ end
46
+
47
+ it 'raises connection error (with wrong AMQP host), or' do
48
+ pending 'Need to rewrite to eliminate dependency on DNS loockup mechanism'
49
+ expect { em do
50
+ Client.connect(:host => 'example.com')
51
+ done(2)
52
+ end
53
+ }.to raise_error AMQP::Error, /Could not connect to server example.com:5672/
54
+ end
55
+
56
+ it 'raises connection error (with wrong AMQP port), or' do
57
+ expect { em do
58
+ Client.connect(:port => 131313)
59
+ done(2)
60
+ end
61
+ }.to raise_error AMQP::Error, /Could not connect to server 127.0.0.1:131313/
62
+ end
63
+
64
+ it 'returns client connecting to AMQP broker (with correct AMQP opts) - DELAY!' do
65
+ em do
66
+ client = Client.connect AMQP_OPTS
67
+ client.should_not be_connected
68
+ done(0.2) { client.should be_connected }
69
+ end
70
+ end
71
+
72
+ context 'when connection is attempted, initiated client:' do
73
+ include AMQP::EMSpec
74
+ em_before { @client = Client.connect AMQP_OPTS }
75
+
76
+ specify { @client.should_not be_connected; done }
77
+
78
+ context "after a delay, connection is established and the client:" do
79
+ it 'receives #connection_completed call from EM reactor' do
80
+ @client.should_receive(:connection_completed).with(no_args)
81
+ done(0.2)
82
+ end
83
+
84
+ it 'fires @connection_status block (if any) with :connected' do
85
+ # Set up status processing block first
86
+ @client.connection_status { |status| @status = status }
87
+ done(0.2) { @status.should == :connected }
88
+ end
89
+
90
+ it 'becomes connected' do
91
+ done(0.2) { @client.should be_connected }
92
+ end
93
+ end
94
+ end
95
+
96
+ end # context 'when .connect is called inside EM event loop:
97
+ end # creating new connection to AMQP broker using .connect
98
+
99
+ context 'given a client that is attempting to connect to AMQP broker using AMQP_OPTS' do
100
+ include AMQP::EMSpec
101
+ em_before { @client = Client.connect AMQP_OPTS }
102
+ subject { @client }
103
+
104
+ describe 'closing down' do
105
+ context 'calling #close' do
106
+ it 'just sets callback (to close connection) if @client is not yet connected' do
107
+ @client.should_not be_connected
108
+ @client.should_not_receive(:send)
109
+ @client.should_receive(:callback)
110
+ @client.close
111
+ done
112
+ end
113
+
114
+ context 'if @client is connected'
115
+ it 'closes all channels first if there are any' do
116
+ EM.add_timer(0.2) {
117
+ @client.should be_connected
118
+ channel1 = MQ.new(@client)
119
+ channel2 = MQ.new(@client)
120
+ channel1.should_receive(:close)
121
+ channel2.should_receive(:close)
122
+ @client.close
123
+ done }
124
+ end
125
+
126
+ it 'sends Protocol::Connection::Close method to broker' do
127
+ EM.add_timer(0.2) {
128
+ @client.should be_connected
129
+ @client.should_receive(:send) do |message|
130
+ message.should be_a Protocol::Connection::Close
131
+ end
132
+ @client.close
133
+ done }
134
+ end
135
+ end # 'calling #close'
136
+
137
+ context 'after broker responded to connection close request' do
138
+ it 'client receives Protocol::Connection::CloseOk method from broker' do
139
+ EM.add_timer(0.2) {
140
+ @client.should_receive(:process_frame) do |frame|
141
+ frame.payload.should be_a Protocol::Connection::CloseOk
142
+ end
143
+ @client.close
144
+ done(0.2) }
145
+ end
146
+
147
+ it 'calls block given to #close instead of pre-set callbacks' do
148
+ # stub it BEFORE #connection_completed fired!
149
+ @client.should_not_receive(:disconnected)
150
+ @client.should_not_receive(:reconnect)
151
+ EM.add_timer(0.2) {
152
+ @client.close { @on_disconnect_called = true }
153
+ done(0.2) { @on_disconnect_called.should be_true } }
154
+ end
155
+
156
+ context 'if no block was given to #close' do
157
+
158
+ it 'calls registered @on_disconnect hook' do
159
+ EM.add_timer(0.2) {
160
+ mock = mock('on_disconnect')
161
+ mock.should_receive(:call).with(no_args)
162
+
163
+ # subject_mock trips something and drops connection :(
164
+ @client.instance_exec { @on_disconnect = mock }
165
+ @client.close
166
+ done(0.2) }
167
+ end
168
+
169
+ it 'normally, @on_disconnect just calls private #disconnected method' do
170
+ # stub it BEFORE #connection_completed fired!
171
+ @client.should_receive(:disconnected)
172
+ EM.add_timer(0.2) { @client.close; done(0.2) }
173
+ end
174
+
175
+ it 'fires @connection_status block (if any) with :disconnected' do
176
+ EM.add_timer(0.2) {
177
+ # Set up status processing block first
178
+ @client.connection_status { |status| @status = status }
179
+ @client.close
180
+ done(0.2) { @status.should == :disconnected } }
181
+ end
182
+
183
+ it 'attempts to reconnect' do
184
+ @client.should_receive(:reconnect)
185
+ EM.add_timer(0.2) { @client.close; done(0.2) }
186
+ end
187
+ end # if no block was given to #close
188
+ end # after broker responded to connection close request
189
+
190
+ context '#unbind' do
191
+ it 'is a hook method called by EM when network connection is dropped' do
192
+ @client.should_receive(:unbind).with(no_args)
193
+ @client.close
194
+ done
195
+ end
196
+
197
+ it 'unsets @connected status' do
198
+ EM.add_timer(0.1) {
199
+ @client.should be_connected
200
+ @client.unbind
201
+ @client.should_not be_connected
202
+ done }
203
+ end
204
+
205
+ it 'seems that it implicitly calls #connection_completed' do
206
+ @client.should_receive(:connection_completed) do
207
+ @client.instance_exec do
208
+ # Avoid raising exception
209
+ @on_disconnect = method(:disconnected)
210
+ end
211
+ end
212
+ @client.unbind
213
+ done
214
+ end
215
+
216
+ it 'calls @on_disconnect hook' do
217
+ block_fired = false
218
+ EM.add_timer(0.1) {
219
+ @client.instance_exec do
220
+ @on_disconnect = proc { block_fired = true }
221
+ end
222
+ @client.unbind
223
+ done(0.1) { block_fired.should be_true } }
224
+ end
225
+ end
226
+ end # closing down
227
+
228
+ context 'reconnecting' do
229
+ include AMQP::EMSpec
230
+
231
+ it 're-initializes the client' do
232
+ @client.should_receive(:initialize)
233
+ @client.reconnect
234
+ done
235
+ end
236
+
237
+ it 'resets and clears existing channels, if any' do
238
+ channel1 = MQ.new(@client)
239
+ channel2 = MQ.new(@client)
240
+ EM.add_timer(0.2) {
241
+ channel1.should_receive(:reset)
242
+ channel2.should_receive(:reset)
243
+ @client.reconnect
244
+ @client.channels.should == {}
245
+ done
246
+ }
247
+ end
248
+
249
+ it 'reconnects to broker through EM.reconnect' do
250
+ EM.should_receive(:reconnect)
251
+ @client.reconnect
252
+ done
253
+ end
254
+
255
+ it 'delays reconnect attempt by 1 sec if reconnect is already in progress' do
256
+ EM.should_receive(:reconnect).exactly(1).times
257
+ EM.should_receive(:add_timer).with(1)
258
+ @client.reconnect
259
+ @client.reconnect
260
+ done
261
+ end
262
+
263
+ it 'attempts to automatically reconnect on #close' do
264
+ @client.should_receive(:reconnect)
265
+ EM.add_timer(0.2) { @client.close; done(0.2) }
266
+ end
267
+ end
268
+
269
+ context 'receiving data/processing frames' do
270
+ describe "#receive_data" do
271
+ it 'is a callback method that EM calls when it gets raw data from broker' do
272
+ @client.should_receive(:receive_data) do |data|
273
+ frame = Frame.parse(data)
274
+ frame.should be_an Frame::Method
275
+ frame.payload.should be_an Protocol::Connection::Start
276
+ done
277
+ end
278
+ end
279
+
280
+ it 'delegates actual frame processing to #process_frame' do
281
+ EM.add_timer(0.2) {
282
+ @client.should_receive(:process_frame).with(basic_header)
283
+ @client.receive_data(basic_header.to_s)
284
+ done }
285
+ end
286
+
287
+ it 'does not call #process_frame on incomplete frames' do
288
+ EM.add_timer(0.2) {
289
+ @client.should_not_receive(:process_frame)
290
+ @client.receive_data(basic_header.to_s[0..5])
291
+ done }
292
+ end
293
+
294
+ it 'calls #process_frame when frame is complete' do
295
+ EM.add_timer(0.2) {
296
+ @client.should_receive(:process_frame).with(basic_header)
297
+ @client.receive_data(basic_header.to_s[0..5])
298
+ @client.receive_data(basic_header.to_s[6..-1])
299
+ done }
300
+ end
301
+ end #receive_data
302
+
303
+ describe '#process_frame', '(client reaction to messages from broker)' do
304
+
305
+ it 'delegates frame processing to channel if the frame indicates channel' do
306
+ EM.add_timer(0.2) {
307
+ MQ.new(@client)
308
+ @client.channels[1].should_receive(:process_frame).
309
+ with(basic_header(:channel => 1))
310
+ @client.process_frame(basic_header(:channel => 1))
311
+ done }
312
+ end
313
+
314
+ describe ' Frame::Method', '(Method with args received)' do
315
+ describe 'Protocol::Connection::Start',
316
+ '(broker initiated start of AMQP connection)' do
317
+ let(:frame) { Frame::Method.new(Protocol::Connection::Start.new) }
318
+
319
+ it 'sends back to broker Protocol::Connection::StartOk with details' do
320
+ EM.add_timer(0.2) {
321
+ @client.should_receive(:send).
322
+ with(Protocol::Connection::StartOk.new(
323
+ {:platform => 'Ruby/EventMachine',
324
+ :product => 'AMQP',
325
+ :information => 'http://github.com/ruby-amqp/amqp',
326
+ :version => AMQP::VERSION}, 'AMQPLAIN',
327
+ {:LOGIN => AMQP.settings[:user],
328
+ :PASSWORD => AMQP.settings[:pass]}, 'en_US'))
329
+ @client.process_frame(frame)
330
+ done }
331
+ end
332
+ end # Protocol::Connection::Start
333
+
334
+ describe 'Protocol::Connection::Tune',
335
+ '(broker suggested connection parameters)' do
336
+ let(:frame) { Frame::Method.new(Protocol::Connection::Tune.new) }
337
+
338
+ it 'sends back to broker TuneOk with parameters and Open' do
339
+ EM.add_timer(0.2) {
340
+ @client.should_receive(:send) do |method|
341
+ if method.is_a? Protocol::Connection::TuneOk
342
+ method.should == Protocol::Connection::TuneOk.new(
343
+ :channel_max => 0,
344
+ :frame_max => 131072,
345
+ :heartbeat => 0)
346
+ else
347
+ method.should == Protocol::Connection::Open.new(
348
+ :virtual_host => AMQP.settings[:vhost],
349
+ :capabilities => '',
350
+ :insist => AMQP.settings[:insist])
351
+ end
352
+ end.exactly(2).times
353
+ @client.process_frame(frame)
354
+ done }
355
+ end
356
+ end # Protocol::Connection::Tune
357
+
358
+ describe 'Protocol::Connection::OpenOk',
359
+ '(broker confirmed connection opening)' do
360
+ let(:frame) { Frame::Method.new(AMQP::Protocol::Connection::OpenOk.new) }
361
+
362
+ it 'succeeds @client (as a deferrable)' do
363
+ @client.instance_variable_get(:@deferred_status).should == :unknown
364
+ @client.process_frame(frame)
365
+ done(0.1) {
366
+ @client.instance_variable_get(:@deferred_status).should == :succeeded }
367
+ end
368
+ end # Protocol::Connection::OpenOk
369
+
370
+ describe 'Protocol::Connection::Close',
371
+ '(unexpectedly received connection close from broker)' do
372
+ let(:frame) { Frame::Method.new(Protocol::Connection::Close.new(
373
+ :reply_code => 320,
374
+ :reply_text => "Nyanya",
375
+ :class_id => 10,
376
+ :method_id => 40)) }
377
+
378
+ it 'just prints debug info to STDERR' do
379
+ STDERR.should_receive(:puts).
380
+ with /Nyanya in AMQP::Protocol::Connection::Open/
381
+ @client.process_frame(frame)
382
+ done(0.1)
383
+ end
384
+
385
+ it 'should probably raise exception or something?'
386
+ end # Protocol::Connection::Close
387
+
388
+ describe 'Protocol::Connection::CloseOk',
389
+ '(broker confirmed our connection close request)' do
390
+ let(:frame) { Frame::Method.new(Protocol::Connection::CloseOk.new) }
391
+
392
+ it 'calls registered @on_disconnect hook' do
393
+ EM.add_timer(0.1) {
394
+ subject_mock(:@on_disconnect).should_receive(:call).with(no_args)
395
+ @client.process_frame(frame)
396
+ done }
397
+ end
398
+
399
+ end # Protocol::Connection::CloseOk
400
+ end # Frame::Method
401
+ end #process_frame
402
+ end # receiving data/processing frames
403
+
404
+ context 'channels' do
405
+ it 'starts with empty channels set' do
406
+ @client.channels.should be_empty
407
+ done
408
+ end
409
+
410
+ context '#add_channel' do
411
+ it 'adds given channel to channels set' do
412
+ mq = mock('mq')
413
+ @client.add_channel mq
414
+ @client.channels[1].should == mq
415
+ done
416
+ end
417
+ end
418
+ end
419
+
420
+ context '#send data' do
421
+ it 'formats data to be sent into Frame, setting channel to 0 by default' do
422
+ data = mock('data')
423
+ framed_data = mock('framed data')
424
+ data.should_receive(:to_frame).and_return(framed_data)
425
+ framed_data.should_receive(:channel=).with(0)
426
+ @client.send data
427
+ done
428
+ end
429
+
430
+ it 'sets channel number given in options' do
431
+ data = mock('data')
432
+ framed_data = mock('framed data')
433
+ data.should_receive(:to_frame).and_return(framed_data)
434
+ framed_data.should_receive(:channel=).with(1313)
435
+ @client.send data, :channel =>1313
436
+ done
437
+ end
438
+
439
+ it 'does not format data if it is already a Frame' do
440
+ data = basic_header
441
+ data.should_not_receive(:to_frame)
442
+ @client.send data
443
+ done
444
+ end
445
+
446
+ it 'then calls EM:Connection#send_data hook with framed data converted to String' do
447
+ EM.add_timer(0.1) {
448
+ data = basic_header
449
+ @client.should_receive(:send_data) do |data|
450
+ data.should be_a String
451
+ frame = Frame.parse(data)
452
+ frame.class.should == Frame::Header
453
+ frame.payload.class.should == Protocol::Header
454
+ frame.channel.should == 0
455
+ end
456
+ @client.send data
457
+ done }
458
+ end
459
+ end #send data
460
+
461
+ context '#connection_status' do
462
+ em_before { @called_with_statuses = [] }
463
+ it 'sets a block to be called on connection status changes' do
464
+ @client.connection_status { |status| @called_with_statuses << status }
465
+ @client.close
466
+ done(0.1) { @called_with_statuses.should == [:connected, :disconnected] }
467
+ end
468
+
469
+ end #connection_status
470
+ end #given a connected client
471
+ end # context with AMQP.client set to BasicClient (default)
472
+ end