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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cfb2dd354eeb33334cf6848b168f2a0caa486b40cefcce924910a16a77ffa4f
4
- data.tar.gz: 7a7896809e27539ab3395b833038c5b328298c9cf6a25aeff7f706e05657e995
3
+ metadata.gz: c07d38a543ff2c0e6e57dfc4ce4921ba74dbac1091cd55a7d06fc791a4ba501e
4
+ data.tar.gz: 1909a126c2c6c21d36990cff7b0c623ad4e843afe52ddd5cad69299487286f0c
5
5
  SHA512:
6
- metadata.gz: c8d0c1797b3b8a81fbe33a154bb255c452f09d925fdeafdfc03fdbb3342b116d7875d6166f7e9b948232f09d69c16179e0270c752b3afdd955d4ad6fca3b6cd1
7
- data.tar.gz: 47c518dee777f61435e748045fb7cfebf808ac5ed9cd53813081b222a9abc81b82f85f436482b25dba1dcd10a96c1d1f9bbf4b5266fc90ece48307b61f03ccfb
6
+ metadata.gz: 6628d1283167e699482baf15ae84bc336c59015b9a86921090ea2a15513267e1fe290788640cef65f92e88f779b039288b05fa6a50d25a449144268edeac7003
7
+ data.tar.gz: 1684f2e5ef0ffb7c8a67371fe80031904681ccac21e164d190a87605f52b5b73560b4dbd2862f4047f944238f1126aa6de139cdbd1fa1b9acdd39324f8d71c56
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./message"
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
- " consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
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 a connection
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(&:close)
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(&:close)
79
- @consumers.each_value(&:clear) # empty the queues too, messages can't be acked anymore
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, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
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 destination [String] Name of the exchange to bind
122
- # @param source [String] Name of the exchange to bind to
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, source, binding_key, arguments: {})
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 destination [String] Name of the exchange to unbind
134
- # @param source [String] Name of the exchange to unbind from
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, source, binding_key, arguments: {})
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 = Struct.new(:queue_name, :message_count, :consumer_count)
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 channel is closed
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] The QueueOk struct got `queue_name`, `message_count` and `consumer_count` properties
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 to bind
192
- # @param exchange [String] Name of the exchange to bind to
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, binding_key, arguments: {})
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 to unbind
215
- # @param exchange [String] Name of the exchange to unbind from
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, binding_key, arguments: {})
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, can be a string or a byte array
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, routing_key, **properties)
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, routing_key, **properties)
309
+ def basic_publish_confirm(body, exchange:, routing_key: "", **properties)
301
310
  confirm_select(no_wait: true)
302
- basic_publish(body, exchange, routing_key, **properties)
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 uniqe among this channel's consumers only
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 [Array<(String, Array<Thread>)>] Returns consumer_tag and an array of worker threads
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, arguments: {}, worker_threads: 1, &blk)
319
- write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
320
- tag, = expect(:basic_consume_ok)
321
- @consumers[tag] = q = ::Queue.new
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
- consume_loop(q, tag, &blk)
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(q, tag, &blk) }
352
+ Thread.new { consume_loop(msg_q, consumer_tag, &blk) }
328
353
  end
329
- [tag, threads]
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.fetch(consumer_tag)
339
- return if consumer.closed?
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
- consumer.close
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: 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 nil
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
- _ack_or_nack, delivery_tag, multiple = *args
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 close_consumer(tag)
513
- @consumers.fetch(tag).close
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::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
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 "./frame_bytes"
7
- require_relative "./channel"
8
- require_relative "./errors"
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, **options)
75
- new(uri, read_loop_thread: read_loop_thread, **options)
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 || raise(Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}")
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::UnexpectedFrameEnd, frame_end if frame_end != 206
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
- channel = @channels_lock.synchronize { @channels.delete(channel_id) }
289
- channel.closed!(:channel, reply_code, reply_text, classid, methodid)
290
- write_bytes FrameBytes.channel_close_ok(channel_id)
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.close_consumer(tag)
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::UnexpectedFrame.new(expected_frame_type, frame_type))
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 host, port, connect_timeout: connect_timeout
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::UnexpectedFrameEnd, frame_end if frame_end != 206
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["AMQP_PORT"])
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