bunny 0.9.0.pre7 → 0.9.0.pre8

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.
@@ -1,3 +1,25 @@
1
+ ## Changes between Bunny 0.9.0.pre7 and 0.9.0.pre8
2
+
3
+ ### Automatic Connection Recovery Can Be Disabled
4
+
5
+ Automatic connection recovery now can be disabled by passing
6
+ the `:automatically_recover => false` option to `Bunny#initialize`).
7
+
8
+ When the recovery is disabled, network I/O-related exceptions will
9
+ cause an exception to be raised in thee thread the connection was
10
+ started on.
11
+
12
+
13
+ ### No Timeout Control For Publishing
14
+
15
+ `Bunny::Exchange#publish` and `Bunny::Channel#basic_publish` no
16
+ longer perform timeout control (using the timeout module) which
17
+ roughly increases throughput for flood publishing by 350%.
18
+
19
+ Apps that need delivery guarantees should use publisher confirms.
20
+
21
+
22
+
1
23
  ## Changes between Bunny 0.9.0.pre6 and 0.9.0.pre7
2
24
 
3
25
  ### Bunny::Channel#on_error
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  # encoding: utf-8
2
2
 
3
- source :rubygems
3
+ source "https://rubygems.org"
4
4
 
5
5
  # Use local clones if possible.
6
6
  # If you want to use your local copy, just symlink it to vendor.
@@ -26,7 +26,7 @@ gem "effin_utf8"
26
26
 
27
27
  group :development do
28
28
  gem "yard"
29
- gem "redcarpet"
29
+ gem "redcarpet", :platform => :mri
30
30
  end
31
31
 
32
32
  group :test do
data/README.md CHANGED
@@ -36,7 +36,7 @@ gem install bunny --pre
36
36
  To use Bunny 0.9.x in a project managed with Bundler:
37
37
 
38
38
  ``` ruby
39
- gem "bunny", ">= 0.9.0.pre6" # optionally: , :git => "git://github.com/ruby-amqp/bunny.git", :branch => "master"
39
+ gem "bunny", ">= 0.9.0.pre7" # optionally: , :git => "git://github.com/ruby-amqp/bunny.git", :branch => "master"
40
40
  ```
41
41
 
42
42
 
