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.
@@ -5,30 +5,37 @@ module AMQP
5
5
  # All errors raised inherit from this class
6
6
  class Error < StandardError
7
7
  # Raised when a frame that wasn't expected arrives
8
- class UnexpectedFrame < Error
8
+ class UnexpectedFrameType < Error
9
9
  def initialize(expected, actual)
10
- super "Expected frame type '#{expected}' but got '#{actual}'"
10
+ super("Expected frame type '#{expected}' but got '#{actual}'")
11
11
  end
12
12
  end
13
13
 
14
14
  # Raised when a frame doesn't end with 206
15
15
  class UnexpectedFrameEnd < Error
16
16
  def initialize(actual)
17
- super "Expected frame end 206 but got '#{actual}'"
17
+ super("Expected frame end 206 but got '#{actual}'")
18
18
  end
19
19
  end
20
20
 
21
21
  # Should never be raised as we support all official frame types
22
22
  class UnsupportedFrameType < Error
23
23
  def initialize(type)
24
- super "Unsupported frame type '#{type}'"
24
+ super("Unsupported frame type '#{type}'")
25
25
  end
26
26
  end
27
27
 
28
28
  # Raised if a frame is received but not implemented
29
29
  class UnsupportedMethodFrame < Error
30
30
  def initialize(class_id, method_id)
31
- super "Unsupported class/method: #{class_id} #{method_id}"
31
+ super("Unsupported class/method: #{class_id} #{method_id}")
32
+ end
33
+ end
34
+
35
+ # Raised if a message published with confirms enabled was not confirmed (nacked)
36
+ class PublishNotConfirmed < Error
37
+ def initialize
38
+ super("Message was not confirmed by the broker")
32
39
  end
33
40
  end
34
41
 
@@ -37,25 +44,93 @@ module AMQP
37
44
  def self.new(id, level, code, reason, classid = 0, methodid = 0)
38
45
  case level
39
46
  when :connection
40
- ConnectionClosed.new(code, reason, classid, methodid)
47
+ build_connection_error(code, reason, classid, methodid)
41
48
  when :channel
42
- ChannelClosed.new(id, code, reason, classid, methodid)
49
+ build_channel_error(id, code, reason, classid, methodid)
43
50
  else raise ArgumentError, "invalid level '#{level}'"
44
51
  end
45
52
  end
53
+
54
+ private_class_method def self.build_connection_error(code, reason, classid, methodid)
55
+ klass = case code
56
+ when 320
57
+ ConnectionForced
58
+ when 501
59
+ FrameError
60
+ when 503
61
+ CommandInvalid
62
+ when 504
63
+ ChannelError
64
+ when 505
65
+ UnexpectedFrame
66
+ when 506
67
+ ResourceError
68
+ when 530
69
+ NotAllowedError
70
+ when 541
71
+ InternalError
72
+ else
73
+ ConnectionClosed
74
+ end
75
+ klass.new(code, reason, classid, methodid)
76
+ end
77
+
78
+ private_class_method def self.build_channel_error(id, code, reason, classid, methodid)
79
+ klass = case code
80
+ when 403
81
+ AccessRefused
82
+ when 404
83
+ NotFound
84
+ when 405
85
+ ResourceLocked
86
+ when 406
87
+ PreconditionFailed
88
+ else
89
+ ChannelClosed
90
+ end
91
+ klass.new(id, code, reason, classid, methodid)
92
+ end
46
93
  end
47
94
 
48
95
  # Raised if channel is already closed
49
96
  class ChannelClosed < Error
50
97
  def initialize(id, code, reason, classid = 0, methodid = 0)
51
- super "Channel[#{id}] closed (#{code}) #{reason} (#{classid}/#{methodid})"
98
+ super("Channel[#{id}] closed (#{code}) #{reason} (#{classid}/#{methodid})")
52
99
  end
53
100
  end
54
101
 
55
102
  # Raised if connection is unexpectedly closed
56
103
  class ConnectionClosed < Error
57
104
  def initialize(code, reason, classid = 0, methodid = 0)
