bunny 0.9.0.pre3 → 0.9.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|