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.
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) Maxium number of channels the client will be allowed to have open.
24
- # Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
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
- @subscriptions = Set.new
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: read_loop_thread, **@options)
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
- @stopped = false
53
- Thread.new(connect(read_loop_thread: false)) do |conn|
54
- Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
55
- loop do
56
- break if @stopped
57
-
58
- conn ||= connect(read_loop_thread: false)
59
- Thread.new do
60
- # restore connection in another thread, read_loop have to run
61
- conn.channel(1) # reserve channel 1 for publishes
62
- @subscriptions.each do |queue_name, no_ack, prefetch, wt, args, blk|
63
- ch = conn.channel
64
- ch.basic_qos(prefetch)
65
- ch.basic_consume(queue_name, no_ack: no_ack, worker_threads: wt, arguments: args, &blk)
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
- @connq << conn
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: durable, auto_delete: auto_delete, arguments: arguments)
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, durable: true, auto_delete: false, internal: false, arguments: {})
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, durable: durable, auto_delete: auto_delete,
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 "" for the default direct exchange)
151
- # @see {#exchange} for other parameters
182
+ # @param name [String] Name of the exchange (defaults to "amq.direct")
183
+ # @see #exchange for other parameters
152
184
  # @return [Exchange]
153
- def direct(name = "", **kwargs)
154
- return exchange(name, "direct", **kwargs) unless name.empty?
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 {#exchange} for other parameters
223
+ # @see #exchange for other parameters
164
224
  # @return [Exchange]
165
- def topic(name = "amq.topic", **kwargs)
166
- exchange(name, "topic", **kwargs)
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 {#exchange} for other parameters
235
+ # @see #exchange for other parameters
172
236
  # @return [Exchange]
173
- def headers(name = "amq.headers", **kwargs)
174
- exchange(name, "headers", **kwargs)
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 (see Connection::Channel#basic_publish_confirm)
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 (see Connection::Channel#basic_publish_confirm)
254
+ # @return [nil]
184
255
  # @raise (see Connection::Channel#basic_publish_confirm)
185
- def publish(body, exchange, routing_key, **properties)
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 = { delivery_mode: 2 }.merge!(properties)
188
- conn.channel(1).basic_publish_confirm(body, exchange, routing_key, **properties)
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
- def publish_and_forget(body, exchange, routing_key, **properties)
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 = { delivery_mode: 2 }.merge!(properties)
200
- conn.channel(1).basic_publish(body, exchange, routing_key, **properties)
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,40 @@ 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 [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
223
- # @return [nil]
224
- def subscribe(queue, no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
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
- ch.basic_consume(queue, no_ack: no_ack, worker_threads: worker_threads, arguments: arguments, &blk)
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
+ ch = conn.channel
335
+ ch.basic_get(queue, no_ack:)
233
336
  end
234
337
  end
235
338
 
@@ -239,9 +342,9 @@ module AMQP
239
342
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
240
343
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
241
344
  # @return [nil]
242
- def bind(queue, exchange, binding_key, arguments: {})
345
+ def bind(queue:, exchange:, binding_key: "", arguments: {})
243
346
  with_connection do |conn|
244
- conn.channel(1).queue_bind(queue, exchange, binding_key, arguments: arguments)
347
+ conn.channel(1).queue_bind(queue, exchange:, binding_key:, arguments:)
245
348
  end
246
349
  end
247
350
 
@@ -251,9 +354,9 @@ module AMQP
251
354
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
252
355
  # @param arguments [Hash] Arguments matching the binding that's being removed
253
356
  # @return [nil]
254
- def unbind(queue, exchange, binding_key, arguments: {})
357
+ def unbind(queue:, exchange:, binding_key: "", arguments: {})
255
358
  with_connection do |conn|
256
- conn.channel(1).queue_unbind(queue, exchange, binding_key, arguments: arguments)
359
+ conn.channel(1).queue_unbind(queue, exchange:, binding_key:, arguments:)
257
360
  end
258
361
  end
259
362
 
@@ -273,7 +376,7 @@ module AMQP
273
376
  # @return [Integer] Number of messages in the queue when deleted
274
377
  def delete_queue(name, if_unused: false, if_empty: false)
275
378
  with_connection do |conn|
276
- msgs = conn.channel(1).queue_delete(name, if_unused: if_unused, if_empty: if_empty)
379
+ msgs = conn.channel(1).queue_delete(name, if_unused:, if_empty:)
277
380
  @queues.delete(name)
278
381
  msgs
279
382
  end
@@ -283,26 +386,26 @@ module AMQP
283
386
  # @!group Exchange actions
284
387
 
285
388
  # Bind an exchange to an exchange
286
- # @param destination [String] Name of the exchange to bind
287
389
  # @param source [String] Name of the exchange to bind to
390
+ # @param destination [String] Name of the exchange to bind
288
391
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
289
392
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
290
393
  # @return [nil]
291
- def exchange_bind(destination, source, binding_key, arguments: {})
394
+ def exchange_bind(source:, destination:, binding_key: "", arguments: {})
292
395
  with_connection do |conn|
293
- conn.channel(1).exchange_bind(destination, source, binding_key, arguments: arguments)
396
+ conn.channel(1).exchange_bind(destination:, source:, binding_key:, arguments:)
294
397
  end
295
398
  end
296
399
 
297
400
  # Unbind an exchange from an exchange
298
- # @param destination [String] Name of the exchange to unbind
299
401
  # @param source [String] Name of the exchange to unbind from
402
+ # @param destination [String] Name of the exchange to unbind
300
403
  # @param binding_key [String] Binding key which the exchange is bound to the exchange with
301
404
  # @param arguments [Hash] Arguments matching the binding that's being removed
302
405
  # @return [nil]
303
- def exchange_unbind(destination, source, binding_key, arguments: {})
406
+ def exchange_unbind(source:, destination:, binding_key: "", arguments: {})
304
407
  with_connection do |conn|
305
- conn.channel(1).exchange_unbind(destination, source, binding_key, arguments: arguments)
408
+ conn.channel(1).exchange_unbind(destination:, source:, binding_key:, arguments:)
306
409
  end
307
410
  end
308
411
 
@@ -318,9 +421,122 @@ module AMQP
318
421
  end
319
422
 
320
423
  # @!endgroup
424
+ # @!group RPC
321
425
 
322
- private
426
+ # Create a RPC server for a single method/function/procedure
427
+ # @param method [String, Symbol] name of the RPC method to host (i.e. queue name on the server side)
428
+ # @param worker_threads [Integer] number of threads that process requests
429
+ # @param durable [Boolean] If true the queue will survive broker restarts
430
+ # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
431
+ # (it won't be deleted until at least one consumer has consumed from it)
432
+ # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
433
+ # @yield Block that processes the RPC request messages
434
+ # @yieldparam [String] The body of the request message
435
+ # @yieldreturn [String] The response message body
436
+ # @return (see #subscribe)
437
+ def rpc_server(method, worker_threads: 1, durable: true, auto_delete: false, arguments: {}, &_)
438
+ queue(method.to_s, durable:, auto_delete:, arguments:)
439
+ .subscribe(prefetch: worker_threads, worker_threads:) do |msg|
440
+ result = yield msg.parse
441
+ properties = { content_type: msg.properties.content_type,
442
+ content_encoding: msg.properties.content_encoding }
443
+ result_body = serialize_and_encode_body(result, properties)
444
+
445
+ msg.channel.basic_publish(result_body, exchange: "", routing_key: msg.properties.reply_to,
446
+ correlation_id: msg.properties.correlation_id, **properties)
447
+ msg.ack
448
+ rescue StandardError
449
+ msg.reject(requeue: false)
450
+ raise
451
+ end
452
+ end
323
453
 
454
+ # Do a RPC call, sends a messages, waits for a response
455
+ # @param method [String, Symbol] name of the RPC method to call (i.e. queue name on the server side)
456
+ # @param arguments [String] arguments/body to the call
457
+ # @param timeout [Numeric, nil] Number of seconds to wait for a response
458
+ # @option (see Client#publish)
459
+ # @return [String] Returns the result from the call
460
+ # @raise [Timeout::Error] if no response is received within the timeout period
461
+ def rpc_call(method, arguments, timeout: nil, **properties)
462
+ ch = with_connection(&:channel)
463
+ begin
464
+ msg = ch.basic_consume_once("amq.rabbitmq.reply-to", timeout:) do
465
+ properties = default_content_properties.merge(properties)
466
+ body = serialize_and_encode_body(arguments, properties)
467
+ ch.basic_publish(body, exchange: "", routing_key: method.to_s,
468
+ reply_to: "amq.rabbitmq.reply-to", **properties)
469
+ end
470
+ msg.parse
471
+ ensure
472
+ ch.close
473
+ end
474
+ end
475
+
476
+ # Create a reusable RPC client
477
+ # @return [RPCClient]
478
+ def rpc_client
479
+ ch = with_connection(&:channel)
480
+ RPCClient.new(ch).start
481
+ end
482
+
483
+ # @!endgroup
484
+ # @!group Message coding
485
+
486
+ class << self
487
+ # Configure the AMQP::Client class-level settings
488
+ # @yield [Configuration] Yields the configuration object for modification
489
+ # @return [Configuration] The configuration object
490
+ # @example
491
+ # AMQP::Client.configure do |config|
492
+ # config.default_content_type = "application/json"
493
+ # config.strict_coding = true
494
+ # end
495
+ def configure
496
+ yield @config if block_given?
497
+ @config
498
+ end
499
+
500
+ # Get the class-level configuration
501
+ # @return [Configuration]
502
+ attr_reader :config
503
+
504
+ # Get the class-level codec registry
505
+ # @return [MessageCodecRegistry]
506
+ attr_reader :codec_registry
507
+
508
+ # We need to set the subclass's configuration and codec registry
509
+ # because these are class instance variables, hence not inherited.
510
+ # @api private
511
+ def inherited(subclass)
512
+ super
513
+ subclass_codec_registry = @codec_registry.dup
514
+ subclass.instance_variable_set(:@codec_registry, subclass_codec_registry)
515
+ subclass.instance_variable_set(:@config, Configuration.new(subclass_codec_registry))
516
+ # Copy configuration settings from parent
517
+ subclass.config.strict_coding = @config.strict_coding
518
+ subclass.config.default_content_type = @config.default_content_type
519
+ subclass.config.default_content_encoding = @config.default_content_encoding
520
+ end
521
+ end
522
+
523
+ # Get the codec registry for this instance
524
+ # @return [MessageCodecRegistry]
525
+ attr_reader :codec_registry
526
+
527
+ # Get/set if condig should be strict, i.e. if the client should raise on unknown codecs
528
+ attr_accessor :strict_coding
529
+
530
+ # Get/set the default content_type to use when publishing messages
531
+ # @return [String, nil]
532
+ attr_accessor :default_content_type
533
+
534
+ # Get/set the default content_encoding to use when publishing messages
535
+ # @return [String, nil]
536
+ attr_accessor :default_content_encoding
537
+
538
+ # @!endgroup
539
+ #
324
540
  def with_connection
325
541
  conn = nil
326
542
  loop do
@@ -335,5 +551,53 @@ module AMQP
335
551
  @connq << conn unless conn.closed?
336
552
  end
337
553
  end
554
+
555
+ # @api private
556
+ def cancel_consumer(consumer)
557
+ @consumers.delete(consumer.id)
558
+ with_connection do |conn|
559
+ conn.channel(consumer.channel_id).basic_cancel(consumer.tag)
560
+ end
561
+ end
562
+
563
+ private
564
+
565
+ def default_content_properties
566
+ {
567
+ content_type: @default_content_type,
568
+ content_encoding: @default_content_encoding
569
+ }.compact
570
+ end
571
+
572
+ def serialize_and_encode_body(body, properties)
573
+ body = serialize_body(body, properties)
574
+ encode_body(body, properties)
575
+ end
576
+
577
+ def encode_body(body, properties)
578
+ ce = properties[:content_encoding]
579
+ coder = @codec_registry.find_coder(ce)
580
+
581
+ return coder.encode(body, properties) if coder
582
+
583
+ is_unsupported = ce && ce != ""
584
+ raise Error::UnsupportedContentEncoding, ce if is_unsupported && @strict_coding
585
+
586
+ body
587
+ end
588
+
589
+ def serialize_body(body, properties)
590
+ return body if body.is_a?(String)
591
+
592
+ ct = properties[:content_type]
593
+ parser = @codec_registry.find_parser(ct)
594
+
595
+ return parser.serialize(body, properties) if parser
596
+
597
+ is_unsupported = ct && ct != "" && ct != "text/plain"
598
+ raise Error::UnsupportedContentType, ct if is_unsupported && @strict_coding
599
+
600
+ body.to_s
601
+ end
338
602
  end
339
603
  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: 1.2.1
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - CloudAMQP
8
- bindir: exe
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