amqp-client 2.0.1 → 2.2.0

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
  SHA256:
3
- metadata.gz: 0d50a249ba8677243b1dbfd865f798b9e24439e725301e31e70c2add330a4380
4
- data.tar.gz: 552df8c243b7cf62ae7e36f873bba3779950b4e2c541f7ef19932a10db3f014b
3
+ metadata.gz: b52ef0851be1716d1833d84448e2470b19938b03050a14d8b5a40a1289cb41ae
4
+ data.tar.gz: 6e33352b3e238b7ccf1c203a4779da7b5f01f659ab27c39bf7a78299398025aa
5
5
  SHA512:
6
- metadata.gz: 0c8a84f3f5b821f8aefa3e60e22c2210a51a512551e7cc25c964abe3e5cdb373d3b40fd0ef64e2cd81e9f3a066747284cb8839b9d646cddc8e979e7d3921305f
7
- data.tar.gz: 8afefadae34068634ba6d74f6828b8848237c4239a3f9bbcbaea8032f6a7a94261c245d6e40891b2a04ddb66be523614ec575a08c8ca4b8293cd43ba296b6d1d
6
+ metadata.gz: e78020ff0cf64c9a4a13fb64625f5f0382950c5981fb2df130f4e61066930ba0c9298bd2d7bd3982b1ddbb1ef492cab89183d7e4a1c815a9fa10204897b0ff27
7
+ data.tar.gz: 2ab30a767b5fc5aa3f5a43949f7cde1b601e74e4bdb0ebc5b0d91306deee773aac9007bcc340610ae52fcba732983b804306cb6fdcbcfdab9e064ec17292ff30
@@ -78,6 +78,8 @@ module AMQP
78
78
  # @return [nil]
79
79
  # @api private
80
80
  def closed!(level, code, reason, classid, methodid)
81
+ return if @closed
82
+
81
83
  @closed = [level, code, reason, classid, methodid]
82
84
  @replies.close
83
85
  @basic_gets.close
@@ -348,8 +350,10 @@ module AMQP
348
350
  consume_loop(msg_q, consumer_tag, &blk)
349
351
  nil
350
352
  else
351
- threads = Array.new(worker_threads) do
352
- Thread.new { consume_loop(msg_q, consumer_tag, &blk) }
353
+ threads = Array.new(worker_threads) do |i|
354
+ t = Thread.new { consume_loop(msg_q, consumer_tag, &blk) }
355
+ t.name = @connection.thread_name(role: "consumer", detail: "ch=#{@id} tag=#{consumer_tag} ##{i + 1}")
356
+ t
353
357
  end
354
358
  @consumers[consumer_tag] =
355
359
  ConsumeOk.new(channel_id: @id, consumer_tag:, worker_threads: threads, msg_q:, on_cancel:)
@@ -467,8 +471,12 @@ module AMQP
467
471
  def wait_for_confirms
468
472
  @unconfirmed_lock.synchronize do
469
473
  until @unconfirmed.empty?
470
- @unconfirmed_empty.wait(@unconfirmed_lock)
474
+ # Check before waiting: if the channel was closed (and the
475
+ # @unconfirmed_empty broadcast from #closed! fired) before we got
476
+ # here, the wakeup is already gone and #wait would block forever.
471
477
  raise Error::Closed.new(@id, *@closed) if @closed
478
+
479
+ @unconfirmed_empty.wait(@unconfirmed_lock)
472
480
  end
473
481
  result = !@nacked
474
482
  @nacked = false # Reset for next round of publishes
@@ -481,15 +489,22 @@ module AMQP
481
489
  def confirm(args)
482
490
  ack_or_nack, delivery_tag, multiple = *args
483
491
  @unconfirmed_lock.synchronize do
