bunny 0.9.0.pre10 → 0.9.0.pre11

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.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -1
  3. data/.travis.yml +4 -7
  4. data/ChangeLog.md +79 -0
  5. data/Gemfile +3 -1
  6. data/README.md +1 -1
  7. data/benchmarks/basic_publish/with_128K_messages.rb +35 -0
  8. data/benchmarks/basic_publish/with_1k_messages.rb +35 -0
  9. data/benchmarks/basic_publish/with_4K_messages.rb +35 -0
  10. data/benchmarks/basic_publish/with_64K_messages.rb +35 -0
  11. data/benchmarks/channel_open.rb +28 -0
  12. data/benchmarks/queue_declare.rb +29 -0
  13. data/benchmarks/queue_declare_and_bind.rb +29 -0
  14. data/benchmarks/queue_declare_bind_and_delete.rb +29 -0
  15. data/benchmarks/write_vs_write_nonblock.rb +49 -0
  16. data/bunny.gemspec +3 -3
  17. data/lib/bunny.rb +2 -0
  18. data/lib/bunny/channel.rb +31 -35
  19. data/lib/bunny/concurrent/continuation_queue.rb +10 -0
  20. data/lib/bunny/concurrent/linked_continuation_queue.rb +4 -2
  21. data/lib/bunny/exceptions.rb +5 -2
  22. data/lib/bunny/heartbeat_sender.rb +6 -4
  23. data/lib/bunny/queue.rb +3 -0
  24. data/lib/bunny/{main_loop.rb → reader_loop.rb} +5 -8
  25. data/lib/bunny/session.rb +93 -48
  26. data/lib/bunny/socket.rb +37 -3
  27. data/lib/bunny/test_kit.rb +26 -0
  28. data/lib/bunny/transport.rb +39 -33
  29. data/lib/bunny/version.rb +1 -1
  30. data/profiling/basic_publish/with_4K_messages.rb +33 -0
  31. data/spec/higher_level_api/integration/consistent_hash_exchange_spec.rb +10 -11
  32. data/spec/higher_level_api/integration/queue_declare_spec.rb +55 -13
  33. data/spec/issues/issue100_spec.rb +29 -27
  34. data/spec/issues/issue78_spec.rb +54 -52
  35. data/spec/stress/channel_open_stress_with_single_threaded_connection_spec.rb +0 -22
  36. data/spec/stress/concurrent_consumers_stress_spec.rb +2 -1
  37. data/spec/stress/concurrent_publishers_stress_spec.rb +7 -10
  38. data/spec/stress/long_running_consumer_spec.rb +83 -0
  39. data/spec/unit/concurrent/condition_spec.rb +7 -5
  40. data/spec/unit/concurrent/linked_continuation_queue_spec.rb +35 -0
  41. metadata +48 -44
@@ -8,8 +8,8 @@ Gem::Specification.new do |s|
8
8
  s.name = "bunny"
9
9
  s.version = Bunny::VERSION.dup
10
10
  s.homepage = "http://github.com/ruby-amqp/bunny"
11
- s.summary = "Easy to use synchronous Ruby client for RabbitMQ"
12
- s.description = "Easy to use synchronous Ruby client for RabbitMQ"
11
+ s.summary = "Popular easy to use Ruby client for RabbitMQ"
12
+ s.description = "Popular easy to use Ruby client for RabbitMQ"
13
13
  s.license = "MIT"
14
14
 
15
15
  # Sorted alphabetically.
@@ -29,7 +29,7 @@ Gem::Specification.new do |s|
29
29
  map { |mail| Base64.decode64(mail) }
30
30
 
31
31
  # Dependencies
32
- s.add_dependency "amq-protocol", ">= 1.4.0"
32
+ s.add_dependency "amq-protocol", ">= 1.6.0"
33
33
 
34
34
  # Files.
35
35
  s.has_rdoc = true
@@ -17,6 +17,8 @@ rescue LoadError => e
17
17
  # no-op
18
18
  end
19
19
 
20
+ require "logger"
21
+
20
22
  # Core entities: connection, channel, exchange, queue, consumer
21
23
  require "bunny/session"
22
24
  require "bunny/channel"
