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 +4 -4
- data/lib/amqp/client/channel.rb +33 -12
- data/lib/amqp/client/connection.rb +33 -5
- data/lib/amqp/client/consumer.rb +2 -1
- data/lib/amqp/client/message_codecs.rb +34 -0
- data/lib/amqp/client/queue.rb +4 -2
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +170 -26
- metadata +10 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b52ef0851be1716d1833d84448e2470b19938b03050a14d8b5a40a1289cb41ae
|
|
4
|
+
data.tar.gz: 6e33352b3e238b7ccf1c203a4779da7b5f01f659ab27c39bf7a78299398025aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e78020ff0cf64c9a4a13fb64625f5f0382950c5981fb2df130f4e61066930ba0c9298bd2d7bd3982b1ddbb1ef492cab89183d7e4a1c815a9fa10204897b0ff27
|
|
7
|
+
data.tar.gz: 2ab30a767b5fc5aa3f5a43949f7cde1b601e74e4bdb0ebc5b0d91306deee773aac9007bcc340610ae52fcba732983b804306cb6fdcbcfdab9e064ec17292ff30
|
data/lib/amqp/client/channel.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
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::
|
|
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::
|
|
563
|
+
raise Error::UnexpectedFrameEnd, frame_end if frame_end != 206
|
|
536
564
|
|
|
537
565
|
case type
|
|
538
566
|
when 1 # method frame
|
data/lib/amqp/client/consumer.rb
CHANGED
|
@@ -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
|
data/lib/amqp/client/queue.rb
CHANGED
|
@@ -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:,
|
|
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
|
data/lib/amqp/client/version.rb
CHANGED
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
|
-
|
|
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:,
|
|
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
|
-
|
|
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
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
|
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:
|
|
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.
|
|
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:
|
|
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: []
|