484
- case multiple
485
- when true
486
- idx = @unconfirmed.index(delivery_tag) || raise("Delivery tag not found")
487
- @unconfirmed.shift(idx + 1)
488
- when false
489
- @unconfirmed.delete(delivery_tag) || raise("Delivery tag not found")
492
+ # A tag we're not tracking (a duplicate, out-of-order, or broker
493
+ # quirk) is logged and ignored, not raised: #confirm runs on the
494
+ # read_loop thread, where an exception would tear down the connection.
495
+ confirmed =
496
+ if multiple
497
+ idx = @unconfirmed.index(delivery_tag)
498
+ @unconfirmed.shift(idx + 1) if idx
499
+ else
500
+ @unconfirmed.delete(delivery_tag)
501
+ end
502
+ if confirmed
503
+ @nacked = true if ack_or_nack == :nack
504
+ @unconfirmed_empty.broadcast if @unconfirmed.empty?
505
+ else
506
+ warn "AMQP-Client received #{ack_or_nack} for unknown delivery tag #{delivery_tag} on channel #{@id}"
490
507
  end
491
- @nacked = true if ack_or_nack == :nack
492
- @unconfirmed_empty.broadcast if @unconfirmed.empty?
493
508
  end
494
509
  end
495
510
 
@@ -589,7 +604,8 @@ module AMQP
589
604
  next_msg = @next_msg
590
605
  if next_msg.is_a? ReturnMessage
591
606
  if @on_return
592
- Thread.new { @on_return.call(next_msg) }
607
+ t = Thread.new { @on_return.call(next_msg) }
608
+ t.name = @connection.thread_name(role: "on_return", detail: "ch=#{@id}")
593
609
  else
594
610
  warn "AMQP-Client message returned: #{next_msg.inspect}"
595
611
  end
@@ -622,6 +638,11 @@ module AMQP
622
638
  while (msg = queue.pop)
623
639
  begin
624
640
  yield msg
641
+ rescue Error::ConnectionClosed, Error::ChannelClosed
642
+ # The connection or channel closed while the message was being processed (e.g. an
643
+ # ack/reject/publish from the consumer raced a shutdown). The worker can't make
644
+ # progress and there's no bug to surface, so stop quietly instead of crashing it.
645
+ return
625
646
  rescue StandardError # cancel the consumer if an uncaught exception is raised
626
647
  begin
627
648
  close("Unexpected exception in consumer #{tag} thread", 500)
@@ -17,6 +17,9 @@ module AMQP
17
17
  # otherwise the user have to run it explicitly, without {#read_loop} the connection won't function
18
18
  # @param codec_registry [MessageCodecRegistry] Registry for message codecs
19
19
  # @param strict_coding [Boolean] Whether to raise errors on unsupported codecs
20
+ # @param name [String, nil] Instance identifier embedded in thread names
21
+ # (e.g. "amqp.read_loop[name] host:port") and lifecycle log prefixes.
22
+ # Usually sourced from the URL's `?name=` query param by {Client}.
20
23
  # @option options [Boolean] connection_name (PROGRAM_NAME) Set a name for the connection to be able to identify
21
24
  # the client from the broker
22
25
  # @option options [Boolean] verify_peer (true) Verify broker's TLS certificate, set to false for self-signed certs
@@ -28,7 +31,7 @@ module AMQP
28
31
  # Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
29
32
  # @option options [String] keepalive (60:10:3) TCP keepalive setting, 60s idle, 10s interval between probes, 3 probes
30
33
  # @return [Connection]
31
- def initialize(uri = "", read_loop_thread: true, codec_registry: nil, strict_coding: false, **options)
34
+ def initialize(uri = "", read_loop_thread: true, codec_registry: nil, strict_coding: false, name: nil, **options)
32
35
  uri = URI.parse(uri)
33
36
  tls = uri.scheme == "amqps"
34
37
  port = port_from_env || uri.port || (tls ? 5671 : 5672)
@@ -38,6 +41,10 @@ module AMQP
38
41
  vhost = URI.decode_www_form_component(uri.path[1..] || "/")
39
42
  options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
40
43
 
44
+ @host = host
45
+ @port = port
46
+ @name = name
47
+
41
48
  socket = open_socket(host, port, tls, options)
42
49
  channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, options)
43
50
 
@@ -59,7 +66,20 @@ module AMQP
59
66
  # Only used with heartbeats
60
67
  @last_activity_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
68
 