@@ -159,6 +159,7 @@ module Bunny
159
159
  # @param [Bunny::ConsumerWorkPool] work_pool Thread pool for delivery processing, by default of size 1
160
160
  def initialize(connection = nil, id = nil, work_pool = ConsumerWorkPool.new(1))
161
161
  @connection = connection
162
+ @logger = connection.logger
162
163
  @id = id || @connection.next_channel_id
163
164
  @status = :opening
164
165
 
@@ -214,6 +215,7 @@ module Bunny
214
215
  def close
215
216
  @connection.close_channel(self)
216
217
  closed!
218
+ maybe_kill_consumer_work_pool!
217
219
  end
218
220
 
219
221
  # @return [Boolean] true if this channel is open, false otherwise
@@ -1281,7 +1283,7 @@ module Bunny
1281
1283
  raise_if_no_longer_open!
1282
1284
 
1283
1285
  if @next_publish_seq_no == 0
1284
- @confirms_continuations = ::Queue.new
1286
+ @confirms_continuations = new_continuation
1285
1287
  @unconfirmed_set = Set.new
1286
1288
  @nacked_set = Set.new
1287
1289
  @next_publish_seq_no = 1
@@ -1358,7 +1360,7 @@ module Bunny
1358
1360
  #
1359
1361
  # @api plugin
1360
1362
  def recover_from_network_failure
1361
- # puts "Recovering channel #{@id}"
1363
+ @logger.debug "Recovering channel #{@id} after network failure"
1362
1364
  release_all_continuations
1363
1365
 
1364
1366
  recover_prefetch_setting
@@ -1392,7 +1394,7 @@ module Bunny
1392
1394
  # @api plugin
1393
1395
  def recover_queues
1394
1396
  @queues.values.dup.each do |q|
1395
- # puts "Recovering queue #{q.name}"
1397
+ @logger.debug "Recovering queue #{q.name}"
1396
1398
  q.recover_from_network_failure
1397
1399
  end
1398
1400
  end
@@ -1436,7 +1438,7 @@ module Bunny
1436
1438
 
1437
1439
  # @private
1438
1440
  def handle_method(method)
1439
- # puts "Channel#handle_frame on channel #{@id}: #{method.inspect}"
1441
+ @logger.debug "Channel#handle_frame on channel #{@id}: #{method.inspect}"
1440
1442
  case method
1441
1443
  when AMQ::Protocol::Queue::DeclareOk then
1442
1444
  @continuations.push(method)
@@ -1525,8 +1527,7 @@ module Bunny
1525
1527
  consumer.call(DeliveryInfo.new(basic_deliver), MessageProperties.new(properties), content)
1526
1528
  end
1527
1529
  else
1528
- # TODO: log it
1529
- puts "[warning] No consumer for tag #{basic_deliver.consumer_tag}"
1530
+ @logger.warn "No consumer for tag #{basic_deliver.consumer_tag} on channel #{@id}!"
1530
1531
  end
1531
1532
  end
1532
1533
 
@@ -1537,7 +1538,7 @@ module Bunny
1537
1538
  if x
1538
1539
  x.handle_return(ReturnInfo.new(basic_return), MessageProperties.new(properties), content)
1539
1540
  else
1540
- # TODO: log a warning
1541
+ @logger.warn "Exchange #{basic_return.exchange} is not in channel #{@id}'s cache! Dropping returned message!"
1541
1542
  end
1542
1543
  end
1543
1544
 
@@ -1574,12 +1575,13 @@ module Bunny
1574
1575
  t = Thread.current
1575
1576
  @threads_waiting_on_continuations << t
1576
1577
 
1577
- v = @continuations.pop
1578
- @threads_waiting_on_continuations.delete(t)
1579
-
1580
- v
1578
+ begin
1579
+ @continuations.poll(@connection.continuation_timeout)
1580
+ ensure
1581
+ @threads_waiting_on_continuations.delete(t)
1582
+ end
1581
1583
  else
1582
- connection.event_loop.run_once until @continuations.length > 0
1584
+ connection.reader_loop.run_once until @continuations.length > 0
1583
1585
 
1584
1586
  @continuations.pop
1585
1587
  end
@@ -1591,10 +1593,11 @@ module Bunny
1591
1593
  t = Thread.current
