amqp-client 2.1.0 → 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: 768fa0e71de1ae6cc2ae41c902c4d61ee505f2dbf400552371fde1185a0afc60
4
- data.tar.gz: 52b30e22ac20f6ab94d30b24457a930b9dd72d36c7196d26f255badaa6cc1220
3
+ metadata.gz: b52ef0851be1716d1833d84448e2470b19938b03050a14d8b5a40a1289cb41ae
4
+ data.tar.gz: 6e33352b3e238b7ccf1c203a4779da7b5f01f659ab27c39bf7a78299398025aa
5
5
  SHA512:
6
- metadata.gz: f5e29bcd3dc25e4af06a2f5401a839a3b0d6796e8265783994facbf1a042504d351c9af5e34f96c2f52ace10e3dfb1f862030eeb1f13dc06164c1099b8bf89a5
7
- data.tar.gz: e65b267fcd879a029752d724c28a17760ad3a4723ad25657b69baa0e1c53cbaa284f67a57872ef6afa3d8e2522dc64414d5b8a75cc0181d85a0f99feae2c7e1d
6
+ metadata.gz: e78020ff0cf64c9a4a13fb64625f5f0382950c5981fb2df130f4e61066930ba0c9298bd2d7bd3982b1ddbb1ef492cab89183d7e4a1c815a9fa10204897b0ff27
7
+ data.tar.gz: 2ab30a767b5fc5aa3f5a43949f7cde1b601e74e4bdb0ebc5b0d91306deee773aac9007bcc340610ae52fcba732983b804306cb6fdcbcfdab9e064ec17292ff30
@@ -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
@@ -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.1.0"
6
+ VERSION = "2.2.0"
7
7
  end
8
8
  end
data/lib/amqp/client.rb CHANGED
@@ -35,9 +35,23 @@ module AMQP
35
35
  # @option options [#info, #warn, #error] logger (nil) Logger for {#start} lifecycle events
36
36
  # (connected/reconnected/disconnected/reconnect errors). When nil, reconnect errors are
37
37
  # written to stderr via Kernel#warn for backwards compatibility.
38
- def initialize(uri = "", **options)
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
+
39
50
  @uri = uri
40
51
  @options = options
52
+ @on_connect = on_connect
53
+ @on_failed = on_failed
54
+ @max_retries = max_retries
41
55
  @logger = options[:logger]
42
56
  @name = parse_name(uri)
43
57
  @queues = {}
@@ -82,36 +96,26 @@ module AMQP
82
96
  @stopped = false
83
97
  initial_conn = connect(read_loop_thread: false)
84
98
  log_lifecycle(:info, "connected")
85
- supervisor = Thread.new(initial_conn) do |conn| # rubocop:disable Metrics/BlockLength
99
+ supervisor = Thread.new(initial_conn) do |conn|
86
100
  Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
87
- loop do # rubocop:disable Metrics/BlockLength
101
+ reconnect_attempts = 0
102
+ loop do
88
103
  break if @stopped
89
104
 
90
105
  unless conn
91
106
  conn = connect(read_loop_thread: false)
107
+ reconnect_attempts = 0
92
108
  log_lifecycle(:info, "reconnected")
93
109
  end
94
110
 
95
- setup = Thread.new do
96
- # restore connection in another thread, read_loop have to run
97
- conn.channel(1) # reserve channel 1 for publishes
98
- @consumers.each_value do |consumer|
99
- ch = conn.channel
100
- ch.basic_qos(consumer.prefetch)
101
- consume_ok = ch.basic_consume(consumer.queue,
102
- **consumer.basic_consume_args,
103
- &consumer.block)
104
- # Update the consumer with new channel and consume_ok metadata
105
- consumer.update_consume_ok(consume_ok)
106
- end
107
- @connq << conn
108
- # Remove consumers whose internal queues were already closed (e.g. cancelled during reconnect window)
109
- @consumers.delete_if { |_, c| c.closed? }
110
- end
111
+ setup = Thread.new { restore_connection(conn) }
111
112
  setup.name = thread_name("reconnect_setup")
112
113
  conn.read_loop # blocks until connection is closed, then reconnect
113
114
  log_lifecycle(:warn, "disconnected")
114
115
  rescue Error => e
116
+ reconnect_attempts += 1
117
+ break if give_up_reconnecting?(reconnect_attempts, e)
118
+
115
119
  log_reconnect_error(e)
116
120
  sleep @options[:reconnect_interval] || 1
117
121
  ensure
@@ -147,7 +151,7 @@ module AMQP
147
151
  # @!group High level objects
148
152
 
149
153
  # Declare a queue
150
- # @param name [String] Name of the queue
154
+ # @param name [String, nil] Name of the queue. Pass nil or "" for a server-named queue.
151
155
  # @param durable [Boolean] If true the queue will survive broker restarts,
152
156
  # messages in the queue will only survive if they are published as persistent
153
157
  # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
@@ -161,7 +165,12 @@ module AMQP
161
165
  # q = amqp.queue("foobar")
162
166
  # q.publish("body")
163
167
  def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
164
- 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
165
174
 
166
175
  @queues.fetch(name) do
167
176
  with_connection do |conn|
@@ -316,10 +325,11 @@ module AMQP
316
325
  # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
317
326
  # The proc will be called with the consumer tag as the only argument
318
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.
319
329
  # @yield [Message] Delivered message from the queue
320
330
  # @return [Consumer] The consumer object, which can be used to cancel the consumer
321
331
  def subscribe(queue, exclusive: false, no_ack: false, prefetch: 1, worker_threads: 1,
322
- on_cancel: nil, arguments: {}, &blk)
332
+ on_cancel: nil, arguments: {}, consumer_tag: nil, &blk)
323
333
  raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
324
334
 
325
335
  with_connection do |conn|
@@ -330,7 +340,9 @@ module AMQP
330
340
  @consumers.delete(consumer_id)
331
341
  on_cancel&.call(tag)
332
342
  end
333
- 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: }
334
346
  consume_ok = ch.basic_consume(queue, **basic_consume_args, &blk)
335
347
  consumer = Consumer.new(client: self, channel_id: ch.id, id: consumer_id, block: blk,
336
348
  queue:, consume_ok:, prefetch:, basic_consume_args:)
@@ -553,6 +565,8 @@ module AMQP
553
565
  # @!endgroup
554
566
  #
555
567
  def with_connection
568
+ return yield Thread.current[reserved_conn_key] if Thread.current[reserved_conn_key]
569
+
556
570
  conn = nil
557
571
  loop do
558
572
  conn = @connq.pop
@@ -608,6 +622,15 @@ module AMQP
608
622
  end
609
623
  end
610
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
+
611
634
  def thread_name(role)
612
635
  @name ? "amqp.#{role}[#{@name}]" : "amqp.#{role}"
613
636
  end
@@ -616,6 +639,74 @@ module AMQP
616
639
  @name ? "AMQP::Client[#{@name}]" : "AMQP::Client"
617
640
  end
618
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
+
619
710
  def default_content_properties
620
711
  {
621
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.1.0
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:
@@ -56,7 +60,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
60
  - !ruby/object:Gem::Version
57
61
  version: '0'
58
62
  requirements: []
59
- rubygems_version: 4.0.10
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: []