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/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
- Bunny::Timer.timeout(1, ClientTimeout) do
273
- @last_basic_consume_ok = @continuations.pop
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
- @consumer_mutex.synchronize do
277
- # make sure to use consumer tag from basic.consume-ok in case it was
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
- @consumer_mutex.synchronize do
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
@@ -14,7 +14,7 @@ module Bunny
14
14
 
15
15
 
16
16
 
17
- def initialize(channel, queue, consumer_tag = "", no_ack = false, exclusive = false, arguments = {})
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
@@ -49,13 +49,14 @@ module Bunny
49
49
  @basic_deliver.consumer_tag
50
50
  end
51
51
 
52
- def deliver_tag
53
- @basic_deliver.deliver_tag
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
@@ -3,7 +3,13 @@ module Bunny
3
3
  attr_reader :hostname, :port
4
4
 
5
5
  def initialize(e, hostname, port)
6
- super("Could not estabilish TCP connection to #{hostname}:#{port}: #{e.message}")
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
 
@@ -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\..+/) || (@name == AMQ::Protocol::EMPTY_STRING)
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
- @period = period
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 (@period / 2)
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 + @period)
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[:no_ack],
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
- # 0 means "no heartbeat". This is the same default RabbitMQ Java client and amqp gem
22
- # use.
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
- # :publisher_confirms => true,
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 = opts[:properties] || DEFAULT_CLIENT_PROPERTIES
84
- @mechanism = "PLAIN"
85
- @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
86
- @channel_mutex = Mutex.new
87
- @channels = Hash.new
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 = ::Queue.new
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
- @heartbeat = negotiate_value(@client_heartbeat, connection_tune.heartbeat)
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
- "\0#{username}\0#{password}"
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
@@ -71,7 +71,14 @@ module Bunny
71
71
  end
72
72
  rescue Errno::EPIPE, Errno::EAGAIN, Bunny::ClientTimeout, IOError => e
73
73
  close
74
- raise Bunny::ConnectionError, e.message
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
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module Bunny
4
- VERSION = "0.9.0.pre3"
4
+ VERSION = "0.9.0.pre4"
5
5
  end
@@ -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