1592
1594
  @threads_waiting_on_basic_get_continuations << t
1593
1595
 
1594
- v = @basic_get_continuations.pop
1595
- @threads_waiting_on_basic_get_continuations.delete(t)
1596
-
1597
- v
1596
+ begin
1597
+ @basic_get_continuations.poll(@connection.continuation_timeout)
1598
+ ensure
1599
+ @threads_waiting_on_basic_get_continuations.delete(t)
1600
+ end
1598
1601
  else
1599
1602
  connection.event_loop.run_once until @basic_get_continuations.length > 0
1600
1603
 
@@ -1608,10 +1611,11 @@ module Bunny
1608
1611
  t = Thread.current
1609
1612
  @threads_waiting_on_confirms_continuations << t
1610
1613
 
1611
- v = @confirms_continuations.pop
1612
- @threads_waiting_on_confirms_continuations.delete(t)
1613
-
1614
- v
1614
+ begin
1615
+ @confirms_continuations.poll(@connection.continuation_timeout)
1616
+ ensure
1617
+ @threads_waiting_on_confirms_continuations.delete(t)
1618
+ end
1615
1619
  else
1616
1620
  connection.event_loop.run_once until @confirms_continuations.length > 0
1617
1621
 
@@ -1622,25 +1626,17 @@ module Bunny
1622
1626
  # Releases all continuations. Used by automatic network recovery.
1623
1627
  # @private
1624
1628
  def release_all_continuations
1625
- if @confirms_continuations.num_waiting > 0
1626
- @threads_waiting_on_confirms_continuations.each do |t|
1627
- t.run
1628
- end
1629
+ @threads_waiting_on_confirms_continuations.each do |t|
1630
+ t.run
1629
1631
  end
1630
- if @continuations.num_waiting > 0
1631
- @threads_waiting_on_continuations.each do |t|
1632
- t.run
1633
- end
1632
+ @threads_waiting_on_continuations.each do |t|
1633
+ t.run
1634
1634
  end
1635
- if @basic_get_continuations.num_waiting > 0
1636
- @threads_waiting_on_basic_get_continuations.each do |t|
1637
- t.run
1638
- end
1635
+ @threads_waiting_on_basic_get_continuations.each do |t|
1636
+ t.run
1639
1637
  end
1640
1638
 
1641
- @continuations = ::Queue.new
1642
- @confirms_continuations = ::Queue.new
1643
- @basic_get_continuations = ::Queue.new
1639
+ self.reset_continuations
1644
1640
  end
1645
1641
 
1646
1642
  # Starts consumer work pool. Lazily called by #basic_consume to avoid creating new threads
@@ -16,6 +16,16 @@ module Bunny
16
16
  @q.pop
17
17
  end
18
18
 
19
+ def poll(timeout_in_ms = nil)
20
+ if timeout_in_ms
21
+ Bunny::Timer.timeout(timeout_in_ms / 1000, Timeout::Error) do
22
+ @q.pop
23
+ end
24
+ else
25
+ @q.pop
26
+ end
27
+ end
28
+
19
29
  def clear
20
30
  @q.clear
21
31
  end
@@ -25,7 +25,7 @@ module Bunny
25
25
 
26
26
  def push(el, timeout_in_ms = nil)
27
27
  if timeout_in_ms
28
- @q.offer(el, timeout_in_ms, TimeUnit.MILLISECONDS)
28
+ @q.offer(el, timeout_in_ms, TimeUnit::MILLISECONDS)
29
29
  else
30
30
  @q.offer(el)
31
31
  end
@@ -38,7 +38,9 @@ module Bunny
38
38
 
39
39
  def poll(timeout_in_ms = nil)
40
40
  if timeout_in_ms
41
- @q.poll(timeout_in_ms, TimeUnit.MILLISECONDS)
41
+ v = @q.poll(timeout_in_ms, TimeUnit::MILLISECONDS)
42
+ raise ::Timeout::Error.new("operation did not finish in #{timeout_in_ms} ms") if v.nil?
43
+ v
42
44
  else
43
45
  @q.poll
44
46
  end
@@ -98,7 +98,7 @@ module Bunny
98
98
  # Raised by adapters when frame does not end with {final octet AMQ::Protocol::Frame::FINAL_OCTET}.
