bunny 0.9.0.pre3 → 0.9.0.pre4
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/ChangeLog.md +56 -1
- data/README.md +96 -30
- data/bunny.gemspec +1 -1
- data/examples/connection/heartbeat.rb +3 -3
- data/examples/connection/unknown_host.rb +2 -2
- data/examples/guides/getting_started/hello_world.rb +1 -2
- data/examples/guides/queues/redeliveries.rb +79 -0
- data/lib/bunny/authentication/credentials_encoder.rb +37 -0
- data/lib/bunny/authentication/external_mechanism_encoder.rb +26 -0
- data/lib/bunny/authentication/plain_mechanism_encoder.rb +17 -0
- data/lib/bunny/channel.rb +44 -22
- data/lib/bunny/consumer.rb +5 -1
- data/lib/bunny/delivery_info.rb +3 -2
- data/lib/bunny/exceptions.rb +7 -1
- data/lib/bunny/exchange.rb +6 -3
- data/lib/bunny/heartbeat_sender.rb +7 -3
- data/lib/bunny/queue.rb +16 -3
- data/lib/bunny/session.rb +28 -14
- data/lib/bunny/transport.rb +8 -1
- data/lib/bunny/version.rb +1 -1
- data/spec/higher_level_api/integration/basic_cancel_spec.rb +43 -0
- data/spec/higher_level_api/integration/connection_spec.rb +2 -7
- data/spec/higher_level_api/integration/consumer_cancellation_notification_spec.rb +1 -1
- data/spec/higher_level_api/integration/exchange_declare_spec.rb +21 -6
- data/spec/higher_level_api/integration/exchange_delete_spec.rb +47 -2
- data/spec/higher_level_api/integration/message_properties_access_spec.rb +95 -0
- data/spec/higher_level_api/integration/publisher_confirms_spec.rb +4 -3
- data/spec/issues/issue78_spec.rb +73 -0
- data/spec/lower_level_api/integration/basic_cancel_spec.rb +6 -1
- metadata +177 -160
data/lib/bunny/channel.rb
CHANGED
@@ -138,7 +138,7 @@ module Bunny
|
|
138
138
|
basic_reject(delivery_tag, requeue)
|
139
139
|
end
|
140
140
|
|
141
|
-
def ack(delivery_tag, multiple)
|
141
|
+
def ack(delivery_tag, multiple = false)
|
142
142
|
basic_ack(delivery_tag, multiple)
|
143
143
|
end
|
144
144
|
alias acknowledge ack
|
@@ -269,17 +269,17 @@ module Bunny
|
|
269
269
|
exclusive,
|
270
270
|
false,
|
271
271
|
arguments))
|
272
|
-
|
273
|
-
|
272
|
+
# helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
|
273
|
+
# in the queue already. MK.
|
274
|
+
if consumer_tag && consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
|
275
|
+
add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
|
274
276
|
end
|
275
277
|
|
276
|
-
|
277
|
-
|
278
|
-
# server-generated
|
279
|
-
c = Consumer.new(self, queue, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments)
|
280
|
-
c.on_delivery(&block) if block
|
281
|
-
@consumers[@last_basic_consume_ok.consumer_tag] = c
|
278
|
+
Bunny::Timer.timeout(1, ClientTimeout) do
|
279
|
+
@last_basic_consume_ok = @continuations.pop
|
282
280
|
end
|
281
|
+
# covers server-generated consumer tags
|
282
|
+
add_consumer(queue_name, @last_basic_consume_ok.consumer_tag, no_ack, exclusive, arguments, &block)
|
283
283
|
|
284
284
|
@last_basic_consume_ok
|
285
285
|
end
|
@@ -296,15 +296,20 @@ module Bunny
|
|
296
296
|
consumer.exclusive,
|
297
297
|
false,
|
298
298
|
consumer.arguments))
|
299
|
+
|
300
|
+
# helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
|
301
|
+
# in the queue already. MK.
|
302
|
+
if consumer.consumer_tag && consumer.consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
|
303
|
+
register_consumer(consumer.consumer_tag, consumer)
|
304
|
+
end
|
305
|
+
|
299
306
|
Bunny::Timer.timeout(1, ClientTimeout) do
|
300
307
|
@last_basic_consume_ok = @continuations.pop
|
301
308
|
end
|
309
|
+
# covers server-generated consumer tags
|
310
|
+
register_consumer(@last_basic_consume_ok.consumer_tag, consumer)
|
302
311
|
|
303
|
-
|
304
|
-
# update the tag in case it was server-generated
|
305
|
-
consumer.consumer_tag = @last_basic_consume_ok.consumer_tag
|
306
|
-
@consumers[@last_basic_consume_ok.consumer_tag] = consumer
|
307
|
-
end
|
312
|
+
raise_if_continuation_resulted_in_a_channel_error!
|
308
313
|
|
309
314
|
@last_basic_consume_ok
|
310
315
|
end
|
@@ -592,6 +597,20 @@ module Bunny
|
|
592
597
|
# Implementation
|
593
598
|
#
|
594
599
|
|
600
|
+
def register_consumer(consumer_tag, consumer)
|
601
|
+
@consumer_mutex.synchronize do
|
602
|
+
@consumers[consumer_tag] = consumer
|
603
|
+
end
|
604
|
+
end
|
605
|
+
|
606
|
+
def add_consumer(queue, consumer_tag, no_ack, exclusive, arguments, &block)
|
607
|
+
@consumer_mutex.synchronize do
|
608
|
+
c = Consumer.new(self, queue, consumer_tag, no_ack, exclusive, arguments)
|
609
|
+
c.on_delivery(&block) if block
|
610
|
+
@consumers[consumer_tag] = c
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
595
614
|
def handle_method(method)
|
596
615
|
# puts "Channel#handle_frame on channel #{@id}: #{method.inspect}"
|
597
616
|
case method
|
@@ -670,6 +689,9 @@ module Bunny
|
|
670
689
|
@work_pool.submit do
|
671
690
|
consumer.call(DeliveryInfo.new(basic_deliver), MessageProperties.new(properties), content)
|
672
691
|
end
|
692
|
+
else
|
693
|
+
# TODO: log it
|
694
|
+
puts "[warning] No consumer for tag #{basic_deliver.consumer_tag}"
|
673
695
|
end
|
674
696
|
end
|
675
697
|
|
@@ -730,6 +752,14 @@ module Bunny
|
|
730
752
|
@exchanges[name]
|
731
753
|
end
|
732
754
|
|
755
|
+
# Unique string supposed to be used as a consumer tag.
|
756
|
+
#
|
757
|
+
# @return [String] Unique string.
|
758
|
+
# @api plugin
|
759
|
+
def generate_consumer_tag(name = "bunny")
|
760
|
+
"#{name}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
|
761
|
+
end
|
762
|
+
|
733
763
|
protected
|
734
764
|
|
735
765
|
def closed!
|
@@ -765,13 +795,5 @@ module Bunny
|
|
765
795
|
def raise_if_no_longer_open!
|
766
796
|
raise ChannelAlreadyClosed.new("cannot use a channel that was already closed! Channel id: #{@id}", self) if closed?
|
767
797
|
end
|
768
|
-
|
769
|
-
# Unique string supposed to be used as a consumer tag.
|
770
|
-
#
|
771
|
-
# @return [String] Unique string.
|
772
|
-
# @api plugin
|
773
|
-
def generate_consumer_tag(name = "bunny")
|
774
|
-
"#{name}-#{Time.now.to_i * 1000}-#{Kernel.rand(999_999_999_999)}"
|
775
|
-
end
|
776
798
|
end
|
777
799
|
end
|
data/lib/bunny/consumer.rb
CHANGED
@@ -14,7 +14,7 @@ module Bunny
|
|
14
14
|
|
15
15
|
|
16
16
|
|
17
|
-
def initialize(channel, queue, consumer_tag =
|
17
|
+
def initialize(channel, queue, consumer_tag = channel.generate_consumer_tag, no_ack = false, exclusive = false, arguments = {})
|
18
18
|
@channel = channel || raise(ArgumentError, "channel is nil")
|
19
19
|
@queue = queue || raise(ArgumentError, "queue is nil")
|
20
20
|
@consumer_tag = consumer_tag
|
@@ -51,6 +51,10 @@ module Bunny
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
+
def cancel
|
55
|
+
@channel.basic_cancel(@consumer_tag)
|
56
|
+
end
|
57
|
+
|
54
58
|
def inspect
|
55
59
|
"#<#{self.class.name}:#{object_id} @channel_id=#{@channel.number} @queue=#{self.queue_name}> @consumer_tag=#{@consumer_tag} @exclusive=#{@exclusive} @no_ack=#{@no_ack}>"
|
56
60
|
end
|
data/lib/bunny/delivery_info.rb
CHANGED
@@ -49,13 +49,14 @@ module Bunny
|
|
49
49
|
@basic_deliver.consumer_tag
|
50
50
|
end
|
51
51
|
|
52
|
-
def
|
53
|
-
@basic_deliver.
|
52
|
+
def delivery_tag
|
53
|
+
@basic_deliver.delivery_tag
|
54
54
|
end
|
55
55
|
|
56
56
|
def redelivered
|
57
57
|
@basic_deliver.redelivered
|
58
58
|
end
|
59
|
+
alias redelivered? redelivered
|
59
60
|
|
60
61
|
def exchange
|
61
62
|
@basic_deliver.exchange
|
data/lib/bunny/exceptions.rb
CHANGED
@@ -3,7 +3,13 @@ module Bunny
|
|
3
3
|
attr_reader :hostname, :port
|
4
4
|
|
5
5
|
def initialize(e, hostname, port)
|
6
|
-
|
6
|
+
m = case e
|
7
|
+
when String then
|
8
|
+
e
|
9
|
+
when Exception then
|
10
|
+
e.message
|
11
|
+
end
|
12
|
+
super("Could not estabilish TCP connection to #{hostname}:#{port}: #{m}")
|
7
13
|
end
|
8
14
|
end
|
9
15
|
|
data/lib/bunny/exchange.rb
CHANGED
@@ -66,7 +66,7 @@ module Bunny
|
|
66
66
|
@auto_delete = @options[:auto_delete]
|
67
67
|
@arguments = @options[:arguments]
|
68
68
|
|
69
|
-
declare! unless opts[:no_declare] || (@name =~ /^amq
|
69
|
+
declare! unless opts[:no_declare] || (@name =~ /^amq\.(direct|fanout|topic|match|headers)/) || (@name == AMQ::Protocol::EMPTY_STRING)
|
70
70
|
|
71
71
|
@channel.register_exchange(self)
|
72
72
|
end
|
@@ -87,6 +87,9 @@ module Bunny
|
|
87
87
|
@arguments
|
88
88
|
end
|
89
89
|
|
90
|
+
def predeclared?
|
91
|
+
@name == AMQ::Protocol::EMPTY_STRING || (@name =~ /^amq\.(direct|fanout|topic|match|headers)/)
|
92
|
+
end
|
90
93
|
|
91
94
|
|
92
95
|
def publish(payload, opts = {})
|
@@ -96,10 +99,10 @@ module Bunny
|
|
96
99
|
end
|
97
100
|
|
98
101
|
|
99
|
-
# Deletes the exchange
|
102
|
+
# Deletes the exchange unless it is a default exchange
|
100
103
|
# @api public
|
101
104
|
def delete(opts = {})
|
102
|
-
@channel.exchange_delete(@name, opts)
|
105
|
+
@channel.exchange_delete(@name, opts) unless predeclared?
|
103
106
|
end
|
104
107
|
|
105
108
|
|
@@ -18,7 +18,11 @@ module Bunny
|
|
18
18
|
|
19
19
|
def start(period = 30)
|
20
20
|
@mutex.synchronize do
|
21
|
-
|
21
|
+
# calculate interval as half the given period plus
|
22
|
+
# some compensation for Ruby's implementation inaccuracy
|
23
|
+
# (we cannot get at the nanos level the Java client uses, and
|
24
|
+
# our approach is simplistic). MK.
|
25
|
+
@interval = [(period / 2) - 1, 0.4].max
|
22
26
|
|
23
27
|
@thread = Thread.new(&method(:run))
|
24
28
|
end
|
@@ -39,7 +43,7 @@ module Bunny
|
|
39
43
|
loop do
|
40
44
|
self.beat
|
41
45
|
|
42
|
-
sleep
|
46
|
+
sleep @interval
|
43
47
|
end
|
44
48
|
rescue IOError => ioe
|
45
49
|
# ignored
|
@@ -51,7 +55,7 @@ module Bunny
|
|
51
55
|
def beat
|
52
56
|
now = Time.now
|
53
57
|
|
54
|
-
if now > (@last_activity_time + @
|
58
|
+
if now > (@last_activity_time + @interval)
|
55
59
|
@transport.send_raw(AMQ::Protocol::HeartbeatFrame.encode)
|
56
60
|
end
|
57
61
|
end
|
data/lib/bunny/queue.rb
CHANGED
@@ -75,18 +75,18 @@ module Bunny
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def subscribe(opts = {
|
78
|
-
:consumer_tag =>
|
78
|
+
:consumer_tag => @channel.generate_consumer_tag,
|
79
79
|
:ack => false,
|
80
80
|
:exclusive => false,
|
81
81
|
:block => false,
|
82
82
|
:on_cancellation => nil
|
83
83
|
}, &block)
|
84
84
|
|
85
|
-
ctag = opts.fetch(:consumer_tag,
|
85
|
+
ctag = opts.fetch(:consumer_tag, @channel.generate_consumer_tag)
|
86
86
|
consumer = Consumer.new(@channel,
|
87
87
|
@name,
|
88
88
|
ctag,
|
89
|
-
opts[:
|
89
|
+
!opts[:ack],
|
90
90
|
opts[:exclusive],
|
91
91
|
opts[:arguments])
|
92
92
|
consumer.on_delivery(&block)
|
@@ -98,12 +98,15 @@ module Bunny
|
|
98
98
|
# the current thread for as long as the consumer pool is active
|
99
99
|
@channel.work_pool.join
|
100
100
|
end
|
101
|
+
|
102
|
+
consumer
|
101
103
|
end
|
102
104
|
|
103
105
|
def subscribe_with(consumer, opts = {:block => false})
|
104
106
|
@channel.basic_consume_with(consumer)
|
105
107
|
|
106
108
|
@channel.work_pool.join if opts[:block]
|
109
|
+
consumer
|
107
110
|
end
|
108
111
|
|
109
112
|
def pop(opts = {:ack => false}, &block)
|
@@ -129,6 +132,16 @@ module Bunny
|
|
129
132
|
end
|
130
133
|
end
|
131
134
|
|
135
|
+
# Publishes a message to the queue via default exchange.
|
136
|
+
#
|
137
|
+
# @see Bunny::Exchange#publish
|
138
|
+
# @see Bunny::Channel#default_exchange
|
139
|
+
def publish(payload, opts = {})
|
140
|
+
@channel.default_exchange.publish(payload, opts.merge(:routing_key => @name))
|
141
|
+
|
142
|
+
self
|
143
|
+
end
|
144
|
+
|
132
145
|
|
133
146
|
# Deletes the queue
|
134
147
|
# @api public
|
data/lib/bunny/session.rb
CHANGED
@@ -5,6 +5,9 @@ require "bunny/transport"
|
|
5
5
|
require "bunny/channel_id_allocator"
|
6
6
|
require "bunny/heartbeat_sender"
|
7
7
|
require "bunny/main_loop"
|
8
|
+
require "bunny/authentication/credentials_encoder"
|
9
|
+
require "bunny/authentication/plain_mechanism_encoder"
|
10
|
+
require "bunny/authentication/external_mechanism_encoder"
|
8
11
|
|
9
12
|
require "bunny/concurrent/condition"
|
10
13
|
|
@@ -18,9 +21,8 @@ module Bunny
|
|
18
21
|
DEFAULT_VHOST = "/"
|
19
22
|
DEFAULT_USER = "guest"
|
20
23
|
DEFAULT_PASSWORD = "guest"
|
21
|
-
#
|
22
|
-
|
23
|
-
DEFAULT_HEARTBEAT = 0
|
24
|
+
# the same value as RabbitMQ 3.0 uses. MK.
|
25
|
+
DEFAULT_HEARTBEAT = 600
|
24
26
|
# 128K
|
25
27
|
DEFAULT_FRAME_MAX = 131072
|
26
28
|
|
@@ -29,9 +31,8 @@ module Bunny
|
|
29
31
|
|
30
32
|
|
31
33
|
DEFAULT_CLIENT_PROPERTIES = {
|
32
|
-
# once we support AMQP 0.9.1 extensions, this needs to be updated. MK.
|
33
34
|
:capabilities => {
|
34
|
-
|
35
|
+
:publisher_confirms => true,
|
35
36
|
:consumer_cancel_notify => true,
|
36
37
|
:exchange_exchange_bindings => true,
|
37
38
|
:"basic.nack" => true
|
@@ -53,6 +54,10 @@ module Bunny
|
|
53
54
|
attr_reader :server_capabilities, :server_properties, :server_authentication_mechanisms, :server_locales
|
54
55
|
attr_reader :default_channel
|
55
56
|
attr_reader :channel_id_allocator
|
57
|
+
# Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
|
58
|
+
# @return [String]
|
59
|
+
attr_reader :mechanism
|
60
|
+
|
56
61
|
|
57
62
|
def initialize(connection_string_or_opts = Hash.new, optz = Hash.new)
|
58
63
|
opts = case (ENV["RABBITMQ_URL"] || connection_string_or_opts)
|
@@ -80,13 +85,14 @@ module Bunny
|
|
80
85
|
@client_channel_max = opts.fetch(:channel_max, 65536)
|
81
86
|
@client_heartbeat = self.heartbeat_from(opts)
|
82
87
|
|
83
|
-
@client_properties
|
84
|
-
@mechanism
|
85
|
-
@
|
86
|
-
@
|
87
|
-
@
|
88
|
+
@client_properties = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
|
89
|
+
@mechanism = opts.fetch(:auth_mechanism, "PLAIN")
|
90
|
+
@credentials_encoder = credentials_encoder_for(@mechanism)
|
91
|
+
@locale = @opts.fetch(:locale, DEFAULT_LOCALE)
|
92
|
+
@channel_mutex = Mutex.new
|
93
|
+
@channels = Hash.new
|
88
94
|
|
89
|
-
@continuations
|
95
|
+
@continuations = ::Queue.new
|
90
96
|
end
|
91
97
|
|
92
98
|
def hostname; self.host; end
|
@@ -445,7 +451,12 @@ module Bunny
|
|
445
451
|
|
446
452
|
@frame_max = negotiate_value(@client_frame_max, connection_tune.frame_max)
|
447
453
|
@channel_max = negotiate_value(@client_channel_max, connection_tune.channel_max)
|
448
|
-
|
454
|
+
# this allows for disabled heartbeats. MK.
|
455
|
+
@heartbeat = if 0 == @client_heartbeat || @client_heartbeat.nil?
|
456
|
+
0
|
457
|
+
else
|
458
|
+
negotiate_value(@client_heartbeat, connection_tune.heartbeat)
|
459
|
+
end
|
449
460
|
|
450
461
|
@channel_id_allocator = ChannelIdAllocator.new(@channel_max)
|
451
462
|
|
@@ -500,10 +511,13 @@ module Bunny
|
|
500
511
|
|
501
512
|
|
502
513
|
# @api plugin
|
503
|
-
# @see http://tools.ietf.org/rfc/rfc2595.txt RFC 2595
|
504
514
|
def encode_credentials(username, password)
|
505
|
-
|
515
|
+
@credentials_encoder.encode_credentials(username, password)
|
506
516
|
end # encode_credentials(username, password)
|
517
|
+
|
518
|
+
def credentials_encoder_for(mechanism)
|
519
|
+
Authentication::CredentialsEncoder.for_session(self)
|
520
|
+
end
|
507
521
|
end # Session
|
508
522
|
|
509
523
|
# backwards compatibility
|
data/lib/bunny/transport.rb
CHANGED
@@ -71,7 +71,14 @@ module Bunny
|
|
71
71
|
end
|
72
72
|
rescue Errno::EPIPE, Errno::EAGAIN, Bunny::ClientTimeout, IOError => e
|
73
73
|
close
|
74
|
-
|
74
|
+
|
75
|
+
m = case e
|
76
|
+
when String then
|
77
|
+
e
|
78
|
+
when Exception then
|
79
|
+
e.message
|
80
|
+
end
|
81
|
+
raise Bunny::ConnectionError.new(m, @host, @port)
|
75
82
|
end
|
76
83
|
end
|
77
84
|
alias send_raw write
|
data/lib/bunny/version.rb
CHANGED
@@ -0,0 +1,43 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Bunny::Consumer, "#cancel" do
|
4
|
+
let(:connection) do
|
5
|
+
c = Bunny.new(:user => "bunny_gem", :password => "bunny_password", :vhost => "bunny_testbed")
|
6
|
+
c.start
|
7
|
+
c
|
8
|
+
end
|
9
|
+
|
10
|
+
after :all do
|
11
|
+
connection.close if connection.open?
|
12
|
+
end
|
13
|
+
|
14
|
+
let(:queue_name) { "bunny.queues.#{rand}" }
|
15
|
+
|
16
|
+
context "when the given consumer tag is valid" do
|
17
|
+
it "cancels the consumer" do
|
18
|
+
delivered_data = []
|
19
|
+
|
20
|
+
t = Thread.new do
|
21
|
+
ch = connection.create_channel
|
22
|
+
q = ch.queue(queue_name, :auto_delete => true, :durable => false)
|
23
|
+
consumer = q.subscribe(:block => false) do |_, _, payload|
|
24
|
+
delivered_data << payload
|
25
|
+
end
|
26
|
+
|
27
|
+
consumer.consumer_tag.should_not be_nil
|
28
|
+
cancel_ok = consumer.cancel
|
29
|
+
cancel_ok.consumer_tag.should == consumer.consumer_tag
|
30
|
+
|
31
|
+
ch.close
|
32
|
+
end
|
33
|
+
t.abort_on_exception = true
|
34
|
+
sleep 0.5
|
35
|
+
|
36
|
+
ch = connection.create_channel
|
37
|
+
ch.default_exchange.publish("", :routing_key => queue_name)
|
38
|
+
|
39
|
+
sleep 0.7
|
40
|
+
delivered_data.should be_empty
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|