62
- Thread.new { read_loop } if read_loop_thread
69
+ return unless read_loop_thread
70
+
71
+ t = Thread.new { read_loop }
72
+ t.name = thread_name(role: "read_loop")
73
+ end
74
+
75
+ # Build a thread name for a role attached to this connection.
76
+ # Format: "amqp.<role>[<name>] <host>:<port>[ <detail>]" — the `[<name>]`
77
+ # segment is only present when the connection has a `name:`.
78
+ # @api private
79
+ def thread_name(role:, detail: nil)
80
+ suffix = @name ? "[#{@name}]" : ""
81
+ base = "amqp.#{role}#{suffix} #{@host}:#{@port}"
82
+ detail ? "#{base} #{detail}" : base
63
83
  end
64
84
 
65
85
  # Indicates that the server is blocking publishes.
@@ -223,7 +243,7 @@ module AMQP
223
243
 
224
244
  # make sure that the frame end is correct
225
245
  frame_end = socket.readchar.ord
226
- raise Error::UnexpectedFrameTypeEnd, frame_end if frame_end != 206
246
+ raise Error::UnexpectedFrameEnd, frame_end if frame_end != 206
227
247
 
228
248
  # parse the frame, will return false if a close frame was received
229
249
  parse_frame(type, channel_id, frame_buffer) || return
@@ -236,6 +256,10 @@ module AMQP
236
256
  ensure
237
257
  @closed ||= [400, "unknown"]
238
258
  @replies.close
259
+ # Wake channels still blocked in #expect / #wait_for_confirms: an abrupt
260
+ # socket close means no channel/connection close frame ever reached them.
261
+ code, reason = @closed.first(2)
262
+ @channels_lock.synchronize { @channels.values }.each { |ch| ch.closed!(:connection, code, reason, 0, 0) }
239
263
  begin
240
264
  if @write_lock.owned? # if connection is blocked
241
265
  @socket.close
@@ -451,7 +475,7 @@ module AMQP
451
475
 
452
476
  # Start the heartbeat background thread (called from connection#tune)
453
477
  def start_heartbeats(period)
454
- Thread.new do
478
+ t = Thread.new do
455
479
  Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
456
480
  interval = period / 2.0
457
481
  loop do
@@ -471,6 +495,7 @@ module AMQP
471
495
  end
472
496
  end
473
497
  end
498
+ t.name = thread_name(role: "heartbeat")
474
499
  end
475
500
 
476
501
  def send_heartbeat
@@ -526,13 +551,16 @@ module AMQP
526
551
  loop do # rubocop:disable Metrics/BlockLength
527
552
  begin
528
553
  socket.readpartial(4096, buf)
554
+ # The 7-byte frame header may arrive split across reads (seen on JRuby); buffer it
555
+ # fully before unpacking, else frame_size is nil and frame_end below fails on nil.
556
+ buf << socket.readpartial(4096) while buf.bytesize < 7
529
557
  rescue *READ_EXCEPTIONS => e
530
558
  raise Error, "Could not establish AMQP connection: #{e.message}"
531
559
  end
532
560
 
533
561
  type, channel_id, frame_size = buf.unpack("C S> L>")
534
562
  frame_end = buf.getbyte(frame_size + 7)
535
- raise Error::UnexpectedFrameTypeEnd, frame_end if frame_end != 206
563
+ raise Error::UnexpectedFrameEnd, frame_end if frame_end != 206
536
564
 
537
565
  case type
538
566
  when 1 # method frame
@@ -39,8 +39,9 @@ module AMQP
39
39
 
40
40
  # Update the consumer with new metadata after reconnection
41
41
  # @api private
42
- def update_consume_ok(consume_ok)
42
+ def update_consume_ok(consume_ok, channel_id)
43
43
  @consume_ok = consume_ok
44
+ @channel_id = channel_id
44
45
  end
45
46
  end
46
47
  end
@@ -38,6 +38,40 @@ module AMQP
38
38
 
39
39
  def decode(data, _properties) = Zlib.inflate(data)
40
40
  end.new