99
99
  # This suggest that there is a bug in adapter or AMQ broker implementation.
100
100
  #
101
- # @see http://files.travis-ci.org/docs/amqp/0.9.1/AMQP091Specification.pdf AMQP 0.9.1 specification (Section 2.3)
101
+ # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.3)
102
102
  class NoFinalOctetError < InconsistentDataError
103
103
  def initialize
104
104
  super("Frame doesn't end with #{AMQ::Protocol::Frame::FINAL_OCTET} as it must, which means the size is miscalculated.")
@@ -109,7 +109,7 @@ module Bunny
109
109
  # to the size specified in that frame's header.
110
110
  # This suggest that there is a bug in adapter or AMQ broker implementation.
111
111
  #
112
- # @see http://files.travis-ci.org/docs/amqp/0.9.1/AMQP091Specification.pdf AMQP 0.9.1 specification (Section 2.3)
112
+ # @see https://www.rabbitmq.com/resources/specs/amqp0-9-1.pdf AMQP 0.9.1 specification (Section 2.3)
113
113
  class BadLengthError < InconsistentDataError
114
114
  def initialize(expected_length, actual_length)
115
115
  super("Frame payload should be #{expected_length} long, but it's #{actual_length} long.")
@@ -146,6 +146,9 @@ module Bunny
146
146
  class InvalidCommand < ConnectionLevelException
147
147
  end
148
148
 
149
+ class FrameError < ConnectionLevelException
150
+ end
151
+
149
152
  class UnexpectedFrame < ConnectionLevelException
150
153
  end
151
154
 
@@ -9,8 +9,9 @@ module Bunny
9
9
  # API
10
10
  #
11
11
 
12
- def initialize(transport)
12
+ def initialize(transport, logger)
13
13
  @transport = transport
14
+ @logger = logger
14
15
  @mutex = Mutex.new
15
16
 
16
17
  @last_activity_time = Time.now
@@ -46,10 +47,10 @@ module Bunny
46
47
  sleep @interval
47
48
  end
48
49
  rescue IOError => ioe
49
- puts ioe.message
50
+ @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
50
51
  stop
51
52
  rescue Exception => e
52
- puts e.message
53
+ @logger.error "I/O error in the hearbeat sender: #{ioe.message}"
53
54
  stop
54
55
  end
55
56
  end
@@ -58,7 +59,8 @@ module Bunny
58
59
  now = Time.now
59
60
 
60
61
  if now > (@last_activity_time + @interval)
61
- @transport.send_raw(AMQ::Protocol::HeartbeatFrame.encode)
62
+ @logger.debug "Sending a heartbeat, last activity time: #{@last_activity_time}, interval (s): #{@interval}"
63
+ @transport.write_without_timeout(AMQ::Protocol::HeartbeatFrame.encode)
62
64
  end
63
65
  end
64
66
  end
@@ -326,12 +326,14 @@ module Bunny
326
326
  @channel.deregister_queue_named(old_name)
327
327
  end
328
328
 
329
+ # TODO: inject and use logger
329
330
  # puts "Recovering queue #{@name}"
330
331
  begin
331
332
  declare!
332
333
 
333
334
  @channel.register_queue(self)
334
335
  rescue Exception => e
336
+ # TODO: inject and use logger
335
337
  puts "Caught #{e.inspect} while redeclaring and registering #{@name}!"
336
338
  end
337
339
  recover_bindings
@@ -340,6 +342,7 @@ module Bunny
340
342
  # @private
341
343
  def recover_bindings
342
344
  @bindings.each do |b|
345
+ # TODO: inject and use logger
343
346
  # puts "Recovering binding #{b.inspect}"
344
347
  self.bind(b[:exchange], b)
345
348
  end
@@ -6,12 +6,13 @@ module Bunny
6
6
  # This loop uses a separate thread internally.
7
7
  #
8
8
  # This mimics the way RabbitMQ Java is designed quite closely.
9
- class MainLoop
9
+ class ReaderLoop
10
10
 
11
11
  def initialize(transport, session, session_thread)
12
12
  @transport = transport
13
13
  @session = session
14
14
  @session_thread = session_thread
15
+ @logger = @session.logger
15
16
  end
