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/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