58
- super "Connection closed (#{code}) #{reason} (#{classid}/#{methodid})"
105
+ super("Connection closed (#{code}) #{reason} (#{classid}/#{methodid})")
106
+ end
107
+ end
108
+
109
+ class AccessRefused < ChannelClosed; end
110
+ class NotFound < ChannelClosed; end
111
+ class ResourceLocked < ChannelClosed; end
112
+ class PreconditionFailed < ChannelClosed; end
113
+
114
+ class ConnectionForced < ConnectionClosed; end
115
+ class FrameError < ConnectionClosed; end
116
+ class CommandInvalid < ConnectionClosed; end
117
+ class ChannelError < ConnectionClosed; end
118
+ class UnexpectedFrame < ConnectionClosed; end
119
+ class ResourceError < ConnectionClosed; end
120
+ class NotAllowedError < ConnectionClosed; end
121
+ class InternalError < ConnectionClosed; end
122
+
123
+ # Raised if trying to parse a message with an unsupported content type
124
+ class UnsupportedContentType < Error
125
+ def initialize(content_type)
126
+ super("Unsupported content type #{content_type}")
127
+ end
128
+ end
129
+
130
+ # Raised if trying to parse a message with an unsupported content encoding
131
+ class UnsupportedContentEncoding < Error
132
+ def initialize(content_encoding)
133
+ super("Unsupported content encoding #{content_encoding}")
59
134
  end
60
135
  end
61
136
  end
@@ -13,27 +13,25 @@ module AMQP
13
13
  @name = name
14
14
  end
15
15
 
16
- # Publish to the exchange
17
- # @param body [String] The message body
18
- # @param routing_key [String] The routing key of the message,
19
- # the exchange may use this when routing the message to bound queues (defaults to empty string)
20
- # @param properties [Properties]
21
- # @option properties [String] content_type Content type of the message body
22
- # @option properties [String] content_encoding Content encoding of the body
23
- # @option properties [Hash<String, Object>] headers Custom headers
24
- # @option properties [Integer] delivery_mode 2 for persisted message, transient messages for all other values
25
- # @option properties [Integer] priority A priority of the message (between 0 and 255)
26
- # @option properties [Integer] correlation_id A correlation id, most often used used for RPC communication
27
- # @option properties [String] reply_to Queue to reply RPC responses to
28
- # @option properties [Integer, String] expiration Number of seconds the message will stay in the queue
29
- # @option properties [String] message_id Can be used to uniquely identify the message, e.g. for deduplication
30
- # @option properties [Date] timestamp Often used for the time the message was originally generated
31
- # @option properties [String] type Can indicate what kind of message this is
32
- # @option properties [String] user_id Can be used to verify that this is the user that published the message
33
- # @option properties [String] app_id Can be used to indicates which app that generated the message
16
+ # Publish to the exchange, wait for confirm
17
+ # @param body [Object] The message body
18
+ # will be encoded if any matching codec is found in the client's codec registry
19
+ # @param routing_key [String] Routing key for the message
20
+ # @option (see Client#publish)
21
+ # @raise (see Client#publish)
34
22
  # @return [Exchange] self
35
- def publish(body, routing_key = "", **properties)
36
- @client.publish(body, @name, routing_key, **properties)
23
+ def publish(body, routing_key: "", **properties)
24
+ @client.publish(body, exchange: @name, routing_key:, **properties)
25
+ self
26
+ end
27
+
28
+ # Publish to the exchange, without waiting for confirm
29
+ # @param (see Exchange#publish)
30
+ # @option (see Exchange#publish)
31
+ # @raise (see Exchange#publish)
32
+ # @return [Exchange] self
33
+ def publish_and_forget(body, routing_key: "", **properties)
34
+ @client.publish_and_forget(body, exchange: @name, routing_key:, **properties)
37
35
  self
38
36
  end
39
37
 
@@ -42,9 +40,9 @@ module AMQP
42
40
  # @param binding_key [String] Binding key on which messages that match might be routed (defaults to empty string)
43
41
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
44
42
  # @return [Exchange] self
45
- def bind(source, binding_key = "", arguments: {})
46
- source = source.is_a?(String) ? source : source.name
47
- @client.exchange_bind(@name, source, binding_key, arguments: arguments)
43
+ def bind(source, binding_key: "", arguments: {})
44
+ source = source.name unless source.is_a?(String)
45
+ @client.exchange_bind(source:, destination: @name, binding_key:, arguments:)
48
46
  self
