bunny 1.2.1 → 1.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: c8f1c8052f218b6d4ed95579a93e03ee9014fea5
4
- data.tar.gz: 739d9b25a80d0c6063124229fe8226fe45390ed3
3
+ metadata.gz: 0b9d1b9674fc6fe77346de7896e4ac8900c4c047
4
+ data.tar.gz: 19972fd3c10baf9a7acf1bc2c49a43ed995baeaa
5
5
  SHA512:
6
- metadata.gz: 0fe50e8b79c8611edf5f6b3f152b21680fdcb9790bf73126a2c3e4e2bbc38d485b26cef1edf0bde99eca2e8d34c784776396cd4cf8a26829b53ca7974de45430
7
- data.tar.gz: 2cb6bf110f3c9508eb055e37c2aebac6ff0915e6d89e014934738b9671041e6b34a9af13aca165d057d296a73efd6f2497efaaa4c0049726b9a362cf31c26e35
6
+ metadata.gz: ba09b21e9cbe72298decba6a551ecc554e17ff58c7f88a907979a4859453c7674341989588fd407d6923de40ebaa9e6af9cbbe31615e41d223a8c4163c5f89a0
7
+ data.tar.gz: df71d64b16826cfb887db8bcec84ecc114410cfaa92e9c97ca2d03cc003ffa7558998fcad6019031729c999888642ec82a69e2b00d10e3984b538dec61ebd800
@@ -1,3 +1,11 @@
1
+ ## Changes between Bunny 1.2.1 and 1.2.2
2
+
3
+ ### Synchronization Improvements for Session#close
4
+
5
+ `Bunny::Session#close` now better synchronizes state transitions,
6
+ eliminating a few race condition scenarios with I/O reader thread.
7
+
8
+
1
9
  ## Changes between Bunny 1.2.0 and 1.2.1
2
10
 
3
11
  ### Better Synchronization for Publisher Confirms
data/Gemfile CHANGED
@@ -35,7 +35,7 @@ end
35
35
 
36
36
  group :test do
37
37
  gem "rspec", ">= 2.13.0"
38
- gem "rabbitmq_http_api_client", "~> 1.0.0"
38
+ gem "rabbitmq_http_api_client", "~> 1.1.0"
39
39
  end
40
40
 
41
41
  gemspec
@@ -55,7 +55,7 @@ module Bunny
55
55
  # @return [Exchange] An instance that corresponds to the default exchange (of type direct).
56
56
  # @api public
57
57
  def self.default(channel_or_connection)
58
- self.new(channel_from(channel_or_connection), :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true)
58
+ self.new(channel_or_connection, :direct, AMQ::Protocol::EMPTY_STRING, :no_declare => true)
59
59
  end
60
60
 
61
61
  # @param [Bunny::Channel] channel_or_connection Channel this exchange will use. {Bunny::Session} instances are supported only for
@@ -14,6 +14,8 @@ module Bunny
14
14
  @session = session
15
15
  @session_thread = session_thread
16
16
  @logger = @session.logger
17
+
18
+ @mutex = Mutex.new
17
19
  end
18
20
 
19
21
 
@@ -29,34 +31,37 @@ module Bunny
29
31
  def run_loop
30
32
  loop do
31
33
  begin
32
- break if @stopping || @network_is_down
34
+ break if @mutex.synchronize { @stopping || @stopped || @network_is_down }
33
35
  run_once
34
- rescue Errno::EBADF => ebadf
35
- break if @stopping
36
- # ignored, happens when we loop after the transport has already been closed
37
36
  rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError => e
38
- break if @stopping
39
- log_exception(e)
37
+ break if terminate? || @session.closing? || @session.closed?
40
38
 
39
+ log_exception(e)
41
40
  @network_is_down = true
42
-
43
41
  if @session.automatically_recover?
44
42
  @session.handle_network_failure(e)
45
43
  else
46
44
  @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
47
45
  end
48
46
  rescue ShutdownSignal => _
47
+ @mutex.synchronize { @stopping = true }
49
48
  break