16
17
 
17
18
 
@@ -33,7 +34,6 @@ module Bunny
33
34
  rescue Errno::EBADF => ebadf
34
35
  # ignored, happens when we loop after the transport has already been closed
35
36
  rescue AMQ::Protocol::EmptyResponseError, IOError, SystemCallError => e
36
- puts "Exception in the main loop:"
37
37
  log_exception(e)
38
38
 
39
39
  @network_is_down = true
@@ -44,7 +44,6 @@ module Bunny
44
44
  @session_thread.raise(Bunny::NetworkFailure.new("detected a network failure: #{e.message}", e))
45
45
  end
46
46
  rescue Exception => e
47
- puts "Unepxected exception in the main loop:"
48
47
  log_exception(e)
49
48
 
50
49
  @network_is_down = true
@@ -55,8 +54,6 @@ module Bunny
55
54
 
56
55
  def run_once
57
56
  frame = @transport.read_next_frame
58
- @session.signal_activity!
59
-
60
57
  return if frame.is_a?(AMQ::Protocol::HeartbeatFrame)
61
58
 
62
59
  if !frame.final? || frame.method_class.has_content?
@@ -88,10 +85,10 @@ module Bunny
88
85
  end
89
86
 
90
87
  def log_exception(e)
91
- puts e.class.name
92
- puts e.message
88
+ @logger.error "Exception in the reader loop: #{e.class.name}: #{e.message}"
89
+ @logger.error "Backtrace: "
93
90
  e.backtrace.each do |line|
94
- puts line
91
+ @logger.error "\t#{line}"
95
92
  end
96
93
  end
97
94
  end
@@ -4,7 +4,7 @@ require "thread"
4
4
  require "bunny/transport"
5
5
  require "bunny/channel_id_allocator"
6
6
  require "bunny/heartbeat_sender"
7
- require "bunny/main_loop"
7
+ require "bunny/reader_loop"
8
8
  require "bunny/authentication/credentials_encoder"
9
9
  require "bunny/authentication/plain_mechanism_encoder"
10
10
  require "bunny/authentication/external_mechanism_encoder"
@@ -70,6 +70,10 @@ module Bunny
70
70
  # Authentication mechanism, e.g. "PLAIN" or "EXTERNAL"
71
71
  # @return [String]
72
72
  attr_reader :mechanism
73
+ # @return [Logger]
74
+ attr_reader :logger
75
+ # @return [Integer] Timeout for blocking protocol operations (queue.declare, queue.bind, etc), in milliseconds. Default is 4000.
76
+ attr_reader :continuation_timeout
73
77
 
74
78
 
75
79
  # @param [String, Hash] connection_string_or_opts Connection string or a hash of connection options
@@ -103,10 +107,11 @@ module Bunny
103
107
  @user = self.username_from(opts)
104
108
  @pass = self.password_from(opts)
105
109
  @vhost = self.vhost_from(opts)
106
- @logfile = opts[:logfile]
107
- @logging = opts[:logging] || false
110
+ @logfile = opts[:log_file] || opts[:logfile] || STDOUT
108
111
  @threaded = opts.fetch(:threaded, true)
109
112
 
113
+ self.init_logger(opts[:log_level] || ENV["BUNNY_LOG_LEVEL"] || Logger::WARN)
114
+
110
115
  # should automatic recovery from network failures be used?
111
116
  @automatically_recover = if opts[:automatically_recover].nil? && opts[:automatic_recovery].nil?
112
117
  true
@@ -114,6 +119,8 @@ module Bunny
114
119
  opts[:automatically_recover] || opts[:automatic_recovery]
115
120
  end
116
121
  @network_recovery_interval = opts.fetch(:network_recovery_interval, DEFAULT_NETWORK_RECOVERY_INTERVAL)
122
+ # in ms
123
+ @continuation_timeout = opts.fetch(:continuation_timeout, 4000)
117
124
 
118
125
  @status = :not_connected
119
126
 
@@ -128,7 +135,6 @@ module Bunny
128
135
  @locale = @opts.fetch(:locale, DEFAULT_LOCALE)
129
136
  # mutex for the channel id => channel hash
130
137
  @channel_mutex = Mutex.new