49
47
  end
50
48
 
@@ -53,9 +51,9 @@ module AMQP
53
51
  # @param binding_key [String] Binding key which the queue is bound to the exchange with (defaults to empty string)
54
52
  # @param arguments [Hash] Arguments matching the binding that's being removed
55
53
  # @return [Exchange] self
56
- def unbind(source, binding_key = "", arguments: {})
57
- source = source.is_a?(String) ? source : source.name
58
- @client.exchange_unbind(@name, source, binding_key, arguments: arguments)
54
+ def unbind(source, binding_key: "", arguments: {})
55
+ source = source.name unless source.is_a?(String)
56
+ @client.exchange_unbind(source:, destination: @name, binding_key:, arguments:)
59
57
  self
60
58
  end
61
59
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./properties"
4
- require_relative "./table"
3
+ require_relative "properties"
4
+ require_relative "table"
5
5
 
6
6
  module AMQP
7
7
  class Client
@@ -370,9 +370,8 @@ module AMQP
370
370
  ].pack("C S> L> a* C")
371
371
  end
372
372
 
373
- def self.basic_consume(id, queue, tag, no_ack, exclusive, arguments)
373
+ def self.basic_consume(id, queue, tag, no_ack, exclusive, no_wait, arguments)
374
374
  no_local = false
375
- no_wait = false
376
375
  bits = 0
377
376
  bits |= (1 << 0) if no_local
378
377
  bits |= (1 << 1) if no_ack
@@ -14,8 +14,12 @@ module AMQP
14
14
  @redelivered = redelivered
15
15
  @properties = nil
16
16
  @body = ""
17
+ @ack_or_reject_sent = false
18
+ @parsed = nil
17
19
  end
18
20
 
21
+ DeliveryInfo = Struct.new(:consumer_tag, :delivery_tag, :redelivered, :exchange, :routing_key, :channel)
22
+
19
23
  # The channel the message was deliviered to
20
24
  # @return [Connection::Channel]
21
25
  attr_reader :channel
@@ -49,17 +53,36 @@ module AMQP
49
53
  # @return [String]
50
54
  attr_accessor :body
51
55
 
56
+ def delivery_info
57
+ @delivery_info ||= DeliveryInfo.new(
58
+ consumer_tag:,
59
+ delivery_tag:,
60
+ redelivered:,
61
+ exchange:,
62
+ routing_key:,
63
+ channel:
64
+ )
65
+ end
66
+
52
67
  # Acknowledge the message
53
68
  # @return [nil]
54
69
  def ack
70
+ return if @ack_or_reject_sent
71
+
55
72
  @channel.basic_ack(@delivery_tag)
73
+ @ack_or_reject_sent = true
74
+ nil
56
75
  end
57
76
 
58
77
  # Reject the message
59
78
  # @param requeue [Boolean] If true the message will be put back into the queue again, ready to be redelivered
60
79
  # @return [nil]
61
80
  def reject(requeue: false)
62
- @channel.basic_reject(@delivery_tag, requeue: requeue)
81
+ return if @ack_or_reject_sent
82
+
83
+ @channel.basic_reject(@delivery_tag, requeue:)
84
+ @ack_or_reject_sent = true
85
+ nil
63
86
  end
64
87
 
65
88
  # @see #exchange
@@ -69,6 +92,48 @@ module AMQP
69
92
  def exchange_name
70
93
  @exchange
71
94
  end
