bunny 0.9.0.pre7 → 0.9.0.pre8

Sign up to get free protection for your applications and to get access to all the features.
@@ -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).