50
49
  rescue Exception => e
51
- break if @stopping
52
- log_exception(e)
50
+ break if terminate?
51
+ if !(@session.closing? || @session.closed?)
52
+ log_exception(e)
53
53
 
54
- @network_is_down = true
55
- @session_thread.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
54
+ @network_is_down = true
55
+ @session_thread.raise(Bunny::NetworkFailure.new("caught an unexpected exception in the network loop: #{e.message}", e))
56
+ end
57
+ rescue Errno::EBADF => ebadf
58
+ break if terminate?
59
+ # ignored, happens when we loop after the transport has already been closed
60
+ @mutex.synchronize { @stopping = true }
56
61
  end
57
62
  end
58
63
 
59
- @stopped = true
64
+ @mutex.synchronize { @stopped = true }
60
65
  end
61
66
 
62
67
  def run_once
@@ -83,11 +88,21 @@ module Bunny
83
88
  end
84
89
 
85
90
  def stop
86
- @stopping = true
91
+ @mutex.synchronize { @stopping = true }
87
92
  end
88
93
 
89
94
  def stopped?
90
- @stopped
95
+ @mutex.synchronize { @stopped = true }
96
+ end
97
+
98
+ def stopping?
99
+ @mutex.synchronize { @stopping = true }
100
+ end
101
+
102
+ def terminate_with(e)
103
+ @mutex.synchronize { @stopping = true }
104
+
105
+ self.raise(e)
91
106
  end
92
107
 
93
108
  def raise(e)
@@ -105,12 +120,26 @@ module Bunny
105
120
  end
106
121
  end
107
122
 
123
+ protected
124
+
108
125
  def log_exception(e)
109
- @logger.error "Exception in the reader loop: #{e.class.name}: #{e.message}"
110
- @logger.error "Backtrace: "
111
- e.backtrace.each do |line|
112
- @logger.error "\t#{line}"
126
+ if !(io_error?(e) && (@session.closing? || @session.closed?))
127
+ @logger.error "Exception in the reader loop: #{e.class.name}: #{e.message}"
128
+ @logger.error "Backtrace: "
129
+ e.backtrace.each do |line|
130
+ @logger.error "\t#{line}"
131
+ end
113
132
  end
114
133
  end
134
+
135
+ def io_error?(e)
136
+ [AMQ::Protocol::EmptyResponseError, IOError, SystemCallError].any? do |klazz|
137
+ e.is_a?(klazz)
138
+ end
139
+ end
140
+
141
+ def terminate?
142
+ @mutex.synchronize { @stopping || @stopped }
143
+ end
115
144
  end
116
145
  end
@@ -172,6 +172,7 @@ module Bunny
172
172
  # transport operations/continuations mutex. A workaround for
173
173
  # the non-reentrant Ruby mutexes. MK.
174
174
  @transport_mutex = @mutex_impl.new
175
+ @status_mutex = @mutex_impl.new
175
176
  @channels = Hash.new
176
177
 
177
178
  @origin_thread = Thread.current
@@ -233,7 +234,7 @@ module Bunny
233
234
  def start
234
235
  return self if connected?
235
236
 
236
- @status = :connecting
237
+ @status_mutex.synchronize { @status = :connecting }
237
238
  # reset here for cases when automatic network recovery kicks in
238
239
  # when we were blocked. MK.
239
240
  @blocked = false
@@ -259,7 +260,7 @@ module Bunny
259
260
 
260
261
  @default_channel = self.create_channel
261
262
  rescue Exception => e
262
- @status = :not_connected
263
+ @status_mutex.synchronize { @status = :not_connected }
263
264
  raise e
264
265
  end
265
266
 
@@ -295,16 +296,18 @@ module Bunny
295
296
 
296
297
  # Closes the connection. This involves closing all of its channels.
297
298
  def close
298
- if @transport.open?
299
- close_all_channels
299
+ @status_mutex.synchronize { @status = :closing }
300
+
301
+ ignoring_io_errors do
302
+ if @transport.open?
303
+ close_all_channels
300
304
 