95
+
96
+ # @!group Message coding
97
+
98
+ # Parse the message body based on content_type and content_encoding
99
+ # @raise [Error::UnsupportedContentEncoding] If the content encoding is not supported
100
+ # @raise [Error::UnsupportedContentType] If the content type is not supported
101
+ # @return [Object] The parsed message body
102
+ def parse
103
+ return @parsed unless @parsed.nil?
104
+
105
+ registry = @channel.connection.codec_registry
106
+ strict = @channel.connection.strict_coding
107
+ decoded = decode
108
+ ct = @properties&.content_type
109
+ parser = registry&.find_parser(ct)
110
+
111
+ return @parsed = parser.parse(decoded, @properties) if parser
112
+
113
+ is_unsupported = ct && ct != "" && ct != "text/plain"
114
+ raise Error::UnsupportedContentType, ct if is_unsupported && strict
115
+
116
+ @parsed = decoded
117
+ end
118
+
119
+ # Decode the message body based on content_encoding
120
+ # @raise [Error::UnsupportedContentEncoding] If the content encoding is not supported
121
+ # @return [String] The decoded message body
122
+ def decode
123
+ registry = @channel.connection.codec_registry
124
+ strict = @channel.connection.strict_coding
125
+ ce = @properties&.content_encoding
126
+ coder = registry&.find_coder(ce)
127
+
128
+ return coder.decode(@body, @properties) if coder
129
+
130
+ is_unsupported = ce && ce != ""
131
+ raise Error::UnsupportedContentEncoding, ce if is_unsupported && strict
132
+
133
+ @body
134
+ end
135
+
136
+ # @!endgroup
72
137
  end
73
138
 
74
139
  # A published message returned by the broker due to some error
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AMQP
4
+ class Client
5
+ # Internal registry that stores content_type parsers and content_encoding coders.
6
+ # Only exact content_type and content_encoding matches are supported.
7
+ class MessageCodecRegistry
8
+ def initialize
9
+ @parsers = {} # content_type => handler
10
+ @coders = {} # content_encoding => handler
11
+ end
12
+
13
+ # Register a parser for a content_type
14
+ # @param content_type [String] The content_type to match
15
+ # @param parser [Object] The parser object,
16
+ # must respond to parse(data, properties) and serialize(obj, properties)
17
+ # @return [self]
18
+ def register_parser(content_type:, parser:)
19
+ validate_parser!(parser)
20
+ @parsers[content_type] = parser
21
+ self
22
+ end
23
+
24
+ # Register a coder for a specific content_encoding
25
+ # @param content_encoding [String] The content_encoding to match
26
+ # @param coder [Object] The coder object,
27
+ # must respond to encode(data, properties) and decode(data, properties)
28
+ # @return [self]
29
+ def register_coder(content_encoding:, coder:)
30
+ validate_coder!(coder)
31
+ @coders[content_encoding] = coder
32
+ self
33
+ end
34
+
35
+ # Find parser handler based on message properties
36
+ # @param content_type [String] The content_type to match
37
+ # @return [Object, nil] The parser object or nil if not found
38
+ def find_parser(content_type)
39
+ @parsers[content_type]
40
+ end
41
+
42
+ # Find coder handler based on content_encoding
43
+ # @param content_encoding [String] The content_encoding to match
44
+ # @return [Object, nil] The coder object or nil if not found
45
+ def find_coder(content_encoding)
46
+ @coders[content_encoding]
47
+ end
48
+
49
+ # Introspection helper to list all registered content types
50
+ # @return [Array<String>] List of registered content types
51
+ def list_content_types
52
+ @parsers.keys
53
+ end
54
+
55
+ # Introspection helper to list all registered content encodings
56
+ # @return [Array<String>] List of registered content encodings
57
+ def list_content_encodings
58
+ @coders.keys
59
+ end
60
+
61
+ # Enable built-in parsers for common content types
62
+ # @return [self]
63
+ def enable_builtin_parsers
64
+ register_parser(content_type: "text/plain", parser: Parsers::Plain)
65
+ register_parser(content_type: "application/json", parser: Parsers::JSONParser)
66
+ self
67
+ end
68
+
69
+ # Enable built-in coders for common content encodings
70
+ # @return [self]
71
+ def enable_builtin_coders
72
+ register_coder(content_encoding: "gzip", coder: Coders::Gzip)
73
+ register_coder(content_encoding: "deflate", coder: Coders::Deflate)
74
+ self
75
+ end
76
+
77
+ # Enable all built-in codecs (parsers and coders)
78
+ # @return [self]
79
+ def enable_builtin_codecs
80
+ enable_builtin_parsers.enable_builtin_coders
81
+ end
82
+
83
+ # Lightweight cloning: registry contents duplicated (shallow copy of handler references)
84
+ def dup
85
+ copy = self.class.new
86
+ @parsers.each { |k, v| copy.register_parser(content_type: k, parser: v) }
87
+ @coders.each { |k, v| copy.register_coder(content_encoding: k, coder: v) }
88
+ copy
89
+ end
90
+
91
+ private
92
+
93
+ def validate_parser!(parser)
94
+ return if parser.respond_to?(:parse) && parser.respond_to?(:serialize)
95
+
96
+ raise ArgumentError, "parser must respond to parse(data, properties) and serialize(obj, properties)"
97
+ end
98
+
99
+ def validate_coder!(coder)
100
+ return if coder.respond_to?(:encode) && coder.respond_to?(:decode)
101
+
102
+ raise ArgumentError, "coder must respond to encode(data, properties) and decode(data, properties)"
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "zlib"
5
+
6
+ module AMQP
7
+ class Client
8
+ module Parsers
9
+ # Plain text passthrough parser
10
+ Plain = Class.new do
11
+ def parse(data, _properties) = data
12
+ def serialize(obj, _properties) = obj.is_a?(String) ? obj : obj.to_s
13
+ end.new
14
+
15
+ JSONParser = Class.new do
16
+ def parse(data, _properties) = ::JSON.parse(data, symbolize_names: true)
17
+ def serialize(obj, _properties) = ::JSON.dump(obj)
18
+ end.new
19
+ end
20
+
21
+ module Coders
22
+ Gzip = Class.new do
23
+ def encode(data, _properties)
24
+ return data if data.encoding == Encoding::BINARY
25
+
26
+ Zlib.gzip(data)
27
+ end
28
+
29
+ def decode(data, _properties) = Zlib.gunzip(data)
30
+ end.new
31
+
32
+ Deflate = Class.new do
33
+ def encode(data, _properties)
34
+ return data if data.encoding == Encoding::BINARY
35
+
36
+ Zlib.deflate(data)
37
+ end
38
+
39
+ def decode(data, _properties) = Zlib.inflate(data)
40
+ end.new
41
+ end
42
+ end
43
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "./table"
3
+ require_relative "table"
4
4
 