131
- @network_mutex = Mutex.new
132
138
  @channels = Hash.new
133
139
 
134
140
  self.reset_continuations
@@ -180,8 +186,8 @@ module Bunny
180
186
  self.init_connection
181
187
  self.open_connection
182
188
 
183
- @event_loop = nil
184
- self.start_main_loop if @threaded
189
+ @reader_loop = nil
190
+ self.start_reader_loop if @threaded
185
191
 
186
192
  @default_channel = self.create_channel
187
193
  rescue Exception => e
@@ -201,11 +207,11 @@ module Bunny
201
207
  # opened (this operation is very fast and inexpensive).
202
208
  #
203
209
  # @return [Bunny::Channel] Newly opened channel
204
- def create_channel(n = nil)
210
+ def create_channel(n = nil, consumer_pool_size = 1)
205
211
  if n && (ch = @channels[n])
206
212
  ch
207
213
  else
208
- ch = Bunny::Channel.new(self, n)
214
+ ch = Bunny::Channel.new(self, n, ConsumerWorkPool.new(consumer_pool_size || 1))
209
215
  ch.open
210
216
  ch
211
217
  end
@@ -341,7 +347,7 @@ module Bunny
341
347
 
342
348
  # @private
343
349
  def handle_frame(ch_number, method)
344
- # puts "Session#handle_frame on #{ch_number}: #{method.inspect}"
350
+ @logger.debug "Session#handle_frame on #{ch_number}: #{method.inspect}"
345
351
  case method
346
352
  when AMQ::Protocol::Channel::OpenOk then
347
353
  @continuations.push(method)
@@ -357,14 +363,14 @@ module Bunny
357
363
  begin
358
364
  @continuations.clear
359
365
 
360
- event_loop.stop
361
- @event_loop = nil
366
+ reader_loop.stop
367
+ @reader_loop = nil
362
368
 
363
369
  @transport.close
364
370
  rescue StandardError => e
365
- puts e.class.name
366
- puts e.message
367
- puts e.backtrace
371
+ @logger.error e.class.name
372
+ @logger.error e.message
373
+ @logger.error e.backtrace
368
374
  ensure
369
375
  @continuations.push(:__unblock__)
370
376
  end
@@ -381,7 +387,7 @@ module Bunny
381
387
  if ch = @channels[ch_number]
382
388
  ch.handle_method(method)
383
389
  else
384
- # TODO: log a warning
390
+ @logger.warn "Channel #{ch_number} is not open on this connection!"
385
391
  end
386
392
  end
387
393
  end
@@ -415,7 +421,7 @@ module Bunny
415
421
  if !recovering_from_network_failure?
416
422
  @recovering_from_network_failure = true
417
423
  if recoverable_network_failure?(exception)
418
- # puts "Recovering from a network failure..."
424
+ @logger.warn "Recovering from a network failure..."
419
425
  @channels.each do |n, ch|
420
426
  ch.maybe_kill_consumer_work_pool!
421
427
  end
@@ -443,7 +449,7 @@ module Bunny
443
449
  def recover_from_network_failure
444
450
  begin
445
451
  sleep @network_recovery_interval
446
- # puts "About to start recovery..."
452
+ @logger.debug "About to start connection recovery..."
447
453
  start
448
454
 
449
455
  if open?
@@ -452,7 +458,7 @@ module Bunny
452
458
  recover_channels
453
459
  end
454
460
  rescue TCPConnectionFailed, AMQ::Protocol::EmptyResponseError => e
455
- # puts "TCP connection failed, reconnecting in 5 seconds"
461
+ @logger.warn "TCP connection failed, reconnecting in 5 seconds"
456
462
  sleep @network_recovery_interval
457
463
  retry if recoverable_network_failure?(e)
458
464
  end
@@ -470,11 +476,6 @@ module Bunny
470
476
  end
471
477
  end
472
478
 
473
- # @private
474
- def send_raw(data)
475
- @transport.write(data)
476
- end
477
-
478
479
  # @private
479
480
  def instantiate_connection_level_exception(frame)
480
481
  case frame
@@ -482,6 +483,8 @@ module Bunny
482
483
  klass = case frame.reply_code
483
484
  when 320 then
484
485
  ConnectionForced