301
- Bunny::Timeout.timeout(@transport.disconnect_timeout, ClientTimeout) do
302
305
  self.close_connection(true)
303
306
  end
304
- end
305
307
 
306
- clean_up_on_shutdown
307
- @status = :closed
308
+ clean_up_on_shutdown
309
+ end
310
+ @status_mutex.synchronize { @status = :closed }
308
311
  end
309
312
  alias stop close
310
313
 
@@ -328,14 +331,22 @@ module Bunny
328
331
  status == :connecting
329
332
  end
330
333
 
334
+ # @return [Boolean] true if this AMQP 0.9.1 connection is closing
335
+ # @api private
336
+ def closing?
337
+ @status_mutex.synchronize { @status == :closing }
338
+ end
339
+
331
340
  # @return [Boolean] true if this AMQP 0.9.1 connection is closed
332
341
  def closed?
333
- status == :closed
342
+ @status_mutex.synchronize { @status == :closed }
334
343
  end
335
344
 
336
345
  # @return [Boolean] true if this AMQP 0.9.1 connection is open
337
346
  def open?
338
- (status == :open || status == :connected || status == :connecting) && @transport.open?
347
+ @status_mutex.synchronize do
348
+ (status == :open || status == :connected || status == :connecting) && @transport.open?
349
+ end
339
350
  end
340
351
  alias connected? open?
341
352
 
@@ -506,7 +517,7 @@ module Bunny
506
517
 
507
518
  shut_down_all_consumer_work_pools!
508
519
  maybe_shutdown_heartbeat_sender
509
- @status = :not_connected
520
+ @status_mutex.synchronize { @status = :not_connected }
510
521
  end
511
522
 
512
523
  # Handles incoming frames and dispatches them.
@@ -597,7 +608,7 @@ module Bunny
597
608
  def handle_network_failure(exception)
598
609
  raise NetworkErrorWrapper.new(exception) unless @threaded
599
610
 
600
- @status = :disconnected
611
+ @status_mutex.synchronize { @status = :disconnected }
601
612
 
602
613
  if !recovering_from_network_failure?
603
614
  begin
@@ -696,7 +707,7 @@ module Bunny
696
707
  @continuations.push(method)
697
708
 
698
709
  clean_up_on_shutdown
699
- @origin_thread.raise(@last_connection_error)
710
+ @origin_thread.terminate_with(@last_connection_error)
700
711
  end
701
712
 
702
713
  def clean_up_on_shutdown
@@ -793,7 +804,7 @@ module Bunny
793
804
  if threaded?
794
805
  # this is the easiest way to wait until the loop
795
806
  # is guaranteed to have terminated
796
- @reader_loop.raise(ShutdownSignal)
807
+ @reader_loop.terminate_with(ShutdownSignal)
797
808
  # joining the thread here may take forever
798
809
  # on JRuby because sun.nio.ch.KQueueArrayWrapper#kevent0 is
799
810
  # a native method that cannot be (easily) interrupted.
@@ -928,7 +939,7 @@ module Bunny
928
939
  @server_authentication_mechanisms = (connection_start.mechanisms || "").split(" ")
929
940
  @server_locales = Array(connection_start.locales)
930
941
 
931
- @status = :connected
942
+ @status_mutex.synchronize { @status = :connected }
932
943
  end
933
944
 
934
945
  # @private
@@ -1004,7 +1015,7 @@ module Bunny
1004
1015
  end
1005
1016
  connection_open_ok = frame2.decode_payload
1006
1017
 
1007
- @status = :open
1018
+ @status_mutex.synchronize { @status = :open }
1008
1019
  if @heartbeat && @heartbeat > 0
1009
1020
  initialize_heartbeat_sender
1010
1021
  end
@@ -1023,7 +1034,7 @@ module Bunny
1023
1034
  close_transport
1024
1035
  end
1025
1036
 
1026
- @origin_thread.raise(e)
1037
+ @origin_thread.terminate_with(e)
1027
1038
  else