5
5
  module AMQP
6
6
  class Client
@@ -30,19 +30,19 @@ module AMQP
30
30
  # @return [Hash] Properties
31
31
  def to_h
32
32
  {
33
- content_type: content_type,
34
- content_encoding: content_encoding,
35
- headers: headers,
36
- delivery_mode: delivery_mode,
37
- priority: priority,
38
- correlation_id: correlation_id,
39
- reply_to: reply_to,
40
- expiration: expiration,
41
- message_id: message_id,
42
- timestamp: timestamp,
43
- type: type,
44
- user_id: user_id,
45
- app_id: app_id
33
+ content_type:,
34
+ content_encoding:,
35
+ headers:,
36
+ delivery_mode:,
37
+ priority:,
38
+ correlation_id:,
39
+ reply_to:,
40
+ expiration:,
41
+ message_id:,
42
+ timestamp:,
43
+ type:,
44
+ user_id:,
45
+ app_id:
46
46
  }
47
47
  end
48
48
 
@@ -184,7 +184,8 @@ module AMQP
184
184
  end
185
185
 
186
186
  if (timestamp = properties[:timestamp])
187
- timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
187
+ timestamp.is_a?(Integer) || timestamp.is_a?(Time) ||
188
+ raise(ArgumentError, "timestamp must be an Integer or a Time")
188
189
 
189
190
  flags |= (1 << 6)
190
191
  arr << timestamp.to_i
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "consumer"
4
+
3
5
  module AMQP
4
6
  class Client
5
7
  # Queue abstraction
@@ -14,26 +16,58 @@ module AMQP
14
16
  end
15
17
 
16
18
  # Publish to the queue, wait for confirm
17
- # @param (see Client#publish)
19
+ # @param body [Object] The message body
20
+ # will be encoded if any matching codec is found in the client's codec registry
18
21
  # @option (see Client#publish)
19
22
  # @raise (see Client#publish)
20
- # @return [self]
23
+ # @return [Queue] self
21
24
  def publish(body, **properties)