486
+ when 501 then
487
+ FrameError
485
488
  when 503 then
486
489
  InvalidCommand
487
490
  when 504 then
@@ -560,18 +563,18 @@ module Bunny
560
563
  end
561
564
 
562
565
  # @private
563
- def start_main_loop
564
- event_loop.start
566
+ def start_reader_loop
567
+ reader_loop.start
565
568
  end
566
569
 
567
570
  # @private
568
- def event_loop
569
- @event_loop ||= MainLoop.new(@transport, self, Thread.current)
571
+ def reader_loop
572
+ @reader_loop ||= ReaderLoop.new(@transport, self, Thread.current)
570
573
  end
571
574
 
572
575
  # @private
573
- def maybe_shutdown_main_loop
574
- @event_loop.stop if @event_loop
576
+ def maybe_shutdown_reader_loop
577
+ @reader_loop.stop if @reader_loop
575
578
  end
576
579
 
577
580
  # @private
@@ -585,11 +588,12 @@ module Bunny
585
588
  #
586
589
  # @raise [ConnectionClosedError]
587
590
  # @private
588
- def send_frame(frame)
591
+ def send_frame(frame, signal_activity = true)
589
592
  if closed?
590
593
  raise ConnectionClosedError.new(frame)
591
594
  else
592
- @network_mutex.synchronize { @transport.write(frame.encode) }
595
+ @transport.write(frame.encode)
596
+ signal_activity! if signal_activity
593
597
  end
594
598
  end
595
599
 
@@ -599,11 +603,12 @@ module Bunny
599
603
  #
600
604
  # @raise [ConnectionClosedError]
601
605
  # @private
602
- def send_frame_without_timeout(frame)
606
+ def send_frame_without_timeout(frame, signal_activity = true)
603
607
  if closed?
604
608
  raise ConnectionClosedError.new(frame)
605
609
  else
606
- @network_mutex.synchronize { @transport.write_without_timeout(frame.encode) }
610
+ @transport.write_without_timeout(frame.encode)
611
+ signal_activity! if signal_activity
607
612
  end
608
613
  end
609
614
 
@@ -618,8 +623,8 @@ module Bunny
618
623
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
619
624
  # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
620
625
  channel.synchronize do
621
- frames.each { |frame| self.send_frame(frame) }
622
- @transport.flush
626
+ frames.each { |frame| self.send_frame(frame, false) }
627
+ signal_activity!
623
628
  end
624
629
  end # send_frameset(frames)
625
630
 
@@ -635,10 +640,17 @@ module Bunny
635
640
  # If we synchronize on the channel, however, this is both thread safe and pretty fine-grained
636
641
  # locking. Note that "single frame" methods do not need this kind of synchronization. MK.
637
642
  channel.synchronize do
638
- frames.each { |frame| self.send_frame_without_timeout(frame) }
643
+ frames.each { |frame| self.send_frame_without_timeout(frame, false) }
644
+ signal_activity!
639
645
  end
640
646
  end # send_frameset_without_timeout(frames)
641
647
 
648
+ # @return [String]
649
+ # @api public
650
+ def to_s
651
+ "Bunny::Session #{@user}@#{@host}:#{@port}, vhost=#{@vhost}"
652
+ end
653
+
642
654
  protected
643
655
 
644
656
  # @api private
@@ -659,6 +671,7 @@ module Bunny
659
671
  # @api private
660
672
  def open_connection
661
673
  @transport.send_frame(AMQ::Protocol::Connection::StartOk.encode(@client_properties, @mechanism, self.encode_credentials(username, password), @locale))
674
+ @logger.debug "Sent connection.start-ok"
662
675
 
663
676
  frame = begin
664
677
  @transport.read_next_frame
@@ -669,6 +682,7 @@ module Bunny
669
682
  end
670
683
  if frame.nil?
671
684
  @state = :closed
685
+ @logger.error "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
672
686
  raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
673
687
  end
674
688
 
@@ -682,11 +696,15 @@ module Bunny
682
696
  else
683
697
  negotiate_value(@client_heartbeat, connection_tune.heartbeat)
684
698
  end
699
+ @logger.debug "Heartbeat interval negotiation: client = #{@client_heartbeat}, server = #{connection_tune.heartbeat}, result = #{@heartbeat}"
700
+ @logger.info "Heartbeat interval used (in seconds): #{@heartbeat}"
685
701
 