@@ -95,7 +95,7 @@ Other documentation guides are available at [rubybunny.info](http://rubybunny.in
95
95
 
96
96
  ### Mailing List
97
97
 
98
- [Bunny a mailing list](groups.google.com/group/ruby-amqp). We encourage you
98
+ [Bunny a mailing list](http://groups.google.com/group/ruby-amqp). We encourage you
99
99
  to also join the [rabbitmq-discuss](https://lists.rabbitmq.com/cgi-bin/mailman/listinfo/rabbitmq-discuss) mailing list. Feel free to ask any questions that you may have.
100
100
 
101
101
 
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: utf-8
3
+
4
+ require "bundler"
5
+ Bundler.setup
6
+
7
+ $:.unshift(File.expand_path("../../../lib", __FILE__))
8
+
9
+ require 'bunny'
10
+
11
+ conn = Bunny.new(:heartbeat_interval => 8, :automatically_recover => false)
12
+ conn.start
13
+
14
+ ch = conn.create_channel
15
+ x = ch.topic("bunny.examples.recovery.topic", :durable => false)
16
+ q = ch.queue("bunny.examples.recovery.client_named_queue2", :durable => true)
17
+ q.purge
18
+
19
+ q.bind(x, :routing_key => "abc").bind(x, :routing_key => "def")
20
+
21
+ loop do
22
+ sleep 1.5
23
+ body = rand.to_s
24
+ puts "Published #{body}"
25
+ x.publish(body, :routing_key => ["abc", "def"].sample)
26
+
27
+ sleep 1.5
28
+ _, _, payload = q.pop
29
+ if payload
30
+ puts "Consumed #{payload}"
31
+ else
32
+ puts "Consumed nothing"
33
+ end
34
+ end
@@ -9,6 +9,14 @@ require "bunny/framing"
9
9
  require "bunny/exceptions"
10
10
  require "bunny/socket"
11
11
 
12
+ begin
13
+ require "openssl"
14
+
15
+ require "bunny/ssl_socket"
16
+ rescue LoadError => e
17
+ # no-op
18
+ end
19
+
12
20
  # Core entities: connection, channel, exchange, queue, consumer
13
21
  require "bunny/session"
14
22
  require "bunny/channel"
@@ -342,6 +342,7 @@ module Bunny
342
342
  # @param [String] name Exchange name
343
343
  # @param [Hash] opts Exchange parameters
344
344
  #
345
+ # @option opts [String,Symbol] :type (:direct) Exchange type, e.g. :fanout or "x-consistent-hash"
345
346
  # @option opts [Boolean] :durable (false) Should the exchange be durable?
346
347
  # @option opts [Boolean] :auto_delete (false) Should the exchange be automatically deleted when no longer in use?
347
348
  # @option opts [Hash] :arguments ({}) Optional exchange arguments
@@ -509,14 +510,15 @@ module Bunny
509
510
  @next_publish_seq_no += 1
510
511
  end
511
512
 
512
- @connection.send_frameset(AMQ::Protocol::Basic::Publish.encode(@id,
513
- payload,
514
- meta,
515
- exchange_name,
516
- routing_key,
517
- meta[:mandatory],
518
- false,
519
- @connection.frame_max), self)
513
+ m = AMQ::Protocol::Basic::Publish.encode(@id,
514
+ payload,
515
+ meta,
516
+ exchange_name,
517
+ routing_key,
518
+ meta[:mandatory],
519
+ false,
520
+ @connection.frame_max)
521
+ @connection.send_frameset_without_timeout(m, self)
520
522
 
521
523
  self
522
524
  end
@@ -782,6 +784,12 @@ module Bunny
782
784
  queue
783
785
  end
784
786
 
787
+ # helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
788
+ # in the queue already. MK.
789
+ if consumer_tag && consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
790
+ add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
791
+ end
792
+
785
793
  @connection.send_frame(AMQ::Protocol::Basic::Consume.encode(@id,
786
794
  queue_name,
787
795
  consumer_tag,
@@ -790,11 +798,6 @@ module Bunny
790
798
  exclusive,
791
799
  false,
792
800
  arguments))
793
- # helps avoid race condition between basic.consume-ok and basic.deliver if there are messages
794
- # in the queue already. MK.
795
- if consumer_tag && consumer_tag.strip != AMQ::Protocol::EMPTY_STRING
796
- add_consumer(queue_name, consumer_tag, no_ack, exclusive, arguments, &block)
797
- end
798
801
 
799
802
  Bunny::Timer.timeout(read_write_timeout, ClientTimeout) do
800
803
  @last_basic_consume_ok = wait_on_continuations
@@ -10,9 +10,8 @@ module Bunny
10
10
 
11
11
  # @param [Integer] max_channel Max allowed channel id
12
12
  def initialize(max_channel = ((1 << 16) - 1))
13
- @int_allocator ||= AMQ::IntAllocator.new(1, max_channel)
14
-
15
- @channel_id_mutex ||= Mutex.new
13
+ @allocator = AMQ::IntAllocator.new(1, max_channel)
14
+ @mutex = Mutex.new
16
15
  end
17
16
 
18
17
 
@@ -23,8 +22,8 @@ module Bunny
23
22
  # @see ChannelManager#release_channel_id
24
23
  # @see ChannelManager#reset_channel_id_allocator
25
24
  def next_channel_id
26
- @channel_id_mutex.synchronize do
27
- @int_allocator.allocate
25
+ @mutex.synchronize do
26
+ @allocator.allocate
28
27
  end
29
28
  end
30
29
 
@@ -35,10 +34,10 @@ module Bunny
35
34
  # @see ChannelManager#next_channel_id
36
35
  # @see ChannelManager#reset_channel_id_allocator
37
36
  def release_channel_id(i)
38
- @channel_id_mutex.synchronize do
39
- @int_allocator.release(i)
37
+ @mutex.synchronize do
38
+ @allocator.release(i)
40
39
  end
41
- end # self.release_channel_id(i)
40
+ end
42
41
 
43
42
 
44
43
  # Returns true if given channel id has been previously allocated and not yet released.
@@ -50,8 +49,8 @@ module Bunny
50
49
  # @see ChannelManager#next_channel_id
51
50
  # @see ChannelManager#release_channel_id
52
51
  def allocated_channel_id?(i)
53
- @channel_id_mutex.synchronize do
54
- @int_allocator.allocated?(i)
52
+ @mutex.synchronize do
53
+ @allocator.allocated?(i)
55
54
  end
56
55
  end
57
56
 
@@ -60,9 +59,13 @@ module Bunny
60
59
  # @see Channel.next_channel_id
61
60
  # @see Channel.release_channel_id
62
61
  def reset_channel_id_allocator
63
- @channel_id_mutex.synchronize do
64
- @int_allocator.reset
62
+ @mutex.synchronize do
63
+ @allocator.reset
65
64
  end
66
- end # reset_channel_id_allocator
65
+ end
66
+
67
+ def synchronize(&block)
68
+ @mutex.synchronize(&block)
69
+ end
67
70
  end
68
71
  end
@@ -1,5 +1,40 @@
1
1
  module Bunny
2
- class TCPConnectionFailed < StandardError
2
+ class Exception < ::Exception
3
+ end
4
+
5
+ class NetworkFailure < Exception
6
+ attr_reader :cause
7
+
8
+ def initialize(message, cause)
9
+ super(message)
10
+ @cause = cause
11
+ end
12
+ end
13
+
14
+ class ChannelLevelException < Exception
15
+ attr_reader :channel, :channel_close
16
+
17
+ def initialize(message, ch, channel_close)
18
+ super(message)
19
+
20
+ @channel = ch
21
+ @channel_close = channel_close
22
+ end
23
+ end
24
+
25
+ class ConnectionLevelException < Exception
26
+ attr_reader :connection, :connection_close
27
+
28
+ def initialize(message, connection, connection_close)
29
+ super(message)
30
+
31
+ @connection = connection
32
+ @connection_close = connection_close
33
+ end
34
+ end
35
+
36
+
37
+ class TCPConnectionFailed < Exception
3
38
  attr_reader :hostname, :port
4
39
 
5
40
  def initialize(e, hostname, port)
@@ -13,7 +48,7 @@ module Bunny
13
48
  end
14
49
  end
15
50
 
16
- class ConnectionClosedError < StandardError
51
+ class ConnectionClosedError < Exception
17
52
  def initialize(frame)
18
53
  if frame.respond_to?(:method_class)
19
54
  super("Trying to send frame through a closed connection. Frame is #{frame.inspect}, method class is #{frame.method_class}")
@@ -23,7 +58,7 @@ module Bunny
23
58
  end
24
59
  end
25
60
 
26
- class PossibleAuthenticationFailureError < StandardError
61
+ class PossibleAuthenticationFailureError < Exception
27
62
 
28
63
  #
29
64
  # API
@@ -44,10 +79,10 @@ module Bunny
44
79
  ConnectionError = TCPConnectionFailed
45
80
  ServerDownError = TCPConnectionFailed
46
81
 
47
- class ForcedChannelCloseError < StandardError; end
48
- class ForcedConnectionCloseError < StandardError; end
49
- class MessageError < StandardError; end
50
- class ProtocolError < StandardError; end
82
+ class ForcedChannelCloseError < Exception; end
83
+ class ForcedConnectionCloseError < Exception; end
84
+ class MessageError < Exception; end
85
+ class ProtocolError < Exception; end
51
86
 
52
87
  # raised when read or write I/O operations time out (but only if
53
88
  # a connection is configured to use them)
@@ -57,7 +92,7 @@ module Bunny
57
92
 
58
93
 
59
94
  # Base exception class for data consistency and framing errors.
60
- class InconsistentDataError < StandardError
95
+ class InconsistentDataError < Exception
61
96
  end
62
97
 
63
98
  # Raised by adapters when frame does not end with {final octet AMQ::Protocol::Frame::FINAL_OCTET}.
@@ -82,7 +117,7 @@ module Bunny
82
117
  end
83
118
 
84
119
 
85
- class ChannelAlreadyClosed < StandardError
120
+ class ChannelAlreadyClosed < Exception
86
121
  attr_reader :channel
87
122
 
88
123
  def initialize(message, ch)
@@ -92,17 +127,6 @@ module Bunny
92
127
  end
93
128
  end
94
129
 
95
- class ChannelLevelException < StandardError
96
- attr_reader :channel, :channel_close
97
-
98
- def initialize(message, ch, channel_close)
99
- super(message)
100
-
101
- @channel = ch
102
- @channel_close = channel_close
103
- end
104
- end
105
-
106
130
  class PreconditionFailed < ChannelLevelException
107
131
  end
108
132
 
@@ -116,18 +140,6 @@ module Bunny
116
140
  end
117
141
 
118
142
 
119
-
120
- class ConnectionLevelException < StandardError
121
- attr_reader :connection, :connection_close
122
-
123
- def initialize(message, connection, connection_close)
124
- super(message)
125
-
126
- @connection = connection
127
- @connection_close = connection_close
128
- end
129
- end
130
-
131
143
  class ChannelError < ConnectionLevelException
132
144
  end
133
145
 
@@ -137,7 +149,7 @@ module Bunny
137
149
  class UnexpectedFrame < ConnectionLevelException
138
150
  end
139
151
 
140
- class NetworkErrorWrapper < StandardError
152
+ class NetworkErrorWrapper < Exception
141
153
  attr_reader :other
142
154
 
143
155
  def initialize(other)
@@ -8,9 +8,10 @@ module Bunny
8
8
  # This mimics the way RabbitMQ Java is designed quite closely.
9
9
  class MainLoop
10
10
 
11
- def initialize(transport, session)
12
- @transport = transport
13
- @session = session
11
+ def initialize(transport, session, session_thread)
12
+ @transport = transport
13
+ @session = session
14
+ @session_thread = session_thread
14
15
  end
15
16
 
16
17
 
@@ -29,19 +30,25 @@ module Bunny
29
30
  begin
30
31
  break if @stopping || @network_is_down
31
32
  run_once
32
- rescue Timeout::Error => te
33
- # given that the server may be pushing data to us, timeout detection/handling
34
- # should happen per operation and not in this loop
35
33
  rescue Errno::EBADF => ebadf
36
34
  # ignored, happens when we loop after the transport has already been closed
37
- rescue AMQ::Protocol::EmptyResponseError, IOError, Errno::EPIPE, Errno::EAGAIN => e
38
- puts "Exception in the main loop: #{e.class.name}"
35
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError => e
36
+ puts "Exception in the main loop:"
37
+ log_exception(e)
38
+
39
39
  @network_is_down = true
40
- @session.handle_network_failure(e)
40
+
41
+ if @session.automatically_recover?
42
+ @session.handle_network_failure(e)
43
+ else
44
+ @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
45
+ end
41
46
  rescue Exception => e
42
- puts e.class.name
43
- puts e.message
44
- puts e.backtrace
47
+ puts "Unepxected exception in the main loop:"
48
+ log_exception(e)
49
+
50
+ @network_is_down = true
51
+ @session_thread.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
45
52
  end
46
53
  end
47
54
  end
@@ -79,5 +86,13 @@ module Bunny
79
86
  @thread.kill
80
87
  @thread.join
81
88
  end
89
+
90
+ def log_exception(e)
91
+ puts e.class.name
92
+ puts e.message
93
+ e.backtrace.each do |line|
94
+ puts line
95
+ end
96
+ end
82
97
  end
83
98
  end
@@ -208,7 +208,7 @@ module Bunny
208
208
 
209
209
  # @param [Hash] opts Options
210
210
  #
211
- # @option opts [Boolean] block (false) Should the call block calling thread?
211
+ # @option opts [Boolean] :ack (false) Will the message be acknowledged manually?
212
212
  #
213
213
  # @return [Array] Triple of delivery info, message properties and message content.
214
214
  # If the queue is empty, all three will be nils.
@@ -101,6 +101,13 @@ module Bunny
101
101
  @logging = opts[:logging] || false
102
102
  @threaded = opts.fetch(:threaded, true)
103
103
 
104
+ # should automatic recovery from network failures be used?
105
+ @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
106
+ true
107
+ else
108
+ opts[:automatically_recover] || opts[:automatic_recovery]
109
+ end
110
+
104
111
  @status = :not_connected
105
112
 
106
113
  # these are negotiated with the broker during the connection tuning phase
@@ -112,7 +119,9 @@ module Bunny
112
119
  @mechanism = opts.fetch(:auth_mechanism, "PLAIN")
113
120
  @credentials_encoder = credentials_encoder_for(@mechanism)
114
121
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
122
+ # mutex for the channel id => channel hash
115
123
  @channel_mutex = Mutex.new
124
+ @network_mutex = Mutex.new
116
125
  @channels = Hash.new
117
126
 
118
127
  @continuations = ::Queue.new
@@ -210,6 +219,10 @@ module Bunny
210
219
  end
211
220
  alias connected? open?
212
221
 
222
+ def automatically_recover?
223
+ @automatically_recover
224
+ end
225
+
213
226
  #
214
227
  # Backwards compatibility
215
228
  #
@@ -254,7 +267,9 @@ module Bunny
254
267
  n = ch.number
255
268
  self.register_channel(ch)
256
269
 
257
- @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
270
+ @channel_mutex.synchronize do
271
+ @transport.send_frame(AMQ::Protocol::Channel::Open.encode(n, AMQ::Protocol::EMPTY_STRING))
272
+ end
258
273
  @last_channel_open_ok = wait_on_continuations
259
274
  raise_if_continuation_resulted_in_a_connection_error!
260
275
 
@@ -317,8 +332,7 @@ module Bunny
317
332
  puts e.message
318
333
  puts e.backtrace
319
334
  ensure
320
- @active_continuation.notify_all if @active_continuation
321
- @active_continuation = false
335
+ @continuations.push(nil)
322
336
  end
323
337
  when AMQ::Protocol::Channel::Close then
324
338
  begin
@@ -420,8 +434,8 @@ module Bunny
420
434
  end
421
435
 
422
436
  # @private
423
- def send_raw(*args)
424
- @transport.write(*args)
437
+ def send_raw(data)
438
+ @transport.write(data)
425
439
  end
426
440
 
427
441
  # @private
@@ -513,7 +527,7 @@ module Bunny
513
527
 
514
528
  # @private
515
529
  def event_loop
516
- @event_loop ||= MainLoop.new(@transport, self)
530
+ @event_loop ||= MainLoop.new(@transport, self, Thread.current)
517
531
  end
518
532
 
519
533
  # @private
@@ -531,7 +545,21 @@ module Bunny
531
545
  if closed?
532
546
  raise ConnectionClosedError.new(frame)
533
547
  else
534
- @transport.send_raw(frame.encode)
548
+ @network_mutex.synchronize { @transport.write(frame.encode) }
549
+ end
550
+ end
551
+
552
+ # Sends frame to the peer, checking that connection is open.
553
+ # Uses transport implementation that does not perform
554
+ # timeout control. Exposed primarily for Bunny::Channel.
555
+ #
556
+ # @raise [ConnectionClosedError]
557
+ # @private
558
+ def send_frame_without_timeout(frame)
559
+ if closed?
560
+ raise ConnectionClosedError.new(frame)
561
+ else
562
+ @network_mutex.synchronize { @transport.write_without_timeout(frame.encode) }
535
563
  end
536
564
  end
537
565
 
@@ -546,11 +574,27 @@ module Bunny
546
574
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
547
575
  # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
548
576
  channel.synchronize do
549
- frames.each { |frame| @transport.send_frame(frame) }
577
+ frames.each { |frame| self.send_frame(frame) }
550
578
  @transport.flush
551
579
  end
552
580
  end # send_frameset(frames)
553
581
 
582
+ # Sends multiple frames, one by one. For thread safety this method takes a channel
583
+ # object and synchronizes on it. Uses transport implementation that does not perform
584
+ # timeout control.
585
+ #
586
+ # @api private
587
+ def send_frameset_without_timeout(frames, channel)
588
+ # some developers end up sharing channels between threads and when multiple
589
+ # threads publish on the same channel aggressively, at some point frames will be
590
+ # delivered out of order and broker will raise 505 UNEXPECTED_FRAME exception.
591
+ # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
592
+ # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
593
+ channel.synchronize do
594
+ frames.each { |frame| self.send_frame_without_timeout(frame) }
595
+ end
596
+ end # send_frameset_without_timeout(frames)
597
+
554
598
  protected
555
599
 
556
600
  # @api private
@@ -576,7 +620,7 @@ module Bunny
576
620
  @transport.read_next_frame
577
621
  # frame timeout means the broker has closed the TCP connection, which it
578
622
  # does per 0.9.1 spec.
579
- rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError => e
623
+ rescue Errno::ECONNRESET, ClientTimeout, AMQ::Protocol::EmptyResponseError, EOFError, IOError => e
580
624
  nil
581
625
  end
582
626
  if frame.nil?
@@ -644,7 +688,7 @@ module Bunny
644
688
 
645
689
  # @api private
646
690
  def initialize_transport
647
- @transport = Transport.new(self, @host, @port, @opts)
691
+ @transport = Transport.new(self, @host, @port, @opts.merge(:session_thread => Thread.current))
648
692
  end
649
693
 
650
694
  # Sends AMQ protocol header (also known as preamble).