22
- @client.publish(body, "", @name, **properties)
25
+ @client.publish(body, exchange: "", routing_key: @name, **properties)
26
+ self
27
+ end
28
+
29
+ # Publish to the queue, without waiting for confirm
30
+ # @param (see Queue#publish)
31
+ # @option (see Queue#publish)
32
+ # @raise (see Queue#publish)
33
+ # @return [Queue] self
34
+ def publish_and_forget(body, **properties)
35
+ @client.publish_and_forget(body, exchange: "", routing_key: @name, **properties)
23
36
  self
24
37
  end
25
38
 
26
39
  # Subscribe/consume from the queue
27
- # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected)
40
+ # @param no_ack [Boolean] If true, messages are automatically acknowledged by the server upon delivery.
41
+ # If false, messages are acknowledged only after the block completes successfully; if the block raises
42
+ # an exception, the message is rejected and can be optionally requeued.
43
+ # You can of course handle the ack/reject in the block yourself. (Default: false)
44
+ # @param exclusive [Boolean] When true only a single consumer can consume from the queue at a time
28
45
  # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false
29
46
  # @param worker_threads [Integer] Number of threads processing messages,
30
47
  # 0 means that the thread calling this method will be blocked
48
+ # @param requeue_on_reject [Boolean] If true, messages that are rejected due to an exception in the block
49
+ # will be requeued. Only relevant if no_ack is false. (Default: true)
50
+ # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
51
+ # The proc will be called with the consumer tag as the only argument
31
52
  # @param arguments [Hash] Custom arguments to the consumer
32
53
  # @yield [Message] Delivered message from the queue
33
- # @return [self]
34
- def subscribe(no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
35
- @client.subscribe(@name, no_ack: no_ack, prefetch: prefetch, worker_threads: worker_threads, arguments: arguments, &blk)
36
- self
54
+ # @return [Consumer] The consumer object, which can be used to cancel the consumer
55
+ def subscribe(no_ack: false, exclusive: false, prefetch: 1, worker_threads: 1, requeue_on_reject: true,
56
+ on_cancel: nil, arguments: {})
57
+ @client.subscribe(@name, no_ack:, exclusive:, prefetch:, worker_threads:, on_cancel:, arguments:) do |message|
58
+ yield message
59
+ message.ack unless no_ack
60
+ rescue StandardError => e
61
+ message.reject(requeue: requeue_on_reject) unless no_ack
62
+ raise e
63
+ end
64
+ end
65
+
66
+ # Get a message from the queue
67
+ # @param no_ack [Boolean] When false the message has to be manually acknowledged (or rejected) (default: false)
68
+ # @return [Message, nil] The message from the queue or nil if the queue is empty
69
+ def get(no_ack: false)
70
+ @client.get(@name, no_ack:)
37
71
  end
38
72
 
39
73
  # Bind the queue to an exchange
@@ -41,9 +75,9 @@ module AMQP
41
75
  # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
42
76
  # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
43
77
  # @return [self]
44
- def bind(exchange, binding_key, arguments: {})
45
- exchange = exchange.is_a?(String) ? exchange : exchange.name
46
- @client.bind(@name, exchange, binding_key, arguments: arguments)
78
+ def bind(exchange, binding_key: "", arguments: {})
79
+ exchange = exchange.name unless exchange.is_a?(String)
80
+ @client.bind(queue: @name, exchange:, binding_key:, arguments:)
47
81
  self
48
82
  end
49
83
 
@@ -52,9 +86,9 @@ module AMQP
52
86
  # @param binding_key [String] Binding key which the queue is bound to the exchange with
53
87
  # @param arguments [Hash] Arguments matching the binding that's being removed
54
88
  # @return [self]
55
- def unbind(exchange, binding_key, arguments: {})
56
- exchange = exchange.is_a?(String) ? exchange : exchange.name
57
- @client.unbind(@name, exchange, binding_key, arguments: arguments)
89
+ def unbind(exchange, binding_key: "", arguments: {})
90
+ exchange = exchange.name unless exchange.is_a?(String)
91
+ @client.unbind(queue: @name, exchange:, binding_key:, arguments:)
58
92
  self
59
93
  end
60
94