41
+
42
+ # Raw DEFLATE coder (RFC 1951 -- no zlib header, no Adler-32 checksum).
43
+ # Not registered by default. Use to interoperate with producers that emit
44
+ # raw DEFLATE under content_encoding "deflate", the same ambiguity HTTP
45
+ # has carried for decades and that Zlib::Deflate.new(level, -MAX_WBITS)
46
+ # exists for.
47
+ #
48
+ # @example Override the built-in deflate coder
49
+ # AMQP::Client.configure do |config|
50
+ # config.enable_builtin_codecs
51
+ # config.register_coder(content_encoding: "deflate",
52
+ # coder: AMQP::Client::Coders::DeflateRaw)
53
+ # end
54
+ DeflateRaw = Class.new do
55
+ def encode(data, _properties)
56
+ return data if data.encoding == Encoding::BINARY
57
+
58
+ deflater = Zlib::Deflate.new(Zlib::DEFAULT_COMPRESSION, -Zlib::MAX_WBITS)
59
+ begin
60
+ deflater.deflate(data, Zlib::FINISH)
61
+ ensure
62
+ deflater.close
63
+ end
64
+ end
65
+
66
+ def decode(data, _properties)
67
+ inflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
68
+ begin
69
+ inflater.inflate(data)
70
+ ensure
71
+ inflater.close
72
+ end
73
+ end
74
+ end.new
41
75
  end
42
76
  end
43
77
  end
@@ -50,11 +50,13 @@ module AMQP
50
50
  # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
51
51
  # The proc will be called with the consumer tag as the only argument
52
52
  # @param arguments [Hash] Custom arguments to the consumer
53
+ # @param consumer_tag [String, nil] Custom consumer tag. Pass nil or "" to let the broker generate one.
53
54
  # @yield [Message] Delivered message from the queue
54
55
  # @return [Consumer] The consumer object, which can be used to cancel the consumer
55
56
  def subscribe(no_ack: false, exclusive: false, prefetch: 1, worker_threads: 1, requeue_on_reject: true,
56
- on_cancel: nil, arguments: {})
57
- @client.subscribe(@name, no_ack:, exclusive:, prefetch:, worker_threads:, on_cancel:, arguments:) do |message|
57
+ on_cancel: nil, arguments: {}, consumer_tag: nil)
58
+ @client.subscribe(@name, no_ack:, exclusive:, prefetch:, worker_threads:,
59
+ on_cancel:, arguments:, consumer_tag:) do |message|
58
60
  yield message
59
61
  message.ack unless no_ack
60
62
  rescue StandardError => e
@@ -3,6 +3,6 @@
3
3
  module AMQP
4
4
  class Client
5
5
  # Version of the client library
6
- VERSION = "2.0.1"
6
+ VERSION = "2.2.0"
7
7
  end
8
8
  end
data/lib/amqp/client.rb CHANGED
@@ -32,9 +32,28 @@ module AMQP
32
32
  # the smallest of the client's and the broker's values will be used
33
33
  # @option options [Integer] channel_max (2048) Maximum number of channels the client will be allowed to have open.
34
34
  # Maximum allowed is 65_536. The smallest of the client's and the broker's value will be used.
35
- def initialize(uri = "", **options)
35
+ # @option options [#info, #warn, #error] logger (nil) Logger for {#start} lifecycle events
36
+ # (connected/reconnected/disconnected/reconnect errors). When nil, reconnect errors are
37
+ # written to stderr via Kernel#warn for backwards compatibility.
38
+ # @param on_connect [Proc, nil] Optional callback invoked with the client after each successful
39
+ # (re)connection, after consumer recovery.
40
+ # @param on_failed [Proc, nil] Optional callback invoked with the triggering error once
41
+ # reconnection has given up after max_retries consecutive failed attempts. The supervisor
42
+ # stops after calling it, same as after {#stop}.
43
+ # @param max_retries [Integer, nil] Number of consecutive reconnect attempts to allow before
44
+ # giving up and calling on_failed. Defaults to nil, retrying forever.
45
+ def initialize(uri = "", on_connect: nil, on_failed: nil, max_retries: nil, **options)
46
+ if max_retries && (!max_retries.is_a?(Integer) || max_retries.negative?)
47
+ raise ArgumentError, "max_retries must be a non-negative Integer or nil"
48
+ end
49
+
36
50
  @uri = uri
37
51
  @options = options
