amqp-client 1.2.1 → 2.0.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 +115 -46
- data/lib/amqp/client/configuration.rb +66 -0
- data/lib/amqp/client/connection.rb +35 -18
- data/lib/amqp/client/consumer.rb +47 -0
- data/lib/amqp/client/errors.rb +84 -9
- data/lib/amqp/client/exchange.rb +24 -26
- data/lib/amqp/client/frame_bytes.rb +3 -4
- data/lib/amqp/client/message.rb +66 -1
- data/lib/amqp/client/message_codec_registry.rb +106 -0
- data/lib/amqp/client/message_codecs.rb +43 -0
- data/lib/amqp/client/properties.rb +16 -15
- data/lib/amqp/client/queue.rb +48 -14
- data/lib/amqp/client/rpc_client.rb +56 -0
- data/lib/amqp/client/table.rb +2 -2
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +343 -79
- metadata +8 -18
- data/.github/workflows/codeql-analysis.yml +0 -41
- data/.github/workflows/docs.yml +0 -28
- data/.github/workflows/main.yml +0 -147
- data/.github/workflows/release.yml +0 -54
- data/.gitignore +0 -9
- data/.rubocop.yml +0 -30
- data/.rubocop_todo.yml +0 -65
- data/.yardopts +0 -1
- data/CHANGELOG.md +0 -115
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -18
- data/README.md +0 -193
- data/Rakefile +0 -197
- data/amqp-client.gemspec +0 -29
- data/bin/console +0 -15
- data/bin/setup +0 -8
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c07d38a543ff2c0e6e57dfc4ce4921ba74dbac1091cd55a7d06fc791a4ba501e
|
|
4
|
+
data.tar.gz: 1909a126c2c6c21d36990cff7b0c623ad4e843afe52ddd5cad69299487286f0c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6628d1283167e699482baf15ae84bc336c59015b9a86921090ea2a15513267e1fe290788640cef65f92e88f779b039288b05fa6a50d25a449144268edeac7003
|
|
7
|
+
data.tar.gz: 1684f2e5ef0ffb7c8a67371fe80031904681ccac21e164d190a87605f52b5b73560b4dbd2862f4047f944238f1126aa6de139cdbd1fa1b9acdd39324f8d71c56
|
data/lib/amqp/client/channel.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "
|
|
3
|
+
require_relative "message"
|
|
4
4
|
require "stringio"
|
|
5
5
|
|
|
6
6
|
module AMQP
|
|
@@ -25,20 +25,25 @@ module AMQP
|
|
|
25
25
|
@unconfirmed = []
|
|
26
26
|
@unconfirmed_lock = Mutex.new
|
|
27
27
|
@unconfirmed_empty = ConditionVariable.new
|
|
28
|
+
@nacked = false
|
|
28
29
|
@basic_gets = ::Queue.new
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
# Override #inspect
|
|
32
33
|
# @api private
|
|
33
34
|
def inspect
|
|
34
|
-
"#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?}"\
|
|
35
|
-
"
|
|
35
|
+
"#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?} " \
|
|
36
|
+
"consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
# Channel ID
|
|
39
40
|
# @return [Integer]
|
|
40
41
|
attr_reader :id
|
|
41
42
|
|
|
43
|
+
# Connection this channel belongs to
|
|
44
|
+
# @return [Connection]
|
|
45
|
+
attr_reader :connection
|
|
46
|
+
|
|
42
47
|
# Open the channel (called from Connection)
|
|
43
48
|
# @return [Channel] self
|
|
44
49
|
# @api private
|
|
@@ -51,7 +56,9 @@ module AMQP
|
|
|
51
56
|
self
|
|
52
57
|
end
|
|
53
58
|
|
|
54
|
-
# Gracefully close
|
|
59
|
+
# Gracefully close channel
|
|
60
|
+
# @param reason [String] The reason for closing the channel
|
|
61
|
+
# @param code [Integer] The close code
|
|
55
62
|
# @return [nil]
|
|
56
63
|
def close(reason: "", code: 200)
|
|
57
64
|
return if @closed
|
|
@@ -62,7 +69,7 @@ module AMQP
|
|
|
62
69
|
@replies.close
|
|
63
70
|
@basic_gets.close
|
|
64
71
|
@unconfirmed_lock.synchronize { @unconfirmed_empty.broadcast }
|
|
65
|
-
@consumers.each_value(
|
|
72
|
+
@consumers.each_value { |c| close_consumer(c) }
|
|
66
73
|
nil
|
|
67
74
|
end
|
|
68
75
|
|
|
@@ -75,8 +82,10 @@ module AMQP
|
|
|
75
82
|
@replies.close
|
|
76
83
|
@basic_gets.close
|
|
77
84
|
@unconfirmed_lock.synchronize { @unconfirmed_empty.broadcast }
|
|
78
|
-
@consumers.each_value
|
|
79
|
-
|
|
85
|
+
@consumers.each_value do |c|
|
|
86
|
+
close_consumer(c)
|
|
87
|
+
c.msg_q.clear # empty the queues too, messages can't be acked anymore
|
|
88
|
+
end
|
|
80
89
|
nil
|
|
81
90
|
end
|
|
82
91
|
|
|
@@ -100,7 +109,7 @@ module AMQP
|
|
|
100
109
|
# @param internal [Boolean] If true the exchange can't be published to directly
|
|
101
110
|
# @param arguments [Hash] Custom arguments
|
|
102
111
|
# @return [nil]
|
|
103
|
-
def exchange_declare(name, type
|
|
112
|
+
def exchange_declare(name, type:, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
|
|
104
113
|
write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
|
|
105
114
|
expect :exchange_declare_ok
|
|
106
115
|
nil
|
|
@@ -118,24 +127,24 @@ module AMQP
|
|
|
118
127
|
end
|
|
119
128
|
|
|
120
129
|
# Bind an exchange to another exchange
|
|
121
|
-
# @param
|
|
122
|
-
# @param
|
|
130
|
+
# @param source [String] Name of the source exchange
|
|
131
|
+
# @param destination [String] Name of the destination exchange
|
|
123
132
|
# @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
|
|
124
133
|
# @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
|
|
125
134
|
# @return [nil]
|
|
126
|
-
def exchange_bind(destination
|
|
135
|
+
def exchange_bind(source:, destination:, binding_key:, arguments: {})
|
|
127
136
|
write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
|
|
128
137
|
expect :exchange_bind_ok
|
|
129
138
|
nil
|
|
130
139
|
end
|
|
131
140
|
|
|
132
141
|
# Unbind an exchange from another exchange
|
|
133
|
-
# @param
|
|
134
|
-
# @param
|
|
142
|
+
# @param source [String] Name of the source exchange
|
|
143
|
+
# @param destination [String] Name of the destination exchange
|
|
135
144
|
# @param binding_key [String] Binding key which the queue is bound to the exchange with
|
|
136
145
|
# @param arguments [Hash] Arguments matching the binding that's being removed
|
|
137
146
|
# @return [nil]
|
|
138
|
-
def exchange_unbind(destination
|
|
147
|
+
def exchange_unbind(source:, destination:, binding_key:, arguments: {})
|
|
139
148
|
write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
|
|
140
149
|
expect :exchange_unbind_ok
|
|
141
150
|
nil
|
|
@@ -151,18 +160,18 @@ module AMQP
|
|
|
151
160
|
# @return [Integer] Number of messages in the queue at the time of declaration
|
|
152
161
|
# @!attribute consumer_count
|
|
153
162
|
# @return [Integer] Number of consumers subscribed to the queue at the time of declaration
|
|
154
|
-
QueueOk =
|
|
163
|
+
QueueOk = Data.define(:queue_name, :message_count, :consumer_count)
|
|
155
164
|
|
|
156
165
|
# Create a queue (operation is idempotent)
|
|
157
166
|
# @param name [String] Name of the queue, can be empty, but will then be generated by the broker
|
|
158
167
|
# @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
|
|
159
168
|
# @param durable [Boolean] If true the queue will survive broker restarts,
|
|
160
169
|
# messages in the queue will only survive if they are published as persistent
|
|
161
|
-
# @param exclusive [Boolean] If true the queue will be deleted when the
|
|
170
|
+
# @param exclusive [Boolean] If true the queue will be deleted when the connection is closed
|
|
162
171
|
# @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
|
|
163
172
|
# (it won't be deleted until at least one consumer has consumed from it)
|
|
164
173
|
# @param arguments [Hash] Custom arguments, such as queue-ttl etc.
|
|
165
|
-
# @return [QueueOk]
|
|
174
|
+
# @return [QueueOk]
|
|
166
175
|
def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
|
|
167
176
|
durable = false if name.empty?
|
|
168
177
|
exclusive = true if name.empty?
|
|
@@ -188,12 +197,12 @@ module AMQP
|
|
|
188
197
|
end
|
|
189
198
|
|
|
190
199
|
# Bind a queue to an exchange
|
|
191
|
-
# @param name [String] Name of the queue
|
|
192
|
-
# @param exchange [String] Name of the exchange
|
|
200
|
+
# @param name [String] Name of the queue
|
|
201
|
+
# @param exchange [String] Name of the exchange
|
|
193
202
|
# @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
|
|
194
203
|
# @param arguments [Hash] Message headers to match on, but only when bound to header exchanges
|
|
195
204
|
# @return [nil]
|
|
196
|
-
def queue_bind(name, exchange
|
|
205
|
+
def queue_bind(name, exchange:, binding_key: "", arguments: {})
|
|
197
206
|
write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
|
|
198
207
|
expect :queue_bind_ok
|
|
199
208
|
nil
|
|
@@ -211,12 +220,12 @@ module AMQP
|
|
|
211
220
|
end
|
|
212
221
|
|
|
213
222
|
# Unbind a queue from an exchange
|
|
214
|
-
# @param name [String] Name of the queue
|
|
215
|
-
# @param exchange [String] Name of the exchange
|
|
223
|
+
# @param name [String] Name of the queue
|
|
224
|
+
# @param exchange [String] Name of the exchange
|
|
216
225
|
# @param binding_key [String] Binding key which the queue is bound to the exchange with
|
|
217
226
|
# @param arguments [Hash] Arguments matching the binding that's being removed
|
|
218
227
|
# @return [nil]
|
|
219
|
-
def queue_unbind(name, exchange
|
|
228
|
+
def queue_unbind(name, exchange:, binding_key: "", arguments: {})
|
|
220
229
|
write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
|
|
221
230
|
expect :queue_unbind_ok
|
|
222
231
|
nil
|
|
@@ -240,7 +249,7 @@ module AMQP
|
|
|
240
249
|
end
|
|
241
250
|
|
|
242
251
|
# Publishes a message to an exchange
|
|
243
|
-
# @param body [String] The body
|
|
252
|
+
# @param body [String] The body
|
|
244
253
|
# @param exchange [String] Name of the exchange to publish to
|
|
245
254
|
# @param routing_key [String] The routing key that the exchange might use to route the message to a queue
|
|
246
255
|
# @param properties [Properties]
|
|
@@ -260,7 +269,7 @@ module AMQP
|
|
|
260
269
|
# @option properties [String] user_id Can be used to verify that this is the user that published the message
|
|
261
270
|
# @option properties [String] app_id Can be used to indicates which app that generated the message
|
|
262
271
|
# @return [nil]
|
|
263
|
-
def basic_publish(body, exchange
|
|
272
|
+
def basic_publish(body, exchange:, routing_key: "", **properties)
|
|
264
273
|
body_max = @connection.frame_max - 8
|
|
265
274
|
id = @id
|
|
266
275
|
mandatory = properties.delete(:mandatory) || false
|
|
@@ -297,50 +306,90 @@ module AMQP
|
|
|
297
306
|
# @option (see #basic_publish)
|
|
298
307
|
# @return [Boolean] True if the message was successfully published
|
|
299
308
|
# @raise (see #basic_publish)
|
|
300
|
-
def basic_publish_confirm(body, exchange
|
|
309
|
+
def basic_publish_confirm(body, exchange:, routing_key: "", **properties)
|
|
301
310
|
confirm_select(no_wait: true)
|
|
302
|
-
basic_publish(body, exchange
|
|
311
|
+
basic_publish(body, exchange:, routing_key:, **properties)
|
|
303
312
|
wait_for_confirms
|
|
304
313
|
end
|
|
305
314
|
|
|
315
|
+
# Response when subscribing (starting a consumer)
|
|
316
|
+
# @!attribute channel_id
|
|
317
|
+
# @return [Integer] The channel ID
|
|
318
|
+
# @!attribute consumer_tag
|
|
319
|
+
# @return [String] The consumer tag
|
|
320
|
+
# @!attribute worker_threads
|
|
321
|
+
# @return [Array<Thread>] Array of worker threads
|
|
322
|
+
ConsumeOk = Data.define(:channel_id, :consumer_tag, :worker_threads, :msg_q, :on_cancel)
|
|
323
|
+
|
|
306
324
|
# Consume messages from a queue
|
|
307
325
|
# @param queue [String] Name of the queue to subscribe to
|
|
308
326
|
# @param tag [String] Custom consumer tag, will be auto assigned by the broker if empty.
|
|
309
|
-
# Has to be
|
|
327
|
+
# Has to be unique among this channel's consumers only
|
|
310
328
|
# @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
|
|
311
329
|
# @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
|
|
312
330
|
# @param arguments [Hash] Custom arguments for the consumer
|
|
313
331
|
# @param worker_threads [Integer] Number of threads processing messages,
|
|
314
332
|
# 0 means that the thread calling this method will process the messages and thus this method will block
|
|
333
|
+
# @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
|
|
334
|
+
# The proc will be called with the consumer tag as the only argument
|
|
315
335
|
# @yield [Message] Delivered message from the queue
|
|
316
|
-
# @return [
|
|
336
|
+
# @return [ConsumeOk]
|
|
317
337
|
# @return [nil] When `worker_threads` is 0 the method will return when the consumer is cancelled
|
|
318
|
-
def basic_consume(queue, tag: "", no_ack: true, exclusive: false,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
338
|
+
def basic_consume(queue, tag: "", no_ack: true, exclusive: false, no_wait: false,
|
|
339
|
+
arguments: {}, worker_threads: 1, on_cancel: nil, &blk)
|
|
340
|
+
raise ArgumentError, "consumer_tag required when no_wait" if no_wait && tag.empty?
|
|
341
|
+
|
|
342
|
+
write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, no_wait, arguments)
|
|
343
|
+
consumer_tag, = expect(:basic_consume_ok) unless no_wait
|
|
344
|
+
msg_q = ::Queue.new
|
|
322
345
|
if worker_threads.zero?
|
|
323
|
-
|
|
346
|
+
@consumers[consumer_tag] =
|
|
347
|
+
ConsumeOk.new(channel_id: @id, consumer_tag:, worker_threads: [], msg_q:, on_cancel:)
|
|
348
|
+
consume_loop(msg_q, consumer_tag, &blk)
|
|
324
349
|
nil
|
|
325
350
|
else
|
|
326
351
|
threads = Array.new(worker_threads) do
|
|
327
|
-
Thread.new { consume_loop(
|
|
352
|
+
Thread.new { consume_loop(msg_q, consumer_tag, &blk) }
|
|
328
353
|
end
|
|
329
|
-
[
|
|
354
|
+
@consumers[consumer_tag] =
|
|
355
|
+
ConsumeOk.new(channel_id: @id, consumer_tag:, worker_threads: threads, msg_q:, on_cancel:)
|
|
330
356
|
end
|
|
331
357
|
end
|
|
332
358
|
|
|
359
|
+
# Consume a single message from a queue
|
|
360
|
+
# @param queue [String] Name of the queue to subscribe to
|
|
361
|
+
# @param timeout [Numeric, nil] Number of seconds to wait for a message
|
|
362
|
+
# @yield [] Block in which the message will be yielded
|
|
363
|
+
# @return [Message] The single message received from the queue
|
|
364
|
+
# @raise [Timeout::Error] if no response is received within the timeout period
|
|
365
|
+
def basic_consume_once(queue, timeout: nil, &)
|
|
366
|
+
tag = "consume-once-#{rand(1024)}"
|
|
367
|
+
write_bytes FrameBytes.basic_consume(@id, queue, tag, true, false, true, nil)
|
|
368
|
+
msg_q = ::Queue.new
|
|
369
|
+
@consumers[tag] =
|
|
370
|
+
ConsumeOk.new(channel_id: @id, consumer_tag: tag, worker_threads: [], msg_q:, on_cancel: nil)
|
|
371
|
+
yield if block_given?
|
|
372
|
+
msg = msg_q.pop(timeout:)
|
|
373
|
+
write_bytes FrameBytes.basic_cancel(@id, tag, no_wait: true)
|
|
374
|
+
consumer = @consumers.delete(tag)
|
|
375
|
+
close_consumer(consumer)
|
|
376
|
+
raise Timeout::Error, "No message received in #{timeout} seconds" if timeout && msg.nil?
|
|
377
|
+
|
|
378
|
+
msg
|
|
379
|
+
end
|
|
380
|
+
|
|
333
381
|
# Cancel/abort/stop a consumer
|
|
334
382
|
# @param consumer_tag [String] Tag of the consumer to cancel
|
|
335
383
|
# @param no_wait [Boolean] Will wait for a confirmation from the broker that the consumer is cancelled
|
|
336
384
|
# @return [nil]
|
|
337
385
|
def basic_cancel(consumer_tag, no_wait: false)
|
|
338
|
-
consumer = @consumers
|
|
339
|
-
return
|
|
386
|
+
consumer = @consumers[consumer_tag]
|
|
387
|
+
return unless consumer
|
|
340
388
|
|
|
341
389
|
write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
|
|
342
390
|
expect(:basic_cancel_ok) unless no_wait
|
|
343
|
-
|
|
391
|
+
@consumers.delete(consumer_tag)
|
|
392
|
+
close_consumer(consumer)
|
|
344
393
|
nil
|
|
345
394
|
end
|
|
346
395
|
|
|
@@ -357,6 +406,7 @@ module AMQP
|
|
|
357
406
|
|
|
358
407
|
# Acknowledge a message
|
|
359
408
|
# @param delivery_tag [Integer] The delivery tag of the message to acknowledge
|
|
409
|
+
# @param multiple [Boolean] Ack all messages up to this message
|
|
360
410
|
# @return [nil]
|
|
361
411
|
def basic_ack(delivery_tag, multiple: false)
|
|
362
412
|
write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
|
|
@@ -387,7 +437,7 @@ module AMQP
|
|
|
387
437
|
# if true to any consumer
|
|
388
438
|
# @return [nil]
|
|
389
439
|
def basic_recover(requeue: false)
|
|
390
|
-
write_bytes FrameBytes.basic_recover(@id, requeue:
|
|
440
|
+
write_bytes FrameBytes.basic_recover(@id, requeue:)
|
|
391
441
|
expect :basic_recover_ok
|
|
392
442
|
nil
|
|
393
443
|
end
|
|
@@ -413,20 +463,23 @@ module AMQP
|
|
|
413
463
|
end
|
|
414
464
|
|
|
415
465
|
# Block until all publishes messages are confirmed
|
|
416
|
-
# @return
|
|
466
|
+
# @return [Boolean] True if all messages were acked, false if any were nacked
|
|
417
467
|
def wait_for_confirms
|
|
418
468
|
@unconfirmed_lock.synchronize do
|
|
419
469
|
until @unconfirmed.empty?
|
|
420
470
|
@unconfirmed_empty.wait(@unconfirmed_lock)
|
|
421
471
|
raise Error::Closed.new(@id, *@closed) if @closed
|
|
422
472
|
end
|
|
473
|
+
result = !@nacked
|
|
474
|
+
@nacked = false # Reset for next round of publishes
|
|
475
|
+
result
|
|
423
476
|
end
|
|
424
477
|
end
|
|
425
478
|
|
|
426
479
|
# Called by Connection when received ack/nack from broker
|
|
427
480
|
# @api private
|
|
428
481
|
def confirm(args)
|
|
429
|
-
|
|
482
|
+
ack_or_nack, delivery_tag, multiple = *args
|
|
430
483
|
@unconfirmed_lock.synchronize do
|
|
431
484
|
case multiple
|
|
432
485
|
when true
|
|
@@ -435,6 +488,7 @@ module AMQP
|
|
|
435
488
|
when false
|
|
436
489
|
@unconfirmed.delete(delivery_tag) || raise("Delivery tag not found")
|
|
437
490
|
end
|
|
491
|
+
@nacked = true if ack_or_nack == :nack
|
|
438
492
|
@unconfirmed_empty.broadcast if @unconfirmed.empty?
|
|
439
493
|
end
|
|
440
494
|
end
|
|
@@ -508,14 +562,29 @@ module AMQP
|
|
|
508
562
|
next_message_finished!
|
|
509
563
|
end
|
|
510
564
|
|
|
565
|
+
# Handle consumer cancellation from the broker
|
|
511
566
|
# @api private
|
|
512
|
-
def
|
|
513
|
-
@consumers.
|
|
567
|
+
def cancel_consumer(tag)
|
|
568
|
+
consumer = @consumers.delete(tag)
|
|
569
|
+
return unless consumer
|
|
570
|
+
|
|
571
|
+
close_consumer(consumer)
|
|
572
|
+
begin
|
|
573
|
+
consumer.on_cancel&.call(consumer.consumer_tag)
|
|
574
|
+
rescue StandardError => e
|
|
575
|
+
warn "AMQP-Client consumer on_cancel callback error: #{e.class}: #{e.message}"
|
|
576
|
+
end
|
|
514
577
|
nil
|
|
515
578
|
end
|
|
516
579
|
|
|
517
580
|
private
|
|
518
581
|
|
|
582
|
+
def close_consumer(consumer)
|
|
583
|
+
consumer.msg_q.close
|
|
584
|
+
# The worker threads will exit when the queue is closed
|
|
585
|
+
nil
|
|
586
|
+
end
|
|
587
|
+
|
|
519
588
|
def next_message_finished!
|
|
520
589
|
next_msg = @next_msg
|
|
521
590
|
if next_msg.is_a? ReturnMessage
|
|
@@ -528,7 +597,7 @@ module AMQP
|
|
|
528
597
|
@basic_gets.push next_msg
|
|
529
598
|
else
|
|
530
599
|
Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
|
|
531
|
-
consumer.push next_msg
|
|
600
|
+
consumer.msg_q.push next_msg
|
|
532
601
|
end
|
|
533
602
|
nil
|
|
534
603
|
ensure
|
|
@@ -544,7 +613,7 @@ module AMQP
|
|
|
544
613
|
def expect(expected_frame_type)
|
|
545
614
|
frame_type, *args = @replies.pop
|
|
546
615
|
raise Error::Closed.new(@id, *@closed) if frame_type.nil?
|
|
547
|
-
raise Error::
|
|
616
|
+
raise Error::UnexpectedFrameType.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
|
|
548
617
|
|
|
549
618
|
args
|
|
550
619
|
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AMQP
|
|
4
|
+
class Client
|
|
5
|
+
# Configuration for AMQP::Client
|
|
6
|
+
# @!attribute strict_coding
|
|
7
|
+
# @return [Boolean] Whether to raise on unknown codecs
|
|
8
|
+
# @!attribute default_content_type
|
|
9
|
+
# @return [String, nil] Default content type for published messages
|
|
10
|
+
# @!attribute default_content_encoding
|
|
11
|
+
# @return [String, nil] Default content encoding for published messages
|
|
12
|
+
class Configuration
|
|
13
|
+
attr_accessor :strict_coding, :default_content_type, :default_content_encoding
|
|
14
|
+
attr_reader :codec_registry
|
|
15
|
+
|
|
16
|
+
# Initialize a new configuration
|
|
17
|
+
# @param codec_registry [MessageCodecRegistry] The codec registry to use
|
|
18
|
+
def initialize(codec_registry)
|
|
19
|
+
@codec_registry = codec_registry
|
|
20
|
+
@strict_coding = false
|
|
21
|
+
@default_content_type = nil
|
|
22
|
+
@default_content_encoding = nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Enable all built-in codecs (parsers and coders) for automatic message encoding/decoding
|
|
26
|
+
# This is a convenience method that delegates to the codec registry
|
|
27
|
+
# @return [self]
|
|
28
|
+
def enable_builtin_codecs
|
|
29
|
+
codec_registry.enable_builtin_codecs
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Enable built-in parsers for common content types
|
|
34
|
+
# @return [self]
|
|
35
|
+
def enable_builtin_parsers
|
|
36
|
+
codec_registry.enable_builtin_parsers
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Enable built-in coders for common content encodings
|
|
41
|
+
# @return [self]
|
|
42
|
+
def enable_builtin_coders
|
|
43
|
+
codec_registry.enable_builtin_coders
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Register a custom parser for a content type
|
|
48
|
+
# @param content_type [String] The content_type to match
|
|
49
|
+
# @param parser [Object] The parser object
|
|
50
|
+
# @return [self]
|
|
51
|
+
def register_parser(content_type:, parser:)
|
|
52
|
+
codec_registry.register_parser(content_type:, parser:)
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Register a custom coder for a content encoding
|
|
57
|
+
# @param content_encoding [String] The content_encoding to match
|
|
58
|
+
# @param coder [Object] The coder object
|
|
59
|
+
# @return [self]
|
|
60
|
+
def register_coder(content_encoding:, coder:)
|
|
61
|
+
codec_registry.register_coder(content_encoding:, coder:)
|
|
62
|
+
self
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
require "socket"
|
|
4
4
|
require "uri"
|
|
5
5
|
require "openssl"
|
|
6
|
-
require_relative "
|
|
7
|
-
require_relative "
|
|
8
|
-
require_relative "
|
|
6
|
+
require_relative "frame_bytes"
|
|
7
|
+
require_relative "channel"
|
|
8
|
+
require_relative "errors"
|
|
9
9
|
|
|
10
10
|
module AMQP
|
|
11
11
|
class Client
|
|
@@ -15,6 +15,8 @@ module AMQP
|
|
|
15
15
|
# @param uri [String] URL on the format amqp://username:password@hostname/vhost, use amqps:// for encrypted connection
|
|
16
16
|
# @param read_loop_thread [Boolean] If true run {#read_loop} in a background thread,
|
|
17
17
|
# otherwise the user have to run it explicitly, without {#read_loop} the connection won't function
|
|
18
|
+
# @param codec_registry [MessageCodecRegistry] Registry for message codecs
|
|
19
|
+
# @param strict_coding [Boolean] Whether to raise errors on unsupported codecs
|
|
18
20
|
# @option options [Boolean] connection_name (PROGRAM_NAME) Set a name for the connection to be able to identify
|
|
19
21
|
# the client from the broker
|
|
20
22
|
# @option options [Boolean] verify_peer (true) Verify broker's TLS certificate, set to false for self-signed certs
|
|
@@ -26,7 +28,7 @@ module AMQP
|
|
|
26
28
|
# Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
|
|
27
29
|
# @option options [String] keepalive (60:10:3) TCP keepalive setting, 60s idle, 10s interval between probes, 3 probes
|
|
28
30
|
# @return [Connection]
|
|
29
|
-
def initialize(uri = "", read_loop_thread: true, **options)
|
|
31
|
+
def initialize(uri = "", read_loop_thread: true, codec_registry: nil, strict_coding: false, **options)
|
|
30
32
|
uri = URI.parse(uri)
|
|
31
33
|
tls = uri.scheme == "amqps"
|
|
32
34
|
port = port_from_env || uri.port || (tls ? 5671 : 5672)
|
|
@@ -43,6 +45,8 @@ module AMQP
|
|
|
43
45
|
@channel_max = channel_max.zero? ? 65_536 : channel_max
|
|
44
46
|
@frame_max = frame_max
|
|
45
47
|
@heartbeat = heartbeat
|
|
48
|
+
@codec_registry = codec_registry
|
|
49
|
+
@strict_coding = strict_coding
|
|
46
50
|
@channels = {}
|
|
47
51
|
@channels_lock = Mutex.new
|
|
48
52
|
@closed = nil
|
|
@@ -71,14 +75,22 @@ module AMQP
|
|
|
71
75
|
# Alias for {#initialize}
|
|
72
76
|
# @see #initialize
|
|
73
77
|
# @deprecated
|
|
74
|
-
def self.connect(uri, read_loop_thread: true, **
|
|
75
|
-
new(uri, read_loop_thread
|
|
78
|
+
def self.connect(uri, read_loop_thread: true, **)
|
|
79
|
+
new(uri, read_loop_thread:, **)
|
|
76
80
|
end
|
|
77
81
|
|
|
78
82
|
# The max frame size negotiated between the client and the broker
|
|
79
83
|
# @return [Integer]
|
|
80
84
|
attr_reader :frame_max
|
|
81
85
|
|
|
86
|
+
# The codec registry for message encoding/decoding
|
|
87
|
+
# @return [MessageCodecRegistry, nil]
|
|
88
|
+
attr_reader :codec_registry
|
|
89
|
+
|
|
90
|
+
# Whether to use strict coding (raise errors on unsupported codecs)
|
|
91
|
+
# @return [Boolean]
|
|
92
|
+
attr_reader :strict_coding
|
|
93
|
+
|
|
82
94
|
# Custom inspect
|
|
83
95
|
# @return [String]
|
|
84
96
|
# @api private
|
|
@@ -91,6 +103,7 @@ module AMQP
|
|
|
91
103
|
# @return [Channel]
|
|
92
104
|
def channel(id = nil)
|
|
93
105
|
raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
|
|
106
|
+
|
|
94
107
|
raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
|
|
95
108
|
|
|
96
109
|
ch = @channels_lock.synchronize do
|
|
@@ -142,7 +155,7 @@ module AMQP
|
|
|
142
155
|
# @param secret [String] The new secret
|
|
143
156
|
# @param reason [String] A reason to update it
|
|
144
157
|
# @return [nil]
|
|
145
|
-
def update_secret(secret, reason)
|
|
158
|
+
def update_secret(secret, reason:)
|
|
146
159
|
write_bytes FrameBytes.update_secret(secret, reason)
|
|
147
160
|
expect(:update_secret_ok)
|
|
148
161
|
nil
|
|
@@ -202,14 +215,15 @@ module AMQP
|
|
|
202
215
|
loop do
|
|
203
216
|
socket.read(7, frame_start) || raise(IOError)
|
|
204
217
|
type, channel_id, frame_size = frame_start.unpack("C S> L>")
|
|
205
|
-
frame_max >= frame_size ||
|
|
218
|
+
frame_max >= frame_size ||
|
|
219
|
+
raise(Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}")
|
|
206
220
|
|
|
207
221
|
# read the frame content
|
|
208
222
|
socket.read(frame_size, frame_buffer) || raise(IOError)
|
|
209
223
|
|
|
210
224
|
# make sure that the frame end is correct
|
|
211
225
|
frame_end = socket.readchar.ord
|
|
212
|
-
raise Error::
|
|
226
|
+
raise Error::UnexpectedFrameTypeEnd, frame_end if frame_end != 206
|
|
213
227
|
|
|
214
228
|
# parse the frame, will return false if a close frame was received
|
|
215
229
|
parse_frame(type, channel_id, frame_buffer) || return
|
|
@@ -285,9 +299,11 @@ module AMQP
|
|
|
285
299
|
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
|
286
300
|
reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
|
|
287
301
|
classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
302
|
+
@channels_lock.synchronize do
|
|
303
|
+
channel = @channels.delete(channel_id)
|
|
304
|
+
channel.closed!(:channel, reply_code, reply_text, classid, methodid)
|
|
305
|
+
write_bytes FrameBytes.channel_close_ok(channel_id)
|
|
306
|
+
end
|
|
291
307
|
when 41 # channel#close-ok
|
|
292
308
|
channel = @channels_lock.synchronize { @channels.delete(channel_id) }
|
|
293
309
|
channel.reply [:channel_close_ok]
|
|
@@ -336,7 +352,7 @@ module AMQP
|
|
|
336
352
|
tag_len = buf.getbyte(4)
|
|
337
353
|
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
|
338
354
|
no_wait = buf.getbyte(5 + tag_len) == 1
|
|
339
|
-
channel.
|
|
355
|
+
channel.cancel_consumer(tag)
|
|
340
356
|
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait
|
|
341
357
|
when 31 # cancel-ok
|
|
342
358
|
tag_len = buf.getbyte(4)
|
|
@@ -470,7 +486,7 @@ module AMQP
|
|
|
470
486
|
|
|
471
487
|
raise Error, "Connection closed while waiting for #{expected_frame_type}"
|
|
472
488
|
end
|
|
473
|
-
frame_type == expected_frame_type || raise(Error::
|
|
489
|
+
frame_type == expected_frame_type || raise(Error::UnexpectedFrameType.new(expected_frame_type, frame_type))
|
|
474
490
|
args
|
|
475
491
|
end
|
|
476
492
|
|
|
@@ -479,7 +495,8 @@ module AMQP
|
|
|
479
495
|
# @return [OpenSSL::SSL::SSLSocket]
|
|
480
496
|
def open_socket(host, port, tls, options)
|
|
481
497
|
connect_timeout = options.fetch(:connect_timeout, 30).to_f
|
|
482
|
-
socket = Socket.tcp
|
|
498
|
+
socket = Socket.tcp(host, port, connect_timeout:)
|
|
499
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
483
500
|
keepalive = options.fetch(:keepalive, "").split(":", 3).map!(&:to_i)
|
|
484
501
|
enable_tcp_keepalive(socket, *keepalive)
|
|
485
502
|
if tls
|
|
@@ -515,7 +532,7 @@ module AMQP
|
|
|
515
532
|
|
|
516
533
|
type, channel_id, frame_size = buf.unpack("C S> L>")
|
|
517
534
|
frame_end = buf.getbyte(frame_size + 7)
|
|
518
|
-
raise Error::
|
|
535
|
+
raise Error::UnexpectedFrameTypeEnd, frame_end if frame_end != 206
|
|
519
536
|
|
|
520
537
|
case type
|
|
521
538
|
when 1 # method frame
|
|
@@ -558,7 +575,7 @@ module AMQP
|
|
|
558
575
|
rescue *READ_EXCEPTIONS
|
|
559
576
|
nil
|
|
560
577
|
end
|
|
561
|
-
raise e
|
|
578
|
+
raise Error, "Could not establish AMQP connection: invalid handshake (#{e.message})"
|
|
562
579
|
end
|
|
563
580
|
|
|
564
581
|
# Enable TCP keepalive, which is preferred to heartbeats
|
|
@@ -583,7 +600,7 @@ module AMQP
|
|
|
583
600
|
# @return [Integer] A port number
|
|
584
601
|
# @return [nil] When the environment variable AMQP_PORT isn't set
|
|
585
602
|
def port_from_env
|
|
586
|
-
return unless (port = ENV
|
|
603
|
+
return unless (port = ENV.fetch("AMQP_PORT", nil))
|
|
587
604
|
|
|
588
605
|
port.to_i
|
|
589
606
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AMQP
|
|
4
|
+
class Client
|
|
5
|
+
# Consumer abstraction
|
|
6
|
+
class Consumer
|
|
7
|
+
attr_reader :queue, :id, :channel_id, :prefetch, :block, :basic_consume_args
|
|
8
|
+
|
|
9
|
+
# @api private
|
|
10
|
+
def initialize(client:, channel_id:, id:, block:, **settings)
|
|
11
|
+
@client = client
|
|
12
|
+
@channel_id = channel_id
|
|
13
|
+
@id = id
|
|
14
|
+
@queue = settings.fetch(:queue)
|
|
15
|
+
@basic_consume_args = settings.fetch(:basic_consume_args)
|
|
16
|
+
@prefetch = settings.fetch(:prefetch)
|
|
17
|
+
@consume_ok = settings.fetch(:consume_ok)
|
|
18
|
+
@block = block
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Cancel the consumer
|
|
22
|
+
# @return [self]
|
|
23
|
+
def cancel
|
|
24
|
+
@client.cancel_consumer(self)
|
|
25
|
+
self
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# True if the consumer is cancelled/closed
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def closed?
|
|
31
|
+
@consume_ok.msg_q.closed?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Return the consumer tag
|
|
35
|
+
# @return [String]
|
|
36
|
+
def tag
|
|
37
|
+
@consume_ok.consumer_tag
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Update the consumer with new metadata after reconnection
|
|
41
|
+
# @api private
|
|
42
|
+
def update_consume_ok(consume_ok)
|
|
43
|
+
@consume_ok = consume_ok
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|