1028
1039
  raise "could not open connection: server did not respond with connection.open-ok but #{connection_open_ok.inspect} instead"
1029
1040
  end
@@ -1146,6 +1157,14 @@ module Bunny
1146
1157
  n
1147
1158
  end
1148
1159
  end
1160
+
1161
+ def ignoring_io_errors(&block)
1162
+ begin
1163
+ block.call
1164
+ rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError, Bunny::NetworkFailure => _
1165
+ # ignore
1166
+ end
1167
+ end
1149
1168
  end # Session
1150
1169
 
1151
1170
  # backwards compatibility
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Bunny
4
4
  # @return [String] Version of the library
5
- VERSION = "1.2.1"
5
+ VERSION = "1.2.2"
6
6
  end
@@ -29,7 +29,7 @@ unless ENV["CI"]
29
29
  q.purge
30
30
  x = ch.default_exchange
31
31
  x.publish("msg", :routing_key => q.name)
32
- sleep 0.2
32
+ sleep 0.5
33
33
  q.message_count.should == 1
34
34
  q.purge
35
35
  end
@@ -37,7 +37,7 @@ unless ENV["CI"]
37
37
  def ensure_queue_binding_recovery(x, q, routing_key = "")
38
38
  q.purge
39
39
  x.publish("msg", :routing_key => routing_key)
40
- sleep 0.2
40
+ sleep 0.5
41
41
  q.message_count.should == 1
42
42
  q.purge
43
43
  end
@@ -11,6 +11,15 @@ describe Bunny::Exchange do
11
11
  connection.close
12
12
  end
13
13
 
14
+ context "of default type" do
15
+ it "is declared with an empty name" do
16
+ ch = connection.create_channel
17
+
18
+ x = Bunny::Exchange.default(ch)
19
+
20
+ x.name.should == ''
21
+ end
22
+ end
14
23
 
15
24
  context "of type fanout" do
16
25
  context "with a non-predefined name" do
@@ -12,7 +12,7 @@ unless defined?(JRUBY_VERSION) && !ENV["FORCE_JRUBY_RUN"]
12
12
  end
13
13
 
14
14
  n.times do |i|
15
- it "can be closed (take #{i})" do
15
+ it "can be closed (automatic recovery disabled, take #{i})" do
16
16
  c = Bunny.new(:automatically_recover => false)
17
17
  c.start
18
18
  ch = c.create_channel
@@ -23,9 +23,39 @@ unless defined?(JRUBY_VERSION) && !ENV["FORCE_JRUBY_RUN"]
23
23
  end
24
24
  end
25
25
 
26
+ n.times do |i|
27
+ it "can be closed in the Hello, World example (take #{i})" do
28
+ c = Bunny.new(:automatically_recover => false)
29
+ c.start
30
+ ch = c.create_channel
31
+ x = ch.default_exchange
32
+ q = ch.queue("", :exclusive => true)
33
+ q.subscribe do |delivery_info, properties, payload|
34
+ # no-op
35
+ end
36
+ 20.times { x.publish("hello", :routing_key => q.name) }
37
+
38
+ c.should be_connected
39
+ c.stop
40
+ c.should be_closed
41
+ end
42
+ end
43
+
44
+ n.times do |i|
45
+ it "can be closed (automatic recovery enabled, take #{i})" do
46
+ c = Bunny.new(:automatically_recover => true)
47
+ c.start
48
+ ch = c.create_channel
49
+
50
+ c.should be_connected
51
+ c.stop
52
+ c.should be_closed
53
+ end
54
+ end
55
+
26
56
  context "in the single threaded mode" do
27
57
  n.times do |i|
28
- it "can be closed (take #{i})" do
58
+ it "can be closed (single threaded mode, take #{i})" do
29
59
  c = Bunny.new(:automatically_recover => false, :threaded => false)
30
60
  c.start
31
61
  ch = c.create_channel
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bunny
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Duncan
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2014-04-11 00:00:00.000000000 Z
15
+ date: 2014-05-26 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: amq-protocol