52
+ @on_connect = on_connect
53
+ @on_failed = on_failed
54
+ @max_retries = max_retries
55
+ @logger = options[:logger]
56
+ @name = parse_name(uri)
38
57
  @queues = {}
39
58
  @exchanges = {}
40
59
  @consumers = {}
@@ -57,7 +76,8 @@ module AMQP
57
76
  # @example
58
77
  # connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
59
78
  def connect(read_loop_thread: true)
60
- Connection.new(@uri, read_loop_thread:, codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
79
+ Connection.new(@uri, read_loop_thread:, name: @name,
80
+ codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
61
81
  end
62
82
 
63
83
  # Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
@@ -74,38 +94,36 @@ module AMQP
74
94
 
75
95
  @supervisor_started = true
76
96
  @stopped = false
77
- Thread.new(connect(read_loop_thread: false)) do |conn|
97
+ initial_conn = connect(read_loop_thread: false)
98
+ log_lifecycle(:info, "connected")
99
+ supervisor = Thread.new(initial_conn) do |conn|
78
100
  Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
101
+ reconnect_attempts = 0
79
102
  loop do
80
103
  break if @stopped
81
104
 
82
- conn ||= connect(read_loop_thread: false)
83
-
84
- Thread.new do
85
- # restore connection in another thread, read_loop have to run
86
- conn.channel(1) # reserve channel 1 for publishes
87
- @consumers.each_value do |consumer|
88
- ch = conn.channel
89
- ch.basic_qos(consumer.prefetch)
90
- consume_ok = ch.basic_consume(consumer.queue,
91
- **consumer.basic_consume_args,
92
- &consumer.block)
93
- # Update the consumer with new channel and consume_ok metadata
94
- consumer.update_consume_ok(consume_ok)
95
- end
96
- @connq << conn
97
- # Remove consumers whose internal queues were already closed (e.g. cancelled during reconnect window)
98
- @consumers.delete_if { |_, c| c.closed? }
105
+ unless conn
106
+ conn = connect(read_loop_thread: false)
107
+ reconnect_attempts = 0
108
+ log_lifecycle(:info, "reconnected")
99
109
  end
110
+
111
+ setup = Thread.new { restore_connection(conn) }
112
+ setup.name = thread_name("reconnect_setup")
100
113
  conn.read_loop # blocks until connection is closed, then reconnect
114
+ log_lifecycle(:warn, "disconnected")
101
115
  rescue Error => e
102
- warn "AMQP-Client reconnect error: #{e.inspect}"
116
+ reconnect_attempts += 1
117
+ break if give_up_reconnecting?(reconnect_attempts, e)
118
+
119
+ log_reconnect_error(e)
103
120
  sleep @options[:reconnect_interval] || 1
104
121
  ensure
105
122
  @connq.clear
106
123
  conn = nil
107
124
  end
108
125
  end
126
+ supervisor.name = thread_name("supervisor")
109
127
  end
110
128
  self
111
129
  end
@@ -133,7 +151,7 @@ module AMQP
133
151
  # @!group High level objects
134
152
 
135
153
  # Declare a queue
136
- # @param name [String] Name of the queue
154
+ # @param name [String, nil] Name of the queue. Pass nil or "" for a server-named queue.
137
155
  # @param durable [Boolean] If true the queue will survive broker restarts,
138
156
  # messages in the queue will only survive if they are published as persistent
139
157
  # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
@@ -147,7 +165,12 @@ module AMQP
147
165
  # q = amqp.queue("foobar")
148
166
  # q.publish("body")
149
167
  def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
150
- raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
168
+ if server_named_queue?(name)
169
+ queue_ok = with_connection do |conn|
170
+ conn.channel(1).queue_declare("", durable:, auto_delete:, exclusive:, passive:, arguments:)
171
+ end
172
+ return Queue.new(self, queue_ok.queue_name)
173
+ end
151
174
 
152
175
  @queues.fetch(name) do
153
176
  with_connection do |conn|
@@ -302,10 +325,11 @@ module AMQP
302
325
  # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
303
326
  # The proc will be called with the consumer tag as the only argument
304
327
  # @param arguments [Hash] Custom arguments to the consumer
328
+ # @param consumer_tag [String, nil] Custom consumer tag. Pass nil or "" to let the broker generate one.
305
329
  # @yield [Message] Delivered message from the queue
306
330
  # @return [Consumer] The consumer object, which can be used to cancel the consumer
307
331
  def subscribe(queue, exclusive: false, no_ack: false, prefetch: 1, worker_threads: 1,
308
- on_cancel: nil, arguments: {}, &blk)
332
+ on_cancel: nil, arguments: {}, consumer_tag: nil, &blk)
309
333
  raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
310
334
 
311
335
  with_connection do |conn|
@@ -316,7 +340,9 @@ module AMQP
316
340
  @consumers.delete(consumer_id)
317
341
  on_cancel&.call(tag)
318
342
  end
319
- basic_consume_args = { exclusive:, no_ack:, worker_threads:, on_cancel: on_cancel_proc, arguments: }
343
+ tag = consumer_tag.nil? ? "" : consumer_tag
344
+ basic_consume_args = { tag:, exclusive:, no_ack:, worker_threads:,
345
+ on_cancel: on_cancel_proc, arguments: }
320
346
  consume_ok = ch.basic_consume(queue, **basic_consume_args, &blk)
321
347
  consumer = Consumer.new(client: self, channel_id: ch.id, id: consumer_id, block: blk,
322
348
  queue:, consume_ok:, prefetch:, basic_consume_args:)
@@ -539,6 +565,8 @@ module AMQP
539
565
  # @!endgroup
540
566
  #
541
567
  def with_connection
568
+ return yield Thread.current[reserved_conn_key] if Thread.current[reserved_conn_key]
569
+
542
570
  conn = nil
543
571
  loop do
544
572
  conn = @connq.pop
@@ -557,12 +585,128 @@ module AMQP
557
585
  def cancel_consumer(consumer)
558
586
  @consumers.delete(consumer.id)
559
587
  with_connection do |conn|
560
- conn.channel(consumer.channel_id).basic_cancel(consumer.tag)
588
+ ch = conn.channel(consumer.channel_id)
589
+ begin
590
+ ch.basic_cancel(consumer.tag)
591
+ ensure
592
+ ch.close
593
+ end
561
594
  end
562
595
  end
563
596
 
564
597
  private
565
598
 
599
+ def parse_name(uri)
600
+ return nil if uri.nil? || uri.empty?
601
+
602
+ query = URI.parse(uri).query
603
+ return nil unless query
604
+
605
+ URI.decode_www_form(query).each { |k, v| return v if k == "name" }
606
+ nil
607
+ rescue URI::InvalidURIError
608
+ nil
609
+ end
610
+
611
+ def log_lifecycle(level, event)
612
+ return unless @logger
613
+
614
+ @logger.public_send(level, "#{lifecycle_prefix}: #{event}")
615
+ end
616
+
617
+ def log_reconnect_error(err)
618
+ if @logger
619
+ @logger.warn("#{lifecycle_prefix}: reconnect error: #{err.inspect}")
620
+ else
621
+ warn "AMQP-Client reconnect error: #{err.inspect}"
622
+ end
623
+ end
624
+
625
+ def log_give_up(reconnect_attempts, err)
626
+ message = "gave up reconnecting after #{reconnect_attempts} attempts: #{err.inspect}"
627
+ if @logger
628
+ @logger.error("#{lifecycle_prefix}: #{message}")
629
+ else
630
+ warn "AMQP-Client #{message}"
631
+ end
632
+ end
633
+
634
+ def thread_name(role)
635
+ @name ? "amqp.#{role}[#{@name}]" : "amqp.#{role}"
636
+ end
637
+
638
+ def lifecycle_prefix
639
+ @name ? "AMQP::Client[#{@name}]" : "AMQP::Client"
640
+ end
641
+
642
+ def restore_connection(conn)
643
+ conn.channel(1)
644
+ # Snapshot because @consumers can mutate while recovery is running.
645
+ @consumers.values.each do |consumer| # rubocop:disable Style/HashEachMethods
646
+ ch = conn.channel
647
+ ch.basic_qos(consumer.prefetch)
648
+ consume_ok = ch.basic_consume(consumer.queue,
649
+ **consumer.basic_consume_args,
650
+ &consumer.block)
651
+ consumer.update_consume_ok(consume_ok, ch.id)
652
+ @consumers.delete(consumer.id) if consumer.closed?
653
+ rescue Error::ChannelClosed => e
654
+ log_lifecycle(:warn, "failed to resubscribe consumer for #{consumer.queue}: #{e.message}")
655
+ @consumers.delete(consumer.id)
656
+ end
657
+ run_on_connect_hook(conn)
658
+ @connq << conn
659
+ end
660
+
661
+ def server_named_queue?(name)
662
+ name.nil? || name.empty?
663
+ end
664
+
665
+ # Thread-local (not an ivar) so the hook can call back into the client's API without
666
+ # deadlocking, and overlapping reconnects can't clobber each other's reservation.
667
+ def run_on_connect_hook(conn)
668
+ return unless @on_connect
669
+
670
+ Thread.current[reserved_conn_key] = conn
671
+ @on_connect.call(self)
672
+ rescue StandardError => e
673
+ if @logger
674
+ log_lifecycle(:warn, "on_connect raised: #{e.class}: #{e.message}")
675
+ else
676
+ warn "AMQP-Client on_connect error: #{e.inspect}"
677
+ end
678
+ ensure
679
+ Thread.current[reserved_conn_key] = nil
680
+ end
681
+
682
+ # Scoped per instance so a thread touching two Client objects can't cross-wire connections.
683
+ def reserved_conn_key
684
+ :"amqp_client_conn_#{object_id}"
685
+ end
686
+
687
+ # Reports whether the supervisor should give up after this reconnect failure, logging and
688
+ # calling on_failed as a side effect when it does.
689
+ def give_up_reconnecting?(reconnect_attempts, err)
690
+ return false unless @max_retries && reconnect_attempts >= @max_retries
691
+
692
+ @stopped = true
693
+ log_give_up(reconnect_attempts, err)
694
+ run_on_failed_hook(err)
695
+ true
696
+ end
697
+
698
+ def run_on_failed_hook(err)
699
+ return unless @on_failed
700
+
701
+ @on_failed.call(err)
702
+ rescue StandardError => e
703
+ if @logger
704
+ log_lifecycle(:warn, "on_failed raised: #{e.class}: #{e.message}")
705
+ else
706
+ warn "AMQP-Client on_failed error: #{e.inspect}"
707
+ end
708
+ end
709
+
566
710
  def default_content_properties
567
711
  {
568
712
  content_type: @default_content_type,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amqp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - CloudAMQP
@@ -9,7 +9,9 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
- description: Modern AMQP 0-9-1 Ruby client
12
+ description: A modern AMQP 0-9-1 Ruby client for RabbitMQ, LavinMQ and any other AMQP
13
+ 0-9-1 broker. Very fast, fully thread-safe, with blocking operations, straight-forward
14
+ error handling and no dependencies.
13
15
  email:
14
16
  - team@cloudamqp.com
15
17
  executables: []
@@ -41,6 +43,8 @@ metadata:
41
43
  homepage_uri: https://github.com/cloudamqp/amqp-client.rb
42
44
  source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
43
45
  changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
46
+ documentation_uri: https://cloudamqp.github.io/amqp-client.rb/
47
+ bug_tracker_uri: https://github.com/cloudamqp/amqp-client.rb/issues
44
48
  rubygems_mfa_required: 'true'
45
49
  rdoc_options: []
46
50
  require_paths:
@@ -49,14 +53,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
49
53
  requirements:
50
54
  - - ">="
51
55
  - !ruby/object:Gem::Version
52
- version: 3.2.0
56
+ version: 3.3.0
53
57
  required_rubygems_version: !ruby/object:Gem::Requirement
54
58
  requirements:
55
59
  - - ">="
56
60
  - !ruby/object:Gem::Version
57
61
  version: '0'
58
62
  requirements: []
59
- rubygems_version: 3.6.9
63
+ rubygems_version: 4.0.16
60
64
  specification_version: 4
61
- summary: AMQP 0-9-1 client
65
+ summary: Modern, fast and dependency-free AMQP 0-9-1 Ruby client for RabbitMQ and
66
+ LavinMQ
62
67
  test_files: []