bunny 0.9.0.pre6 → 0.9.0.pre7
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 +1 -0
- data/ChangeLog.md +56 -0
- data/Gemfile +5 -0
- data/README.md +12 -6
- data/bunny.gemspec +1 -1
- data/examples/connection/automatic_recovery_with_basic_get.rb +34 -0
- data/examples/connection/automatic_recovery_with_client_named_queues.rb +6 -3
- data/examples/connection/automatic_recovery_with_server_named_queues.rb +6 -3
- data/lib/bunny/channel.rb +958 -90
- data/lib/bunny/channel_id_allocator.rb +10 -1
- data/lib/bunny/consumer.rb +44 -10
- data/lib/bunny/exceptions.rb +9 -0
- data/lib/bunny/exchange.rb +91 -12
- data/lib/bunny/main_loop.rb +27 -23
- data/lib/bunny/queue.rb +129 -9
- data/lib/bunny/session.rb +118 -24
- data/lib/bunny/system_timer.rb +5 -1
- data/lib/bunny/transport.rb +8 -5
- data/lib/bunny/version.rb +1 -1
- data/spec/higher_level_api/integration/basic_ack_spec.rb +5 -3
- data/spec/higher_level_api/integration/basic_consume_spec.rb +30 -2
- data/spec/higher_level_api/integration/basic_nack_spec.rb +22 -0
- data/spec/higher_level_api/integration/basic_publish_spec.rb +0 -47
- data/spec/higher_level_api/integration/basic_reject_spec.rb +22 -0
- data/spec/higher_level_api/integration/predeclared_exchanges_spec.rb +24 -0
- data/spec/higher_level_api/integration/publishing_edge_cases_spec.rb +63 -0
- data/spec/issues/issue97_attachment.json +1 -0
- data/spec/issues/issue97_spec.rb +174 -0
- metadata +214 -197
data/lib/bunny/session.rb
CHANGED
@@ -15,21 +15,28 @@ require "amq/protocol/client"
|
|
15
15
|
require "amq/settings"
|
16
16
|
|
17
17
|
module Bunny
|
18
|
+
# Represents AMQP 0.9.1 connection (connection to RabbitMQ).
|
19
|
+
# @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
|
18
20
|
class Session
|
19
21
|
|
22
|
+
# Default host used for connection
|
20
23
|
DEFAULT_HOST = "127.0.0.1"
|
24
|
+
# Default virtual host used for connection
|
21
25
|
DEFAULT_VHOST = "/"
|
26
|
+
# Default username used for connection
|
22
27
|
DEFAULT_USER = "guest"
|
28
|
+
# Default password used for connection
|
23
29
|
DEFAULT_PASSWORD = "guest"
|
24
|
-
# the same value as RabbitMQ 3.0 uses.
|
30
|
+
# Default heartbeat interval, the same value as RabbitMQ 3.0 uses.
|
25
31
|
DEFAULT_HEARTBEAT = 600
|
26
|
-
#
|
32
|
+
# @private
|
27
33
|
DEFAULT_FRAME_MAX = 131072
|
28
34
|
|
29
35
|
# backwards compatibility
|
36
|
+
# @private
|
30
37
|
CONNECT_TIMEOUT = Transport::DEFAULT_CONNECTION_TIMEOUT
|
31
38
|
|
32
|
-
|
39
|
+
# RabbitMQ client metadata
|
33
40
|
DEFAULT_CLIENT_PROPERTIES = {
|
34
41
|
:capabilities => {
|
35
42
|
:publisher_confirms => true,
|
@@ -40,7 +47,7 @@ module Bunny
|
|
40
47
|
:product => "Bunny",
|
41
48
|
:platform => ::RUBY_DESCRIPTION,
|
42
49
|
:version => Bunny::VERSION,
|
43
|
-
:information => "http://
|
50
|
+
:information => "http://rubybunny.info",
|
44
51
|
}
|
45
52
|
|
46
53
|
DEFAULT_LOCALE = "en_GB"
|
@@ -50,7 +57,7 @@ module Bunny
|
|
50
57
|
# API
|
51
58
|
#
|
52
59
|
|
53
|
-
attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max
|
60
|
+
attr_reader :status, :host, :port, :heartbeat, :user, :pass, :vhost, :frame_max, :threaded
|
54
61
|
attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
|
55
62
|
attr_reader :default_channel
|
56
63
|
attr_reader :channel_id_allocator
|
@@ -59,6 +66,21 @@ module Bunny
|
|
59
66
|
attr_reader :mechanism
|
60
67
|
|
61
68
|
|
69
|
+
# @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
|
70
|
+
# @param [Hash] optz Extra options not related to connection
|
71
|
+
#
|
72
|
+
# @option connection_string_or_opts [String] :host ("127.0.0.1") Hostname or IP address to connect to
|
73
|
+
# @option connection_string_or_opts [Integer] :port (5672) Port RabbitMQ listens on
|
74
|
+
# @option connection_string_or_opts [String] :username ("guest") Username
|
75
|
+
# @option connection_string_or_opts [String] :password ("guest") Password
|
76
|
+
# @option connection_string_or_opts [String] :vhost ("/") Virtual host to use
|
77
|
+
# @option connection_string_or_opts [Integer] :heartbeat (600) Heartbeat interval. 0 means no heartbeat.
|
78
|
+
#
|
79
|
+
# @option optz [String] :auth_mechanism ("PLAIN") Authentication mechanism, PLAIN or EXTERNAL
|
80
|
+
# @option optz [String] :locale ("PLAIN") Locale RabbitMQ should use
|
81
|
+
#
|
82
|
+
# @see http://rubybunny.info/articles/connecting.html Connecting to RabbitMQ guide
|
83
|
+
# @api public
|
62
84
|
def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
|
63
85
|
opts = case (ENV["RABBITMQ_URL"] || connection_string_or_opts)
|
64
86
|
when nil then
|
@@ -77,6 +99,7 @@ module Bunny
|
|
77
99
|
@vhost = self.vhost_from(opts)
|
78
100
|
@logfile = opts[:logfile]
|
79
101
|
@logging = opts[:logging] || false
|
102
|
+
@threaded = opts.fetch(:threaded, true)
|
80
103
|
|
81
104
|
@status = :not_connected
|
82
105
|
|
@@ -95,21 +118,29 @@ module Bunny
|
|
95
118
|
@continuations = ::Queue.new
|
96
119
|
end
|
97
120
|
|
121
|
+
# @return [String] RabbitMQ hostname (or IP address) used
|
98
122
|
def hostname; self.host; end
|
123
|
+
# @return [String] Username used
|
99
124
|
def username; self.user; end
|
125
|
+
# @return [String] Password used
|
100
126
|
def password; self.pass; end
|
127
|
+
# @return [String] Virtual host used
|
101
128
|
def virtual_host; self.vhost; end
|
102
129
|
|
130
|
+
# @return [Boolean] true if this connection uses TLS (SSL)
|
103
131
|
def uses_tls?
|
104
132
|
@transport.uses_tls?
|
105
133
|
end
|
106
134
|
alias tls? uses_tls?
|
107
135
|
|
136
|
+
# @return [Boolean] true if this connection uses TLS (SSL)
|
108
137
|
def uses_ssl?
|
109
138
|
@transport.uses_ssl?
|
110
139
|
end
|
111
140
|
alias ssl? uses_ssl?
|
112
141
|
|
142
|
+
# Starts connection process
|
143
|
+
# @api public
|
113
144
|
def start
|
114
145
|
@continuations = ::Queue.new
|
115
146
|
@status = :connecting
|
@@ -120,12 +151,20 @@ module Bunny
|
|
120
151
|
self.open_connection
|
121
152
|
|
122
153
|
@event_loop = nil
|
123
|
-
self.start_main_loop
|
154
|
+
self.start_main_loop if @threaded
|
124
155
|
|
125
156
|
@default_channel = self.create_channel
|
126
157
|
end
|
127
158
|
|
159
|
+
def read_write_timeout
|
160
|
+
@transport.read_write_timeout
|
161
|
+
end
|
128
162
|
|
163
|
+
# Opens a new channel and returns it. This method will block the calling
|
164
|
+
# thread until the response is received and the channel is guaranteed to be
|
165
|
+
# opened (this operation is very fast and inexpensive).
|
166
|
+
#
|
167
|
+
# @return [Bunny::Channel] Newly opened channel
|
129
168
|
def create_channel(n = nil)
|
130
169
|
if n && (ch = @channels[n])
|
131
170
|
ch
|
@@ -137,11 +176,12 @@ module Bunny
|
|
137
176
|
end
|
138
177
|
alias channel create_channel
|
139
178
|
|
179
|
+
# Closes the connection. This involves closing all of its channels.
|
140
180
|
def close
|
141
181
|
if @transport.open?
|
142
182
|
close_all_channels
|
143
183
|
|
144
|
-
Bunny::Timer.timeout(@disconnect_timeout, ClientTimeout) do
|
184
|
+
Bunny::Timer.timeout(@transport.disconnect_timeout, ClientTimeout) do
|
145
185
|
self.close_connection(false)
|
146
186
|
end
|
147
187
|
end
|
@@ -166,7 +206,7 @@ module Bunny
|
|
166
206
|
end
|
167
207
|
|
168
208
|
def open?
|
169
|
-
status == :open || status == :connected
|
209
|
+
(status == :open || status == :connected) && @transport.open?
|
170
210
|
end
|
171
211
|
alias connected? open?
|
172
212
|
|
@@ -174,26 +214,32 @@ module Bunny
|
|
174
214
|
# Backwards compatibility
|
175
215
|
#
|
176
216
|
|
217
|
+
# @private
|
177
218
|
def queue(*args)
|
178
219
|
@default_channel.queue(*args)
|
179
220
|
end
|
180
221
|
|
222
|
+
# @private
|
181
223
|
def direct(*args)
|
182
224
|
@default_channel.direct(*args)
|
183
225
|
end
|
184
226
|
|
227
|
+
# @private
|
185
228
|
def fanout(*args)
|
186
229
|
@default_channel.fanout(*args)
|
187
230
|
end
|
188
231
|
|
232
|
+
# @private
|
189
233
|
def topic(*args)
|
190
234
|
@default_channel.topic(*args)
|
191
235
|
end
|
192
236
|
|
237
|
+
# @private
|
193
238
|
def headers(*args)
|
194
239
|
@default_channel.headers(*args)
|
195
240
|
end
|
196
241
|
|
242
|
+
# @private
|
197
243
|
def exchange(*args)
|
198
244
|
@default_channel.exchange(*args)
|
199
245
|
end
|
@@ -203,35 +249,38 @@ module Bunny
|
|
203
249
|
# Implementation
|
204
250
|
#
|
205
251
|
|
206
|
-
|
252
|
+
# @private
|
207
253
|
def open_channel(ch)
|
208
254
|
n = ch.number
|
209
255
|
self.register_channel(ch)
|
210
256
|
|
211
257
|
@transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
|
212
|
-
@last_channel_open_ok =
|
258
|
+
@last_channel_open_ok = wait_on_continuations
|
213
259
|
raise_if_continuation_resulted_in_a_connection_error!
|
214
260
|
|
215
261
|
@last_channel_open_ok
|
216
262
|
end
|
217
263
|
|
264
|
+
# @private
|
218
265
|
def close_channel(ch)
|
219
266
|
n = ch.number
|
220
267
|
|
221
268
|
@transport.send_frame(AMQ::Protocol::Channel::Close.encode(n, 200, "Goodbye", 0, 0))
|
222
|
-
@last_channel_close_ok =
|
269
|
+
@last_channel_close_ok = wait_on_continuations
|
223
270
|
raise_if_continuation_resulted_in_a_connection_error!
|
224
271
|
|
225
272
|
self.unregister_channel(ch)
|
226
273
|
@last_channel_close_ok
|
227
274
|
end
|
228
275
|
|
276
|
+
# @private
|
229
277
|
def close_all_channels
|
230
278
|
@channels.reject {|n, ch| n == 0 || !ch.open? }.each do |_, ch|
|
231
|
-
Bunny::Timer.timeout(@disconnect_timeout, ClientTimeout) { ch.close }
|
279
|
+
Bunny::Timer.timeout(@transport.disconnect_timeout, ClientTimeout) { ch.close }
|
232
280
|
end
|
233
281
|
end
|
234
282
|
|
283
|
+
# @private
|
235
284
|
def close_connection(sync = true)
|
236
285
|
@transport.send_frame(AMQ::Protocol::Connection::Close.encode(200, "Goodbye", 0, 0))
|
237
286
|
|
@@ -239,10 +288,11 @@ module Bunny
|
|
239
288
|
@status = :not_connected
|
240
289
|
|
241
290
|
if sync
|
242
|
-
@last_connection_close_ok =
|
291
|
+
@last_connection_close_ok = wait_on_continuations
|
243
292
|
end
|
244
293
|
end
|
245
294
|
|
295
|
+
# @private
|
246
296
|
def handle_frame(ch_number, method)
|
247
297
|
# puts "Session#handle_frame on #{ch_number}: #{method.inspect}"
|
248
298
|
case method
|
@@ -252,17 +302,17 @@ module Bunny
|
|
252
302
|
@continuations.push(method)
|
253
303
|
when AMQ::Protocol::Connection::Close then
|
254
304
|
@last_connection_error = instantiate_connection_level_exception(method)
|
255
|
-
@
|
305
|
+
@continuations.push(method)
|
256
306
|
when AMQ::Protocol::Connection::CloseOk then
|
257
307
|
@last_connection_close_ok = method
|
258
308
|
begin
|
259
309
|
@continuations.clear
|
260
310
|
|
261
|
-
|
311
|
+
event_loop.stop
|
262
312
|
@event_loop = nil
|
263
313
|
|
264
314
|
@transport.close
|
265
|
-
rescue
|
315
|
+
rescue StandardError => e
|
266
316
|
puts e.class.name
|
267
317
|
puts e.message
|
268
318
|
puts e.backtrace
|
@@ -280,10 +330,15 @@ module Bunny
|
|
280
330
|
when AMQ::Protocol::Basic::GetEmpty then
|
281
331
|
@channels[ch_number].handle_basic_get_empty(method)
|
282
332
|
else
|
283
|
-
@channels[ch_number]
|
333
|
+
if ch = @channels[ch_number]
|
334
|
+
ch.handle_method(method)
|
335
|
+
else
|
336
|
+
# TODO: log a warning
|
337
|
+
end
|
284
338
|
end
|
285
339
|
end
|
286
340
|
|
341
|
+
# @private
|
287
342
|
def raise_if_continuation_resulted_in_a_connection_error!
|
288
343
|
raise @last_connection_error if @last_connection_error
|
289
344
|
end
|
@@ -303,7 +358,10 @@ module Bunny
|
|
303
358
|
end
|
304
359
|
end
|
305
360
|
|
361
|
+
# @private
|
306
362
|
def handle_network_failure(exception)
|
363
|
+
raise NetworkErrorWrapper.new(exception) unless @threaded
|
364
|
+
|
307
365
|
if !recovering_from_network_failure?
|
308
366
|
@recovering_from_network_failure = true
|
309
367
|
if recoverable_network_failure?(exception)
|
@@ -320,15 +378,18 @@ module Bunny
|
|
320
378
|
end
|
321
379
|
end
|
322
380
|
|
381
|
+
# @private
|
323
382
|
def recoverable_network_failure?(exception)
|
324
383
|
# TODO: investigate if we can be a bit smarter here. MK.
|
325
384
|
true
|
326
385
|
end
|
327
386
|
|
387
|
+
# @private
|
328
388
|
def recovering_from_network_failure?
|
329
389
|
@recovering_from_network_failure
|
330
390
|
end
|
331
391
|
|
392
|
+
# @private
|
332
393
|
def recover_from_network_failure
|
333
394
|
begin
|
334
395
|
# puts "About to start recovery..."
|
@@ -346,6 +407,7 @@ module Bunny
|
|
346
407
|
end
|
347
408
|
end
|
348
409
|
|
410
|
+
# @private
|
349
411
|
def recover_channels
|
350
412
|
# default channel is reopened right after connection
|
351
413
|
# negotiation is completed, so make sure we do not try to open
|
@@ -357,10 +419,12 @@ module Bunny
|
|
357
419
|
end
|
358
420
|
end
|
359
421
|
|
422
|
+
# @private
|
360
423
|
def send_raw(*args)
|
361
424
|
@transport.write(*args)
|
362
425
|
end
|
363
426
|
|
427
|
+
# @private
|
364
428
|
def instantiate_connection_level_exception(frame)
|
365
429
|
case frame
|
366
430
|
when AMQ::Protocol::Connection::Close then
|
@@ -379,10 +443,12 @@ module Bunny
|
|
379
443
|
end
|
380
444
|
end
|
381
445
|
|
446
|
+
# @private
|
382
447
|
def hostname_from(options)
|
383
448
|
options[:host] || options[:hostname] || DEFAULT_HOST
|
384
449
|
end
|
385
450
|
|
451
|
+
# @private
|
386
452
|
def port_from(options)
|
387
453
|
fallback = if options[:tls] || options[:ssl]
|
388
454
|
AMQ::Protocol::TLS_PORT
|
@@ -393,36 +459,44 @@ module Bunny
|
|
393
459
|
options.fetch(:port, fallback)
|
394
460
|
end
|
395
461
|
|
462
|
+
# @private
|
396
463
|
def vhost_from(options)
|
397
464
|
options[:virtual_host] || options[:vhost] || DEFAULT_VHOST
|
398
465
|
end
|
399
466
|
|
467
|
+
# @private
|
400
468
|
def username_from(options)
|
401
469
|
options[:username] || options[:user] || DEFAULT_USER
|
402
470
|
end
|
403
471
|
|
472
|
+
# @private
|
404
473
|
def password_from(options)
|
405
474
|
options[:password] || options[:pass] || options [:pwd] || DEFAULT_PASSWORD
|
406
475
|
end
|
407
476
|
|
477
|
+
# @private
|
408
478
|
def heartbeat_from(options)
|
409
479
|
options[:heartbeat] || options[:heartbeat_interval] || options[:requested_heartbeat] || DEFAULT_HEARTBEAT
|
410
480
|
end
|
411
481
|
|
482
|
+
# @private
|
412
483
|
def next_channel_id
|
413
484
|
@channel_id_allocator.next_channel_id
|
414
485
|
end
|
415
486
|
|
487
|
+
# @private
|
416
488
|
def release_channel_id(i)
|
417
489
|
@channel_id_allocator.release_channel_id(i)
|
418
490
|
end
|
419
491
|
|
492
|
+
# @private
|
420
493
|
def register_channel(ch)
|
421
494
|
@channel_mutex.synchronize do
|
422
495
|
@channels[ch.number] = ch
|
423
496
|
end
|
424
497
|
end
|
425
498
|
|
499
|
+
# @private
|
426
500
|
def unregister_channel(ch)
|
427
501
|
@channel_mutex.synchronize do
|
428
502
|
n = ch.number
|
@@ -432,11 +506,17 @@ module Bunny
|
|
432
506
|
end
|
433
507
|
end
|
434
508
|
|
509
|
+
# @private
|
435
510
|
def start_main_loop
|
436
|
-
|
437
|
-
@event_loop.start
|
511
|
+
event_loop.start
|
438
512
|
end
|
439
513
|
|
514
|
+
# @private
|
515
|
+
def event_loop
|
516
|
+
@event_loop ||= MainLoop.new(@transport, self)
|
517
|
+
end
|
518
|
+
|
519
|
+
# @private
|
440
520
|
def signal_activity!
|
441
521
|
@heartbeat_sender.signal_activity! if @heartbeat_sender
|
442
522
|
end
|
@@ -458,7 +538,7 @@ module Bunny
|
|
458
538
|
# Sends multiple frames, one by one. For thread safety this method takes a channel
|
459
539
|
# object and synchronizes on it.
|
460
540
|
#
|
461
|
-
# @api
|
541
|
+
# @api private
|
462
542
|
def send_frameset(frames, channel)
|
463
543
|
# some developers end up sharing channels between threads and when multiple
|
464
544
|
# threads publish on the same channel aggressively, at some point frames will be
|
@@ -473,6 +553,7 @@ module Bunny
|
|
473
553
|
|
474
554
|
protected
|
475
555
|
|
556
|
+
# @api private
|
476
557
|
def init_connection
|
477
558
|
self.send_preamble
|
478
559
|
|
@@ -487,6 +568,7 @@ module Bunny
|
|
487
568
|
@status = :connected
|
488
569
|
end
|
489
570
|
|
571
|
+
# @api private
|
490
572
|
def open_connection
|
491
573
|
@transport.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
|
492
574
|
|
@@ -539,6 +621,7 @@ module Bunny
|
|
539
621
|
raise "could not open connection: server did not respond with connection.open-ok" unless connection_open_ok.is_a?(AMQ::Protocol::Connection::OpenOk)
|
540
622
|
end
|
541
623
|
|
624
|
+
# @api private
|
542
625
|
def negotiate_value(client_value, server_value)
|
543
626
|
if client_value == 0 || server_value == 0
|
544
627
|
[client_value, server_value].max
|
@@ -547,37 +630,48 @@ module Bunny
|
|
547
630
|
end
|
548
631
|
end
|
549
632
|
|
633
|
+
# @api private
|
550
634
|
def initialize_heartbeat_sender
|
551
635
|
# puts "Initializing heartbeat sender..."
|
552
636
|
@heartbeat_sender = HeartbeatSender.new(@transport)
|
553
637
|
@heartbeat_sender.start(@heartbeat)
|
554
638
|
end
|
555
639
|
|
640
|
+
# @api private
|
556
641
|
def maybe_shutdown_heartbeat_sender
|
557
642
|
@heartbeat_sender.stop if @heartbeat_sender
|
558
643
|
end
|
559
644
|
|
560
|
-
|
645
|
+
# @api private
|
561
646
|
def initialize_transport
|
562
647
|
@transport = Transport.new(self, @host, @port, @opts)
|
563
648
|
end
|
564
649
|
|
565
650
|
# Sends AMQ protocol header (also known as preamble).
|
651
|
+
# @api private
|
566
652
|
def send_preamble
|
567
653
|
@transport.send_raw(AMQ::Protocol::PREAMBLE)
|
568
654
|
end
|
569
655
|
|
570
656
|
|
571
|
-
|
572
|
-
|
573
|
-
# @api plugin
|
657
|
+
# @api private
|
574
658
|
def encode_credentials(username, password)
|
575
659
|
@credentials_encoder.encode_credentials(username, password)
|
576
660
|
end # encode_credentials(username, password)
|
577
661
|
|
662
|
+
# @api private
|
578
663
|
def credentials_encoder_for(mechanism)
|
579
664
|
Authentication::CredentialsEncoder.for_session(self)
|
580
665
|
end
|
666
|
+
|
667
|
+
# @api private
|
668
|
+
def wait_on_continuations
|
669
|
+
unless @threaded
|
670
|
+
event_loop.run_once until @continuations.length > 0
|
671
|
+
end
|
672
|
+
|
673
|
+
@continuations.pop
|
674
|
+
end
|
581
675
|
end # Session
|
582
676
|
|
583
677
|
# backwards compatibility
|