amqp-client 1.2.1 → 2.0.1
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 +344 -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
data/lib/amqp/client.rb
CHANGED
|
@@ -4,6 +4,11 @@ require_relative "client/version"
|
|
|
4
4
|
require_relative "client/connection"
|
|
5
5
|
require_relative "client/exchange"
|
|
6
6
|
require_relative "client/queue"
|
|
7
|
+
require_relative "client/consumer"
|
|
8
|
+
require_relative "client/rpc_client"
|
|
9
|
+
require_relative "client/message_codecs"
|
|
10
|
+
require_relative "client/message_codec_registry"
|
|
11
|
+
require_relative "client/configuration"
|
|
7
12
|
|
|
8
13
|
# AMQP 0-9-1 Protocol, this library only implements the Client
|
|
9
14
|
# @see Client
|
|
@@ -11,6 +16,11 @@ module AMQP
|
|
|
11
16
|
# AMQP 0-9-1 Client
|
|
12
17
|
# @see Connection
|
|
13
18
|
class Client
|
|
19
|
+
# Class-level codec registry
|
|
20
|
+
@codec_registry = MessageCodecRegistry.new
|
|
21
|
+
# Class-level configuration
|
|
22
|
+
@config = Configuration.new(@codec_registry)
|
|
23
|
+
|
|
14
24
|
# Create a new Client object, this won't establish a connection yet, use {#connect} or {#start} for that
|
|
15
25
|
# @param uri [String] URL on the format amqp://username:password@hostname/vhost,
|
|
16
26
|
# use amqps:// for encrypted connection
|
|
@@ -20,15 +30,23 @@ module AMQP
|
|
|
20
30
|
# @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
|
|
21
31
|
# @option options [Integer] frame_max (131_072) Maximum frame size,
|
|
22
32
|
# the smallest of the client's and the broker's values will be used
|
|
23
|
-
# @option options [Integer] channel_max (2048)
|
|
24
|
-
#
|
|
33
|
+
# @option options [Integer] channel_max (2048) Maximum number of channels the client will be allowed to have open.
|
|
34
|
+
# Maximum allowed is 65_536. The smallest of the client's and the broker's value will be used.
|
|
25
35
|
def initialize(uri = "", **options)
|
|
26
36
|
@uri = uri
|
|
27
37
|
@options = options
|
|
28
38
|
@queues = {}
|
|
29
39
|
@exchanges = {}
|
|
30
|
-
@
|
|
40
|
+
@consumers = {}
|
|
41
|
+
@next_consumer_id = 0
|
|
31
42
|
@connq = SizedQueue.new(1)
|
|
43
|
+
@codec_registry = self.class.codec_registry.dup
|
|
44
|
+
@strict_coding = self.class.config.strict_coding
|
|
45
|
+
@default_content_encoding = self.class.config.default_content_encoding
|
|
46
|
+
@default_content_type = self.class.config.default_content_type
|
|
47
|
+
@start_lock = Mutex.new
|
|
48
|
+
@supervisor_started = false
|
|
49
|
+
@stopped = false
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
# @!group Connect and disconnect
|
|
@@ -39,7 +57,7 @@ module AMQP
|
|
|
39
57
|
# @example
|
|
40
58
|
# connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
|
|
41
59
|
def connect(read_loop_thread: true)
|
|
42
|
-
Connection.new(@uri, read_loop_thread:
|
|
60
|
+
Connection.new(@uri, read_loop_thread:, codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
|
|
43
61
|
end
|
|
44
62
|
|
|
45
63
|
# Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
|
|
@@ -49,38 +67,53 @@ module AMQP
|
|
|
49
67
|
# amqp.start
|
|
50
68
|
# amqp.queue("foobar")
|
|
51
69
|
def start
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
return self if started?
|
|
71
|
+
|
|
72
|
+
@start_lock.synchronize do # rubocop:disable Metrics/BlockLength
|
|
73
|
+
return self if started?
|
|
74
|
+
|
|
75
|
+
@supervisor_started = true
|
|
76
|
+
@stopped = false
|
|
77
|
+
Thread.new(connect(read_loop_thread: false)) do |conn|
|
|
78
|
+
Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
|
|
79
|
+
loop do
|
|
80
|
+
break if @stopped
|
|
81
|
+
|
|
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? }
|
|
66
99
|
end
|
|
67
|
-
|
|
100
|
+
conn.read_loop # blocks until connection is closed, then reconnect
|
|
101
|
+
rescue Error => e
|
|
102
|
+
warn "AMQP-Client reconnect error: #{e.inspect}"
|
|
103
|
+
sleep @options[:reconnect_interval] || 1
|
|
104
|
+
ensure
|
|
105
|
+
@connq.clear
|
|
106
|
+
conn = nil
|
|
68
107
|
end
|
|
69
|
-
conn.read_loop # blocks until connection is closed, then reconnect
|
|
70
|
-
rescue Error => e
|
|
71
|
-
warn "AMQP-Client reconnect error: #{e.inspect}"
|
|
72
|
-
sleep @options[:reconnect_interval] || 1
|
|
73
|
-
ensure
|
|
74
|
-
conn = nil
|
|
75
108
|
end
|
|
76
109
|
end
|
|
77
110
|
self
|
|
78
111
|
end
|
|
79
112
|
|
|
80
|
-
# Close the currently open connection
|
|
113
|
+
# Close the currently open connection and stop the supervision / reconnection logic.
|
|
81
114
|
# @return [nil]
|
|
82
115
|
def stop
|
|
83
|
-
return if @stopped
|
|
116
|
+
return if @stopped && !@supervisor_started
|
|
84
117
|
|
|
85
118
|
@stopped = true
|
|
86
119
|
return unless @connq.size.positive?
|
|
@@ -90,6 +123,12 @@ module AMQP
|
|
|
90
123
|
nil
|
|
91
124
|
end
|
|
92
125
|
|
|
126
|
+
# Check if the client is connected
|
|
127
|
+
# @return [Boolean] true if connected or currently trying to connect, false otherwise
|
|
128
|
+
def started?
|
|
129
|
+
@supervisor_started && !@stopped
|
|
130
|
+
end
|
|
131
|
+
|
|
93
132
|
# @!endgroup
|
|
94
133
|
# @!group High level objects
|
|
95
134
|
|
|
@@ -99,18 +138,20 @@ module AMQP
|
|
|
99
138
|
# messages in the queue will only survive if they are published as persistent
|
|
100
139
|
# @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
|
|
101
140
|
# (it won't be deleted until at least one consumer has consumed from it)
|
|
141
|
+
# @param exclusive [Boolean] If true the queue will be deleted when the connection is closed
|
|
142
|
+
# @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
|
|
102
143
|
# @param arguments [Hash] Custom arguments, such as queue-ttl etc.
|
|
103
144
|
# @return [Queue]
|
|
104
145
|
# @example
|
|
105
146
|
# amqp = AMQP::Client.new.start
|
|
106
147
|
# q = amqp.queue("foobar")
|
|
107
148
|
# q.publish("body")
|
|
108
|
-
def queue(name, durable: true, auto_delete: false, arguments: {})
|
|
149
|
+
def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
|
|
109
150
|
raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
|
|
110
151
|
|
|
111
152
|
@queues.fetch(name) do
|
|
112
153
|
with_connection do |conn|
|
|
113
|
-
conn.channel(1).queue_declare(name, durable
|
|
154
|
+
conn.channel(1).queue_declare(name, durable:, auto_delete:, exclusive:, passive:, arguments:)
|
|
114
155
|
end
|
|
115
156
|
@queues[name] = Queue.new(self, name)
|
|
116
157
|
end
|
|
@@ -126,66 +167,104 @@ module AMQP
|
|
|
126
167
|
# @return [Exchange]
|
|
127
168
|
# @example
|
|
128
169
|
# amqp = AMQP::Client.new.start
|
|
129
|
-
# x = amqp.exchange("my.hash.exchange", "x-consistent-hash")
|
|
130
|
-
# x.publish("body", "routing-key")
|
|
131
|
-
def exchange(name, type
|
|
170
|
+
# x = amqp.exchange("my.hash.exchange", type: "x-consistent-hash")
|
|
171
|
+
# x.publish("body", routing_key: "routing-key")
|
|
172
|
+
def exchange(name, type:, durable: true, auto_delete: false, internal: false, arguments: {})
|
|
132
173
|
@exchanges.fetch(name) do
|
|
133
174
|
with_connection do |conn|
|
|
134
|
-
conn.channel(1).exchange_declare(name, type
|
|
135
|
-
internal: internal, arguments: arguments)
|
|
175
|
+
conn.channel(1).exchange_declare(name, type:, durable:, auto_delete:, internal:, arguments:)
|
|
136
176
|
end
|
|
137
177
|
@exchanges[name] = Exchange.new(self, name)
|
|
138
178
|
end
|
|
139
179
|
end
|
|
140
180
|
|
|
141
|
-
# Declare a fanout exchange and return a high level Exchange object
|
|
142
|
-
# @param name [String] Name of the exchange (defaults to "amq.fanout")
|
|
143
|
-
# @see {#exchange} for other parameters
|
|
144
|
-
# @return [Exchange]
|
|
145
|
-
def fanout(name = "amq.fanout", **kwargs)
|
|
146
|
-
exchange(name, "fanout", **kwargs)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
181
|
# Declare a direct exchange and return a high level Exchange object
|
|
150
|
-
# @param name [String] Name of the exchange (defaults to ""
|
|
151
|
-
# @see
|
|
182
|
+
# @param name [String] Name of the exchange (defaults to "amq.direct")
|
|
183
|
+
# @see #exchange for other parameters
|
|
152
184
|
# @return [Exchange]
|
|
153
|
-
def
|
|
154
|
-
return exchange(name, "direct", **
|
|
185
|
+
def direct_exchange(name = "amq.direct", **)
|
|
186
|
+
return exchange(name, type: "direct", **) unless name.empty?
|
|
155
187
|
|
|
188
|
+
# Return the default exchange
|
|
156
189
|
@exchanges.fetch(name) do
|
|
157
190
|
@exchanges[name] = Exchange.new(self, name)
|
|
158
191
|
end
|
|
159
192
|
end
|
|
160
193
|
|
|
194
|
+
# @deprecated
|
|
195
|
+
# @see #direct_exchange
|
|
196
|
+
alias direct direct_exchange
|
|
197
|
+
|
|
198
|
+
# Return a high level Exchange object for the default direct exchange
|
|
199
|
+
# @see #direct for parameters
|
|
200
|
+
# @return [Exchange]
|
|
201
|
+
def default_exchange(**)
|
|
202
|
+
direct("", **)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# @deprecated
|
|
206
|
+
# @see #default_exchange
|
|
207
|
+
alias default default_exchange
|
|
208
|
+
|
|
209
|
+
# Declare a fanout exchange and return a high level Exchange object
|
|
210
|
+
# @param name [String] Name of the exchange (defaults to "amq.fanout")
|
|
211
|
+
# @see #exchange for other parameters
|
|
212
|
+
# @return [Exchange]
|
|
213
|
+
def fanout_exchange(name = "amq.fanout", **)
|
|
214
|
+
exchange(name, type: "fanout", **)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @deprecated
|
|
218
|
+
# @see #fanout_exchange
|
|
219
|
+
alias fanout fanout_exchange
|
|
220
|
+
|
|
161
221
|
# Declare a topic exchange and return a high level Exchange object
|
|
162
222
|
# @param name [String] Name of the exchange (defaults to "amq.topic")
|
|
163
|
-
# @see
|
|
223
|
+
# @see #exchange for other parameters
|
|
164
224
|
# @return [Exchange]
|
|
165
|
-
def
|
|
166
|
-
exchange(name, "topic", **
|
|
225
|
+
def topic_exchange(name = "amq.topic", **)
|
|
226
|
+
exchange(name, type: "topic", **)
|
|
167
227
|
end
|
|
168
228
|
|
|
229
|
+
# @deprecated
|
|
230
|
+
# @see #topic_exchange
|
|
231
|
+
alias topic topic_exchange
|
|
232
|
+
|
|
169
233
|
# Declare a headers exchange and return a high level Exchange object
|
|
170
234
|
# @param name [String] Name of the exchange (defaults to "amq.headers")
|
|
171
|
-
# @see
|
|
235
|
+
# @see #exchange for other parameters
|
|
172
236
|
# @return [Exchange]
|
|
173
|
-
def
|
|
174
|
-
exchange(name, "headers", **
|
|
237
|
+
def headers_exchange(name = "amq.headers", **)
|
|
238
|
+
exchange(name, type: "headers", **)
|
|
175
239
|
end
|
|
176
240
|
|
|
241
|
+
# @deprecated
|
|
242
|
+
# @see #headers_exchange
|
|
243
|
+
alias headers headers_exchange
|
|
244
|
+
|
|
177
245
|
# @!endgroup
|
|
178
246
|
# @!group Publish
|
|
179
247
|
|
|
180
248
|
# Publish a (persistent) message and wait for confirmation
|
|
181
|
-
# @param
|
|
249
|
+
# @param body [Object] The message body
|
|
250
|
+
# will be encoded if any matching codec is found in the client's codec registry
|
|
251
|
+
# @param exchange [String] Name of the exchange to publish to
|
|
252
|
+
# @param routing_key [String] Routing key for the message
|
|
182
253
|
# @option (see Connection::Channel#basic_publish_confirm)
|
|
183
|
-
# @return
|
|
254
|
+
# @return [nil]
|
|
184
255
|
# @raise (see Connection::Channel#basic_publish_confirm)
|
|
185
|
-
|
|
256
|
+
# @raise [Error::PublishNotConfirmed] If the message was not confirmed by the broker
|
|
257
|
+
# @raise [Error::UnsupportedContentType] If content type is unsupported
|
|
258
|
+
# @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
|
|
259
|
+
def publish(body, exchange:, routing_key: "", **properties)
|
|
186
260
|
with_connection do |conn|
|
|
187
|
-
properties
|
|
188
|
-
|
|
261
|
+
properties[:delivery_mode] ||= 2
|
|
262
|
+
properties = default_content_properties.merge(properties)
|
|
263
|
+
body = serialize_and_encode_body(body, properties)
|
|
264
|
+
result = conn.channel(1).basic_publish_confirm(body, exchange:, routing_key:, **properties)
|
|
265
|
+
raise Error::PublishNotConfirmed unless result
|
|
266
|
+
|
|
267
|
+
nil
|
|
189
268
|
end
|
|
190
269
|
end
|
|
191
270
|
|
|
@@ -194,10 +273,14 @@ module AMQP
|
|
|
194
273
|
# @option (see Connection::Channel#basic_publish)
|
|
195
274
|
# @return (see Connection::Channel#basic_publish)
|
|
196
275
|
# @raise (see Connection::Channel#basic_publish)
|
|
197
|
-
|
|
276
|
+
# @raise [Error::UnsupportedContentType] If content type is unsupported
|
|
277
|
+
# @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
|
|
278
|
+
def publish_and_forget(body, exchange:, routing_key: "", **properties)
|
|
198
279
|
with_connection do |conn|
|
|
199
|
-
properties
|
|
200
|
-
|
|
280
|
+
properties[:delivery_mode] ||= 2
|
|
281
|
+
properties = default_content_properties.merge(properties)
|
|
282
|
+
body = serialize_and_encode_body(body, properties)
|
|
283
|
+
conn.channel(1).basic_publish(body, exchange:, routing_key:, **properties)
|
|
201
284
|
end
|
|
202
285
|
end
|
|
203
286
|
|
|
@@ -209,7 +292,6 @@ module AMQP
|
|
|
209
292
|
end
|
|
210
293
|
end
|
|
211
294
|
|
|
212
|
-
# @!endgroup
|
|
213
295
|
# @!group Queue actions
|
|
214
296
|
|
|
215
297
|
# Consume messages from a queue
|
|
@@ -217,19 +299,41 @@ module AMQP
|
|
|
217
299
|
# @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected) (default: false)
|
|
218
300
|
# @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false (default: 1)
|
|
219
301
|
# @param worker_threads [Integer] Number of threads processing messages (default: 1)
|
|
302
|
+
# @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
|
|
303
|
+
# The proc will be called with the consumer tag as the only argument
|
|
220
304
|
# @param arguments [Hash] Custom arguments to the consumer
|
|
221
305
|
# @yield [Message] Delivered message from the queue
|
|
222
|
-
# @return [
|
|
223
|
-
|
|
224
|
-
|
|
306
|
+
# @return [Consumer] The consumer object, which can be used to cancel the consumer
|
|
307
|
+
def subscribe(queue, exclusive: false, no_ack: false, prefetch: 1, worker_threads: 1,
|
|
308
|
+
on_cancel: nil, arguments: {}, &blk)
|
|
225
309
|
raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
|
|
226
310
|
|
|
227
|
-
@subscriptions.add? [queue, no_ack, prefetch, worker_threads, arguments, blk]
|
|
228
|
-
|
|
229
311
|
with_connection do |conn|
|
|
230
312
|
ch = conn.channel
|
|
231
313
|
ch.basic_qos(prefetch)
|
|
232
|
-
|
|
314
|
+
consumer_id = @next_consumer_id += 1
|
|
315
|
+
on_cancel_proc = proc do |tag|
|
|
316
|
+
@consumers.delete(consumer_id)
|
|
317
|
+
on_cancel&.call(tag)
|
|
318
|
+
end
|
|
319
|
+
basic_consume_args = { exclusive:, no_ack:, worker_threads:, on_cancel: on_cancel_proc, arguments: }
|
|
320
|
+
consume_ok = ch.basic_consume(queue, **basic_consume_args, &blk)
|
|
321
|
+
consumer = Consumer.new(client: self, channel_id: ch.id, id: consumer_id, block: blk,
|
|
322
|
+
queue:, consume_ok:, prefetch:, basic_consume_args:)
|
|
323
|
+
@consumers[consumer_id] = consumer
|
|
324
|
+
consumer
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Get a message from a queue
|
|
329
|
+
# @param queue [String] Name of the queue to get the message from
|
|
330
|
+
# @param no_ack [Boolean] When false the message has to be manually acknowledged (or rejected) (default: false)
|
|
331
|
+
# @return [Message, nil] The message from the queue or nil if the queue is empty
|
|
332
|
+
def get(queue, no_ack: false)
|
|
333
|
+
with_connection do |conn|
|
|
334
|
+
conn.with_channel do |ch|
|
|
335
|
+
ch.basic_get(queue, no_ack:)
|
|
336
|
+
end
|
|
233
337
|
end
|
|
234
338
|
end
|
|
235
339
|
|
|
@@ -239,9 +343,9 @@ module AMQP
|
|
|
239
343
|
# @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
|
|
240
344
|
# @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
|
|
241
345
|
# @return [nil]
|
|
242
|
-
def bind(queue
|
|
346
|
+
def bind(queue:, exchange:, binding_key: "", arguments: {})
|
|
243
347
|
with_connection do |conn|
|
|
244
|
-
conn.channel(1).queue_bind(queue, exchange
|
|
348
|
+
conn.channel(1).queue_bind(queue, exchange:, binding_key:, arguments:)
|
|
245
349
|
end
|
|
246
350
|
end
|
|
247
351
|
|
|
@@ -251,9 +355,9 @@ module AMQP
|
|
|
251
355
|
# @param binding_key [String] Binding key which the queue is bound to the exchange with
|
|
252
356
|
# @param arguments [Hash] Arguments matching the binding that's being removed
|
|
253
357
|
# @return [nil]
|
|
254
|
-
def unbind(queue
|
|
358
|
+
def unbind(queue:, exchange:, binding_key: "", arguments: {})
|
|
255
359
|
with_connection do |conn|
|
|
256
|
-
conn.channel(1).queue_unbind(queue, exchange
|
|
360
|
+
conn.channel(1).queue_unbind(queue, exchange:, binding_key:, arguments:)
|
|
257
361
|
end
|
|
258
362
|
end
|
|
259
363
|
|
|
@@ -273,7 +377,7 @@ module AMQP
|
|
|
273
377
|
# @return [Integer] Number of messages in the queue when deleted
|
|
274
378
|
def delete_queue(name, if_unused: false, if_empty: false)
|
|
275
379
|
with_connection do |conn|
|
|
276
|
-
msgs = conn.channel(1).queue_delete(name, if_unused
|
|
380
|
+
msgs = conn.channel(1).queue_delete(name, if_unused:, if_empty:)
|
|
277
381
|
@queues.delete(name)
|
|
278
382
|
msgs
|
|
279
383
|
end
|
|
@@ -283,26 +387,26 @@ module AMQP
|
|
|
283
387
|
# @!group Exchange actions
|
|
284
388
|
|
|
285
389
|
# Bind an exchange to an exchange
|
|
286
|
-
# @param destination [String] Name of the exchange to bind
|
|
287
390
|
# @param source [String] Name of the exchange to bind to
|
|
391
|
+
# @param destination [String] Name of the exchange to bind
|
|
288
392
|
# @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
|
|
289
393
|
# @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
|
|
290
394
|
# @return [nil]
|
|
291
|
-
def exchange_bind(destination
|
|
395
|
+
def exchange_bind(source:, destination:, binding_key: "", arguments: {})
|
|
292
396
|
with_connection do |conn|
|
|
293
|
-
conn.channel(1).exchange_bind(destination
|
|
397
|
+
conn.channel(1).exchange_bind(destination:, source:, binding_key:, arguments:)
|
|
294
398
|
end
|
|
295
399
|
end
|
|
296
400
|
|
|
297
401
|
# Unbind an exchange from an exchange
|
|
298
|
-
# @param destination [String] Name of the exchange to unbind
|
|
299
402
|
# @param source [String] Name of the exchange to unbind from
|
|
403
|
+
# @param destination [String] Name of the exchange to unbind
|
|
300
404
|
# @param binding_key [String] Binding key which the exchange is bound to the exchange with
|
|
301
405
|
# @param arguments [Hash] Arguments matching the binding that's being removed
|
|
302
406
|
# @return [nil]
|
|
303
|
-
def exchange_unbind(destination
|
|
407
|
+
def exchange_unbind(source:, destination:, binding_key: "", arguments: {})
|
|
304
408
|
with_connection do |conn|
|
|
305
|
-
conn.channel(1).exchange_unbind(destination
|
|
409
|
+
conn.channel(1).exchange_unbind(destination:, source:, binding_key:, arguments:)
|
|
306
410
|
end
|
|
307
411
|
end
|
|
308
412
|
|
|
@@ -318,9 +422,122 @@ module AMQP
|
|
|
318
422
|
end
|
|
319
423
|
|
|
320
424
|
# @!endgroup
|
|
425
|
+
# @!group RPC
|
|
321
426
|
|
|
322
|
-
|
|
427
|
+
# Create a RPC server for a single method/function/procedure
|
|
428
|
+
# @param method [String, Symbol] name of the RPC method to host (i.e. queue name on the server side)
|
|
429
|
+
# @param worker_threads [Integer] number of threads that process requests
|
|
430
|
+
# @param durable [Boolean] If true the queue will survive broker restarts
|
|
431
|
+
# @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
|
|
432
|
+
# (it won't be deleted until at least one consumer has consumed from it)
|
|
433
|
+
# @param arguments [Hash] Custom arguments, such as queue-ttl etc.
|
|
434
|
+
# @yield Block that processes the RPC request messages
|
|
435
|
+
# @yieldparam [String] The body of the request message
|
|
436
|
+
# @yieldreturn [String] The response message body
|
|
437
|
+
# @return (see #subscribe)
|
|
438
|
+
def rpc_server(method, worker_threads: 1, durable: true, auto_delete: false, arguments: {}, &_)
|
|
439
|
+
queue(method.to_s, durable:, auto_delete:, arguments:)
|
|
440
|
+
.subscribe(prefetch: worker_threads, worker_threads:) do |msg|
|
|
441
|
+
result = yield msg.parse
|
|
442
|
+
properties = { content_type: msg.properties.content_type,
|
|
443
|
+
content_encoding: msg.properties.content_encoding }
|
|
444
|
+
result_body = serialize_and_encode_body(result, properties)
|
|
445
|
+
|
|
446
|
+
msg.channel.basic_publish(result_body, exchange: "", routing_key: msg.properties.reply_to,
|
|
447
|
+
correlation_id: msg.properties.correlation_id, **properties)
|
|
448
|
+
msg.ack
|
|
449
|
+
rescue StandardError
|
|
450
|
+
msg.reject(requeue: false)
|
|
451
|
+
raise
|
|
452
|
+
end
|
|
453
|
+
end
|
|
323
454
|
|
|
455
|
+
# Do a RPC call, sends a messages, waits for a response
|
|
456
|
+
# @param method [String, Symbol] name of the RPC method to call (i.e. queue name on the server side)
|
|
457
|
+
# @param arguments [String] arguments/body to the call
|
|
458
|
+
# @param timeout [Numeric, nil] Number of seconds to wait for a response
|
|
459
|
+
# @option (see Client#publish)
|
|
460
|
+
# @return [String] Returns the result from the call
|
|
461
|
+
# @raise [Timeout::Error] if no response is received within the timeout period
|
|
462
|
+
def rpc_call(method, arguments, timeout: nil, **properties)
|
|
463
|
+
ch = with_connection(&:channel)
|
|
464
|
+
begin
|
|
465
|
+
msg = ch.basic_consume_once("amq.rabbitmq.reply-to", timeout:) do
|
|
466
|
+
properties = default_content_properties.merge(properties)
|
|
467
|
+
body = serialize_and_encode_body(arguments, properties)
|
|
468
|
+
ch.basic_publish(body, exchange: "", routing_key: method.to_s,
|
|
469
|
+
reply_to: "amq.rabbitmq.reply-to", **properties)
|
|
470
|
+
end
|
|
471
|
+
msg.parse
|
|
472
|
+
ensure
|
|
473
|
+
ch.close
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Create a reusable RPC client
|
|
478
|
+
# @return [RPCClient]
|
|
479
|
+
def rpc_client
|
|
480
|
+
ch = with_connection(&:channel)
|
|
481
|
+
RPCClient.new(ch).start
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# @!endgroup
|
|
485
|
+
# @!group Message coding
|
|
486
|
+
|
|
487
|
+
class << self
|
|
488
|
+
# Configure the AMQP::Client class-level settings
|
|
489
|
+
# @yield [Configuration] Yields the configuration object for modification
|
|
490
|
+
# @return [Configuration] The configuration object
|
|
491
|
+
# @example
|
|
492
|
+
# AMQP::Client.configure do |config|
|
|
493
|
+
# config.default_content_type = "application/json"
|
|
494
|
+
# config.strict_coding = true
|
|
495
|
+
# end
|
|
496
|
+
def configure
|
|
497
|
+
yield @config if block_given?
|
|
498
|
+
@config
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Get the class-level configuration
|
|
502
|
+
# @return [Configuration]
|
|
503
|
+
attr_reader :config
|
|
504
|
+
|
|
505
|
+
# Get the class-level codec registry
|
|
506
|
+
# @return [MessageCodecRegistry]
|
|
507
|
+
attr_reader :codec_registry
|
|
508
|
+
|
|
509
|
+
# We need to set the subclass's configuration and codec registry
|
|
510
|
+
# because these are class instance variables, hence not inherited.
|
|
511
|
+
# @api private
|
|
512
|
+
def inherited(subclass)
|
|
513
|
+
super
|
|
514
|
+
subclass_codec_registry = @codec_registry.dup
|
|
515
|
+
subclass.instance_variable_set(:@codec_registry, subclass_codec_registry)
|
|
516
|
+
subclass.instance_variable_set(:@config, Configuration.new(subclass_codec_registry))
|
|
517
|
+
# Copy configuration settings from parent
|
|
518
|
+
subclass.config.strict_coding = @config.strict_coding
|
|
519
|
+
subclass.config.default_content_type = @config.default_content_type
|
|
520
|
+
subclass.config.default_content_encoding = @config.default_content_encoding
|
|
521
|
+
end
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# Get the codec registry for this instance
|
|
525
|
+
# @return [MessageCodecRegistry]
|
|
526
|
+
attr_reader :codec_registry
|
|
527
|
+
|
|
528
|
+
# Get/set if condig should be strict, i.e. if the client should raise on unknown codecs
|
|
529
|
+
attr_accessor :strict_coding
|
|
530
|
+
|
|
531
|
+
# Get/set the default content_type to use when publishing messages
|
|
532
|
+
# @return [String, nil]
|
|
533
|
+
attr_accessor :default_content_type
|
|
534
|
+
|
|
535
|
+
# Get/set the default content_encoding to use when publishing messages
|
|
536
|
+
# @return [String, nil]
|
|
537
|
+
attr_accessor :default_content_encoding
|
|
538
|
+
|
|
539
|
+
# @!endgroup
|
|
540
|
+
#
|
|
324
541
|
def with_connection
|
|
325
542
|
conn = nil
|
|
326
543
|
loop do
|
|
@@ -335,5 +552,53 @@ module AMQP
|
|
|
335
552
|
@connq << conn unless conn.closed?
|
|
336
553
|
end
|
|
337
554
|
end
|
|
555
|
+
|
|
556
|
+
# @api private
|
|
557
|
+
def cancel_consumer(consumer)
|
|
558
|
+
@consumers.delete(consumer.id)
|
|
559
|
+
with_connection do |conn|
|
|
560
|
+
conn.channel(consumer.channel_id).basic_cancel(consumer.tag)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
private
|
|
565
|
+
|
|
566
|
+
def default_content_properties
|
|
567
|
+
{
|
|
568
|
+
content_type: @default_content_type,
|
|
569
|
+
content_encoding: @default_content_encoding
|
|
570
|
+
}.compact
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def serialize_and_encode_body(body, properties)
|
|
574
|
+
body = serialize_body(body, properties)
|
|
575
|
+
encode_body(body, properties)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def encode_body(body, properties)
|
|
579
|
+
ce = properties[:content_encoding]
|
|
580
|
+
coder = @codec_registry.find_coder(ce)
|
|
581
|
+
|
|
582
|
+
return coder.encode(body, properties) if coder
|
|
583
|
+
|
|
584
|
+
is_unsupported = ce && ce != ""
|
|
585
|
+
raise Error::UnsupportedContentEncoding, ce if is_unsupported && @strict_coding
|
|
586
|
+
|
|
587
|
+
body
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def serialize_body(body, properties)
|
|
591
|
+
return body if body.is_a?(String)
|
|
592
|
+
|
|
593
|
+
ct = properties[:content_type]
|
|
594
|
+
parser = @codec_registry.find_parser(ct)
|
|
595
|
+
|
|
596
|
+
return parser.serialize(body, properties) if parser
|
|
597
|
+
|
|
598
|
+
is_unsupported = ct && ct != "" && ct != "text/plain"
|
|
599
|
+
raise Error::UnsupportedContentType, ct if is_unsupported && @strict_coding
|
|
600
|
+
|
|
601
|
+
body.to_s
|
|
602
|
+
end
|
|
338
603
|
end
|
|
339
604
|
end
|
metadata
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: amqp-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- CloudAMQP
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
@@ -16,33 +16,22 @@ executables: []
|
|
|
16
16
|
extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
|
18
18
|
files:
|
|
19
|
-
- ".github/workflows/codeql-analysis.yml"
|
|
20
|
-
- ".github/workflows/docs.yml"
|
|
21
|
-
- ".github/workflows/main.yml"
|
|
22
|
-
- ".github/workflows/release.yml"
|
|
23
|
-
- ".gitignore"
|
|
24
|
-
- ".rubocop.yml"
|
|
25
|
-
- ".rubocop_todo.yml"
|
|
26
|
-
- ".yardopts"
|
|
27
|
-
- CHANGELOG.md
|
|
28
|
-
- CODEOWNERS
|
|
29
|
-
- Gemfile
|
|
30
19
|
- LICENSE.txt
|
|
31
|
-
- README.md
|
|
32
|
-
- Rakefile
|
|
33
|
-
- amqp-client.gemspec
|
|
34
|
-
- bin/console
|
|
35
|
-
- bin/setup
|
|
36
20
|
- lib/amqp-client.rb
|
|
37
21
|
- lib/amqp/client.rb
|
|
38
22
|
- lib/amqp/client/channel.rb
|
|
23
|
+
- lib/amqp/client/configuration.rb
|
|
39
24
|
- lib/amqp/client/connection.rb
|
|
25
|
+
- lib/amqp/client/consumer.rb
|
|
40
26
|
- lib/amqp/client/errors.rb
|
|
41
27
|
- lib/amqp/client/exchange.rb
|
|
42
28
|
- lib/amqp/client/frame_bytes.rb
|
|
43
29
|
- lib/amqp/client/message.rb
|
|
30
|
+
- lib/amqp/client/message_codec_registry.rb
|
|
31
|
+
- lib/amqp/client/message_codecs.rb
|
|
44
32
|
- lib/amqp/client/properties.rb
|
|
45
33
|
- lib/amqp/client/queue.rb
|
|
34
|
+
- lib/amqp/client/rpc_client.rb
|
|
46
35
|
- lib/amqp/client/table.rb
|
|
47
36
|
- lib/amqp/client/version.rb
|
|
48
37
|
homepage: https://github.com/cloudamqp/amqp-client.rb
|
|
@@ -52,6 +41,7 @@ metadata:
|
|
|
52
41
|
homepage_uri: https://github.com/cloudamqp/amqp-client.rb
|
|
53
42
|
source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
|
|
54
43
|
changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
55
45
|
rdoc_options: []
|
|
56
46
|
require_paths:
|
|
57
47
|
- lib
|