amqp-client 2.0.1 → 2.1.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/message_codecs.rb +34 -0
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +60 -7
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 768fa0e71de1ae6cc2ae41c902c4d61ee505f2dbf400552371fde1185a0afc60
|
|
4
|
+
data.tar.gz: 52b30e22ac20f6ab94d30b24457a930b9dd72d36c7196d26f255badaa6cc1220
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f5e29bcd3dc25e4af06a2f5401a839a3b0d6796e8265783994facbf1a042504d351c9af5e34f96c2f52ace10e3dfb1f862030eeb1f13dc06164c1099b8bf89a5
|
|
7
|
+
data.tar.gz: e65b267fcd879a029752d724c28a17760ad3a4723ad25657b69baa0e1c53cbaa284f67a57872ef6afa3d8e2522dc64414d5b8a75cc0181d85a0f99feae2c7e1d
|
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
|
|
@@ -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/version.rb
CHANGED
data/lib/amqp/client.rb
CHANGED
|
@@ -32,9 +32,14 @@ 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
|
+
# @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.
|
|
35
38
|
def initialize(uri = "", **options)
|
|
36
39
|
@uri = uri
|
|
37
40
|
@options = options
|
|
41
|
+
@logger = options[:logger]
|
|
42
|
+
@name = parse_name(uri)
|
|
38
43
|
@queues = {}
|
|
39
44
|
@exchanges = {}
|
|
40
45
|
@consumers = {}
|
|
@@ -57,7 +62,8 @@ module AMQP
|
|
|
57
62
|
# @example
|
|
58
63
|
# connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
|
|
59
64
|
def connect(read_loop_thread: true)
|
|
60
|
-
Connection.new(@uri, read_loop_thread:,
|
|
65
|
+
Connection.new(@uri, read_loop_thread:, name: @name,
|
|
66
|
+
codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
|
|
61
67
|
end
|
|
62
68
|
|
|
63
69
|
# Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
|
|
@@ -74,14 +80,19 @@ module AMQP
|
|
|
74
80
|
|
|
75
81
|
@supervisor_started = true
|
|
76
82
|
@stopped = false
|
|
77
|
-
|
|
83
|
+
initial_conn = connect(read_loop_thread: false)
|
|
84
|
+
log_lifecycle(:info, "connected")
|
|
85
|
+
supervisor = Thread.new(initial_conn) do |conn| # rubocop:disable Metrics/BlockLength
|
|
78
86
|
Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
|
|
79
|
-
loop do
|
|
87
|
+
loop do # rubocop:disable Metrics/BlockLength
|
|
80
88
|
break if @stopped
|
|
81
89
|
|
|
82
|
-
conn
|
|
90
|
+
unless conn
|
|
91
|
+
conn = connect(read_loop_thread: false)
|
|
92
|
+
log_lifecycle(:info, "reconnected")
|
|
93
|
+
end
|
|
83
94
|
|
|
84
|
-
Thread.new do
|
|
95
|
+
setup = Thread.new do
|
|
85
96
|
# restore connection in another thread, read_loop have to run
|
|
86
97
|
conn.channel(1) # reserve channel 1 for publishes
|
|
87
98
|
@consumers.each_value do |consumer|
|
|
@@ -97,15 +108,18 @@ module AMQP
|
|
|
97
108
|
# Remove consumers whose internal queues were already closed (e.g. cancelled during reconnect window)
|
|
98
109
|
@consumers.delete_if { |_, c| c.closed? }
|
|
99
110
|
end
|
|
111
|
+
setup.name = thread_name("reconnect_setup")
|
|
100
112
|
conn.read_loop # blocks until connection is closed, then reconnect
|
|
113
|
+
log_lifecycle(:warn, "disconnected")
|
|
101
114
|
rescue Error => e
|
|
102
|
-
|
|
115
|
+
log_reconnect_error(e)
|
|
103
116
|
sleep @options[:reconnect_interval] || 1
|
|
104
117
|
ensure
|
|
105
118
|
@connq.clear
|
|
106
119
|
conn = nil
|
|
107
120
|
end
|
|
108
121
|
end
|
|
122
|
+
supervisor.name = thread_name("supervisor")
|
|
109
123
|
end
|
|
110
124
|
self
|
|
111
125
|
end
|
|
@@ -557,12 +571,51 @@ module AMQP
|
|
|
557
571
|
def cancel_consumer(consumer)
|
|
558
572
|
@consumers.delete(consumer.id)
|
|
559
573
|
with_connection do |conn|
|
|
560
|
-
conn.channel(consumer.channel_id)
|
|
574
|
+
ch = conn.channel(consumer.channel_id)
|
|
575
|
+
begin
|
|
576
|
+
ch.basic_cancel(consumer.tag)
|
|
577
|
+
ensure
|
|
578
|
+
ch.close
|
|
579
|
+
end
|
|
561
580
|
end
|
|
562
581
|
end
|
|
563
582
|
|
|
564
583
|
private
|
|
565
584
|
|
|
585
|
+
def parse_name(uri)
|
|
586
|
+
return nil if uri.nil? || uri.empty?
|
|
587
|
+
|
|
588
|
+
query = URI.parse(uri).query
|
|
589
|
+
return nil unless query
|
|
590
|
+
|
|
591
|
+
URI.decode_www_form(query).each { |k, v| return v if k == "name" }
|
|
592
|
+
nil
|
|
593
|
+
rescue URI::InvalidURIError
|
|
594
|
+
nil
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def log_lifecycle(level, event)
|
|
598
|
+
return unless @logger
|
|
599
|
+
|
|
600
|
+
@logger.public_send(level, "#{lifecycle_prefix}: #{event}")
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def log_reconnect_error(err)
|
|
604
|
+
if @logger
|
|
605
|
+
@logger.warn("#{lifecycle_prefix}: reconnect error: #{err.inspect}")
|
|
606
|
+
else
|
|
607
|
+
warn "AMQP-Client reconnect error: #{err.inspect}"
|
|
608
|
+
end
|
|
609
|
+
end
|
|
610
|
+
|
|
611
|
+
def thread_name(role)
|
|
612
|
+
@name ? "amqp.#{role}[#{@name}]" : "amqp.#{role}"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def lifecycle_prefix
|
|
616
|
+
@name ? "AMQP::Client[#{@name}]" : "AMQP::Client"
|
|
617
|
+
end
|
|
618
|
+
|
|
566
619
|
def default_content_properties
|
|
567
620
|
{
|
|
568
621
|
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.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CloudAMQP
|
|
@@ -49,14 +49,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
49
49
|
requirements:
|
|
50
50
|
- - ">="
|
|
51
51
|
- !ruby/object:Gem::Version
|
|
52
|
-
version: 3.
|
|
52
|
+
version: 3.3.0
|
|
53
53
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
54
|
requirements:
|
|
55
55
|
- - ">="
|
|
56
56
|
- !ruby/object:Gem::Version
|
|
57
57
|
version: '0'
|
|
58
58
|
requirements: []
|
|
59
|
-
rubygems_version:
|
|
59
|
+
rubygems_version: 4.0.10
|
|
60
60
|
specification_version: 4
|
|
61
61
|
summary: AMQP 0-9-1 client
|
|
62
62
|
test_files: []
|