bunny 0.9.0.pre10 → 0.9.0.pre11

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