686
702
  @channel_id_allocator = ChannelIdAllocator.new(@channel_max)
687
703
 
688
704
  @transport.send_frame(AMQ::Protocol::Connection::TuneOk.encode(@channel_max, @frame_max, @heartbeat))
705
+ @logger.debug "Sent connection.tune-ok with heartbeat interval = #{@heartbeat}, frame_max = #{@frame_max}, channel_max = #{@channel_max}"
689
706
  @transport.send_frame(AMQ::Protocol::Connection::Open.encode(self.vhost))
707
+ @logger.debug "Sent connection.open with vhost = #{self.vhost}"
690
708
 
691
709
  frame2 = begin
692
710
  @transport.read_next_frame
@@ -697,6 +715,7 @@ module Bunny
697
715
  end
698
716
  if frame2.nil?
699
717
  @state = :closed
718
+ @logger.warn "RabbitMQ closed TCP connection before AMQP 0.9.1 connection was finalized. Most likely this means authentication failure."
700
719
  raise Bunny::PossibleAuthenticationFailureError.new(self.user, self.vhost, self.password.size)
701
720
  end
702
721
  connection_open_ok = frame2.decode_payload
@@ -726,8 +745,8 @@ module Bunny
726
745
 
727
746
  # @api private
728
747
  def initialize_heartbeat_sender
729
- # puts "Initializing heartbeat sender..."
730
- @heartbeat_sender = HeartbeatSender.new(@transport)
748
+ @logger.debug "Initializing heartbeat sender..."
749
+ @heartbeat_sender = HeartbeatSender.new(@transport, @logger)
731
750
  @heartbeat_sender.start(@heartbeat)
732
751
  end
733
752
 
@@ -749,7 +768,8 @@ module Bunny
749
768
  # Sends AMQ protocol header (also known as preamble).
750
769
  # @api private
751
770
  def send_preamble
752
- @transport.send_raw(AMQ::Protocol::PREAMBLE)
771
+ @transport.write(AMQ::Protocol::PREAMBLE)
772
+ @logger.debug "Sent protocol preamble"
753
773
  end
754
774
 
755
775
 
@@ -763,22 +783,47 @@ module Bunny
763
783
  Authentication::CredentialsEncoder.for_session(self)
764
784
  end
765
785
 
766
- # @api private
767
- def reset_continuations
768
- @continuations = if defined?(JRUBY_VERSION)
769
- Concurrent::LinkedContinuationQueue.new
770
- else
771
- Concurrent::ContinuationQueue.new
772
- end
786
+ if defined?(JRUBY_VERSION)
787
+ # @api private
788
+ def reset_continuations
789
+ @continuations = Concurrent::LinkedContinuationQueue.new
790
+ end
791
+ else
792
+ # @api private
793
+ def reset_continuations
794
+ @continuations = Concurrent::ContinuationQueue.new
795
+ end
773
796
  end
774
797
 
775
798
  # @api private
776
799
  def wait_on_continuations
777
800
  unless @threaded
778
- event_loop.run_once until @continuations.length > 0
801
+ reader_loop.run_once until @continuations.length > 0
779
802
  end
780
803
 
781
- @continuations.pop
804
+ @continuations.poll(@continuation_timeout)
805
+ end
806
+
807
+ # @api private
808
+ def init_logger(level)
809
+ @logger = ::Logger.new(@logfile)
810
+ @logger.level = normalize_log_level(level)
811
+ @logger.progname = self.to_s
812
+
813
+ @logger
814
+ end
815
+
816
+ # @api private
817
+ def normalize_log_level(level)
818
+ case level
819
+ when :debug, Logger::DEBUG, "debug" then Logger::DEBUG
820
+ when :info, Logger::INFO, "info" then Logger::INFO
821
+ when :warn, Logger::WARN, "warn" then Logger::WARN
822
+ when :error, Logger::ERROR, "error" then Logger::ERROR
823
+ when :fatal, Logger::FATAL, "fatal" then Logger::FATAL
824
+ else
825
+ Logger::WARN
826
+ end
782
827
  end
783
828
  end # Session
784
829