amqp-client 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d7dae171425db13570982c850a9efaeca9bc848be72b0a7e8652bef0407181d
4
- data.tar.gz: e8e263b52a8055cb4385b980727c7ef5e35af5bbf127ac270474145ac77ebfc2
3
+ metadata.gz: 5e6dd3fef130c286e97eadd9bb6d3d4de0fa67599361604cec3560879b9b4bdf
4
+ data.tar.gz: c2ce00f897c68d3085427854b08cc98d3487b40bb5327ce38bb216c98357e366
5
5
  SHA512:
6
- metadata.gz: eb41014541c095620671d2a19564051eb65bcbf296e3f7464b4542f4a22ff74867b0466063eed15442f26d3acbec1de81c498cf94cb77adce612fe413389523c
7
- data.tar.gz: c7620262a2de427abb0efb06b253e065f3aa41e4a59feee4336045650afc2c8ac389a8ce784631b17e8f8a9d95be426eb370b7573f068c9848dd5130baeb0564
6
+ metadata.gz: 9b62e8ee74ec542be5617b08fbd0e36e5fc16294f99cd0ec61781c7c7425005c426e8358a74abf45a1a24ecb88451b94ef50ee7524403ac7097748b2e94b55b5
7
+ data.tar.gz: 5c3e63369578923e20b441fbe9728b374e18150c75ed155291b802c42f80f668b704b39e80b144b90346a8d769d9b71f031b9fca712e6ac52c100a88e46380e3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2021-08-27
4
+
5
+ - Verify TLS certificate matches hostname
6
+ - TLS thread-safety
7
+ - Assemble Messages in the (single threaded) read_loop thread
8
+ - Give read_loop_thread higher priority so that channel errors crop up faster
9
+ - One less Thread required per Consumer
10
+ - Read exactly one frame at a time, not trying to split/assemble frames over socket reads
11
+ - Heafty speedup for message assembling with StringIO
12
+ - Channel#queue_declare returns a struct for nicer API (still backward compatible)
13
+ - AMQP::Client#publish_and_forget for fast, non confirmed publishes
14
+ - Allow Properties#timestamp to be an integer (in addition to Time)
15
+ - Bug fix allow Properties#expiration to be an Integer
16
+ - Consistent use of named parameters
17
+ - High level Exchange API
18
+ - Don't try to reconnect if first connect fails
19
+ - Bug fix: Close all channels when connection is closed by server
20
+ - Raise error if run out of channels
21
+ - Improved retry in high level client
22
+ - Bug fix: Support channel_max 0
23
+
24
+ ## [0.3.0] - 2021-08-20
25
+
26
+ - Channel#wait_for_confirms is a smarter way of waiting for publish confirms
27
+ - Default connection_name to $PROGRAM_NAME
28
+
29
+ ## [0.2.3] - 2021-08-19
30
+
31
+ - Improved TLS/AMQPS support
32
+
33
+ ## [0.2.2] - 2021-08-19
34
+
35
+ - TLS port issue fixed
3
36
 
4
37
  ## [0.2.1] - 2021-08-19
5
38
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # AMQP::Client
2
2
 
3
- An AMQP 0-9-1 client alternative, trying to keep things as simple as possible.
3
+ An AMQP 0-9-1 Ruby client, trying to keep things as simple as possible.
4
4
 
5
5
  ## Installation
6
6
 
@@ -34,7 +34,7 @@ msg = ch.basic_get q[:queue_name]
34
34
  puts msg.body
35
35
  ```
36
36
 
37
- High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to be extreme throughput, be expect 100% delivery guarantees (messages might be deliviered twice, in the unlikely event of a connection loss between message publish and message confirmed by the server).
37
+ High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to have extreme throughput, but expect 100% delivery guarantees (messages might be delivered twice, in the unlikely event of connection loss between message publish and message confirmation by the server).
38
38
 
39
39
  ```ruby
40
40
  amqp = AMQP::Client.new("amqp://localhost")
@@ -46,8 +46,8 @@ q = amqp.queue("myqueue")
46
46
  # Bind the queue to any exchange, with any binding key
47
47
  q.bind("amq.topic", "my.events.*")
48
48
 
49
- # The message will be reprocessed if the client lost connection to the server
50
- # between the message arrived and the message was supposed to be ack:ed.
49
+ # The message will be reprocessed if the client loses connection to the server
50
+ # between message arrival and when the message was supposed to be ack'ed.
51
51
  q.subscribe(prefetch: 20) do |msg|
52
52
  process(JSON.parse(msg.body))
53
53
  msg.ack
@@ -6,25 +6,32 @@ module AMQP
6
6
  # AMQP Channel
7
7
  class Channel
8
8
  def initialize(connection, id)
9
- @replies = ::Queue.new
10
9
  @connection = connection
11
10
  @id = id
11
+ @replies = ::Queue.new
12
12
  @consumers = {}
13
- @confirm = nil
14
- @last_confirmed = 0
15
13
  @closed = nil
16
- @on_return = nil
17
14
  @open = false
15
+ @on_return = nil
16
+ @confirm = nil
17
+ @unconfirmed = ::Queue.new
18
+ @unconfirmed_empty = ::Queue.new
19
+ @basic_gets = ::Queue.new
18
20
  end
19
21
 
20
- attr_reader :id, :consumers
22
+ def inspect
23
+ "#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?}"\
24
+ " consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
25
+ end
26
+
27
+ attr_reader :id
21
28
 
22
29
  def open
23
30
  return self if @open
24
31
 
32
+ @open = true
25
33
  write_bytes FrameBytes.channel_open(@id)
26
34
  expect(:channel_open_ok)
27
- @open = true
28
35
  self
29
36
  end
30
37
 
@@ -32,21 +39,25 @@ module AMQP
32
39
  return if @closed
33
40
 
34
41
  write_bytes FrameBytes.channel_close(@id, reason, code)
35
- expect :channel_close_ok
36
42
  @closed = [code, reason]
43
+ expect :channel_close_ok
44
+ @replies.close
45
+ @basic_gets.close
46
+ @unconfirmed_empty.close
47
+ @consumers.each_value(&:close)
37
48
  end
38
49
 
39
- # Called when closed by server
50
+ # Called when channel is closed by server
40
51
  def closed!(code, reason, classid, methodid)
41
- write_bytes FrameBytes.channel_close_ok(@id)
42
52
  @closed = [code, reason, classid, methodid]
43
53
  @replies.close
44
- @consumers.each { |_, q| q.close }
45
- @consumers.clear
54
+ @basic_gets.close
55
+ @unconfirmed_empty.close
56
+ @consumers.each_value(&:close)
46
57
  end
47
58
 
48
- def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, **args)
49
- write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, args)
59
+ def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
60
+ write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
50
61
  expect :exchange_declare_ok
51
62
  end
52
63
 
@@ -55,16 +66,18 @@ module AMQP
55
66
  expect :exchange_delete_ok
56
67
  end
57
68
 
58
- def exchange_bind(destination, source, binding_key, arguments = {})
69
+ def exchange_bind(destination, source, binding_key, arguments: {})
59
70
  write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
60
71
  expect :exchange_bind_ok
61
72
  end
62
73
 
63
- def exchange_unbind(destination, source, binding_key, arguments = {})
74
+ def exchange_unbind(destination, source, binding_key, arguments: {})
64
75
  write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
65
76
  expect :exchange_unbind_ok
66
77
  end
67
78
 
79
+ QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
80
+
68
81
  def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
69
82
  durable = false if name.empty?
70
83
  exclusive = true if name.empty?
@@ -72,11 +85,8 @@ module AMQP
72
85
 
73
86
  write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
74
87
  name, message_count, consumer_count = expect(:queue_declare_ok)
75
- {
76
- queue_name: name,
77
- message_count: message_count,
78
- consumer_count: consumer_count
79
- }
88
+
89
+ QueueOk.new(name, message_count, consumer_count)
80
90
  end
81
91
 
82
92
  def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
@@ -85,7 +95,7 @@ module AMQP
85
95
  message_count
86
96
  end
87
97
 
88
- def queue_bind(name, exchange, binding_key, arguments = {})
98
+ def queue_bind(name, exchange, binding_key, arguments: {})
89
99
  write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
90
100
  expect :queue_bind_ok
91
101
  end
@@ -95,44 +105,38 @@ module AMQP
95
105
  expect :queue_purge_ok unless no_wait
96
106
  end
97
107
 
98
- def queue_unbind(name, exchange, binding_key, arguments = {})
108
+ def queue_unbind(name, exchange, binding_key, arguments: {})
99
109
  write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
100
110
  expect :queue_unbind_ok
101
111
  end
102
112
 
103
113
  def basic_get(queue_name, no_ack: true)
104
114
  write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
105
- frame, *rest = @replies.shift
106
- case frame
107
- when :basic_get_ok
108
- delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
109
- body_size, properties = expect(:header)
110
- pos = 0
111
- body = String.new("", capacity: body_size)
112
- while pos < body_size
113
- body_part, = expect(:body)
114
- body += body_part
115
- pos += body_part.bytesize
116
- end
117
- Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
115
+ case (msg = @basic_gets.pop)
116
+ when Message then msg
118
117
  when :basic_get_empty then nil
119
118
  when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
120
- else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
121
119
  end
122
120
  end
123
121
 
124
122
  def basic_publish(body, exchange, routing_key, **properties)
125
123
  frame_max = @connection.frame_max - 8
126
124
  id = @id
125
+ mandatory = properties.delete(:mandatory) || false
126
+ case properties.delete(:persistent)
127
+ when true then properties[:delivery_mode] = 2
128
+ when false then properties[:delivery_mode] = 1
129
+ end
127
130
 
128
- if 0 < body.bytesize && body.bytesize <= frame_max
129
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
131
+ if body.bytesize.between?(1, frame_max)
132
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
130
133
  FrameBytes.header(id, body.bytesize, properties),
131
134
  FrameBytes.body(id, body)
132
- return @confirm ? @confirm += 1 : nil
135
+ @unconfirmed.push @confirm += 1 if @confirm
136
+ return
133
137
  end
134
138
 
135
- write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
139
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
136
140
  FrameBytes.header(id, body.bytesize, properties)
137
141
  pos = 0
138
142
  while pos < body.bytesize # split body into multiple frame_max frames
@@ -141,33 +145,31 @@ module AMQP
141
145
  write_bytes FrameBytes.body(id, body_part)
142
146
  pos += len
143
147
  end
144
- @confirm += 1 if @confirm
148
+ @unconfirmed.push @confirm += 1 if @confirm
149
+ nil
145
150
  end
146
151
 
147
152
  def basic_publish_confirm(body, exchange, routing_key, **properties)
148
153
  confirm_select(no_wait: true)
149
- id = basic_publish(body, exchange, routing_key, **properties)
150
- wait_for_confirm(id)
154
+ basic_publish(body, exchange, routing_key, **properties)
155
+ wait_for_confirms
151
156
  end
152
157
 
153
158
  # Consume from a queue
154
159
  # worker_threads: 0 => blocking, messages are executed in the thread calling this method
155
- def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
156
- worker_threads: 1)
160
+ def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
157
161
  write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
158
162
  tag, = expect(:basic_consume_ok)
159
163
  q = @consumers[tag] = ::Queue.new
160
- msgs = ::Queue.new
161
- Thread.new { recv_deliveries(tag, q, msgs) }
162
164
  if worker_threads.zero?
163
- while (msg = msgs.shift)
164
- yield msg
165
+ loop do
166
+ yield (q.pop || break)
165
167
  end
166
168
  else
167
169
  threads = Array.new(worker_threads) do
168
170
  Thread.new do
169
- while (msg = msgs.shift)
170
- yield(msg)
171
+ loop do
172
+ yield (q.pop || break)
171
173
  end
172
174
  end
173
175
  end
@@ -211,20 +213,37 @@ module AMQP
211
213
 
212
214
  write_bytes FrameBytes.confirm_select(@id, no_wait)
213
215
  expect :confirm_select_ok unless no_wait
214
- @confirms = ::Queue.new
215
216
  @confirm = 0
216
217
  end
217
218
 
218
- def wait_for_confirm(id)
219
- raise ArgumentError, "Confirm id has to a positive number" unless id&.positive?
220
- return true if @last_confirmed >= id
219
+ # Block until all publishes messages are confirmed
220
+ def wait_for_confirms
221
+ return true if @unconfirmed.empty?
222
+
223
+ case @unconfirmed_empty.pop
224
+ when true then true
225
+ when false then false
226
+ else raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
227
+ end
228
+ end
221
229
 
230
+ # Called by Connection when received ack/nack from server
231
+ def confirm(args)
232
+ ack_or_nack, delivery_tag, multiple = *args
222
233
  loop do
223
- ack, delivery_tag, multiple = @confirms.shift || break
224
- @last_confirmed = delivery_tag
225
- return ack if delivery_tag == id || (delivery_tag > id && multiple)
234
+ tag = @unconfirmed.pop(true)
235
+ break if tag == delivery_tag
236
+ next if multiple && tag < delivery_tag
237
+
238
+ @unconfirmed << tag # requeue
239
+ rescue ThreadError
240
+ break
241
+ end
242
+ return unless @unconfirmed.empty?
243
+
244
+ @unconfirmed_empty.num_waiting.times do
245
+ @unconfirmed_empty << (ack_or_nack == :ack)
226
246
  end
227
- false
228
247
  end
229
248
 
230
249
  def tx_select
@@ -242,51 +261,66 @@ module AMQP
242
261
  expect :tx_rollback_ok
243
262
  end
244
263
 
264
+ def on_return(&block)
265
+ @on_return = block
266
+ end
267
+
245
268
  def reply(args)
246
269
  @replies.push(args)
247
270
  end
248
271
 
249
- def confirm(args)
250
- @confirms.push(args)
272
+ def message_returned(reply_code, reply_text, exchange, routing_key)
273
+ @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
251
274
  end
252
275
 
253
- def message_returned(reply_code, reply_text, exchange, routing_key)
254
- Thread.new do
255
- body_size, properties = expect(:header)
256
- body = String.new("", capacity: body_size)
257
- while body.bytesize < body_size
258
- body_part, = expect(:body)
259
- body += body_part
260
- end
261
- msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
276
+ def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
277
+ @next_msg = Message.new(self, delivery_tag, exchange, routing_key, nil, "", redelivered, consumer_tag)
278
+ end
262
279
 
263
- if @on_return
264
- @on_return.call(msg)
265
- else
266
- puts "[WARN] Message returned: #{msg.inspect}"
267
- end
280
+ def basic_get_empty
281
+ @basic_gets.push :basic_get_empty
282
+ end
283
+
284
+ def header_delivered(body_size, properties)
285
+ @next_msg.properties = properties
286
+ if body_size.zero?
287
+ next_message_finished!
288
+ else
289
+ @next_body = StringIO.new(String.new(capacity: body_size))
290
+ @next_body_size = body_size
268
291
  end
269
292
  end
270
293
 
271
- def on_return(&block)
272
- @on_return = block
294
+ def body_delivered(body_part)
295
+ @next_body.write(body_part)
296
+ return unless @next_body.pos == @next_body_size
297
+
298
+ @next_msg.body = @next_body.string
299
+ next_message_finished!
300
+ end
301
+
302
+ def close_consumer(tag)
303
+ @consumers.fetch(tag).close
273
304
  end
274
305
 
275
306
  private
276
307
 
277
- def recv_deliveries(consumer_tag, deliver_queue, msgs)
278
- loop do
279
- _, delivery_tag, redelivered, exchange, routing_key = deliver_queue.shift || raise(ClosedQueueError)
280
- body_size, properties = expect(:header)
281
- body = String.new("", capacity: body_size)
282
- while body.bytesize < body_size
283
- body_part, = expect(:body)
284
- body += body_part
308
+ def next_message_finished!
309
+ next_msg = @next_msg
310
+ if next_msg.is_a? ReturnMessage
311
+ if @on_return
312
+ Thread.new { @on_return.call(next_msg) }
313
+ else
314
+ warn "AMQP-Client message returned: #{msg.inspect}"
285
315
  end
286
- msgs.push Message.new(self, delivery_tag, exchange, routing_key, properties, body, redelivered, consumer_tag)
316
+ elsif next_msg.consumer_tag.nil?
317
+ @basic_gets.push next_msg
318
+ else
319
+ Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
320
+ consumer.push next_msg
287
321
  end
288
322
  ensure
289
- msgs.close
323
+ @next_msg = @next_body = @next_body_size = nil
290
324
  end
291
325
 
292
326
  def write_bytes(*bytes)
@@ -296,13 +330,11 @@ module AMQP
296
330
  end
297
331
 
298
332
  def expect(expected_frame_type)
299
- loop do
300
- frame_type, *args = @replies.shift
301
- raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
302
- return args if frame_type == expected_frame_type
333
+ frame_type, *args = @replies.pop
334
+ raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
335
+ raise AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
303
336
 
304
- @replies.push [frame_type, *args]
305
- end
337
+ args
306
338
  end
307
339
  end
308
340
  end
@@ -10,12 +10,10 @@ require_relative "./errors"
10
10
  module AMQP
11
11
  # Represents a single AMQP connection
12
12
  class Connection
13
- def self.connect(uri, **options)
14
- read_loop_thread = options[:read_loop_thread] || true
15
-
13
+ def self.connect(uri, read_loop_thread: true, **options)
16
14
  uri = URI.parse(uri)
17
15
  tls = uri.scheme == "amqps"
18
- port = port_from_env || uri.port || (@tls ? 5671 : 5672)
16
+ port = port_from_env || uri.port || (tls ? 5671 : 5672)
19
17
  host = uri.host || "localhost"
20
18
  user = uri.user || "guest"
21
19
  password = uri.password || "guest"
@@ -25,10 +23,16 @@ module AMQP
25
23
  socket = Socket.tcp host, port, connect_timeout: 20, resolv_timeout: 5
26
24
  enable_tcp_keepalive(socket)
27
25
  if tls
26
+ cert_store = OpenSSL::X509::Store.new
27
+ cert_store.set_default_paths
28
28
  context = OpenSSL::SSL::SSLContext.new
29
+ context.cert_store = cert_store
29
30
  context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless [false, "false", "none"].include? options[:verify_peer]
30
31
  socket = OpenSSL::SSL::SSLSocket.new(socket, context)
31
32
  socket.sync_close = true # closing the TLS socket also closes the TCP socket
33
+ socket.hostname = host # SNI host
34
+ socket.connect
35
+ socket.post_connection_check(host) || raise(AMQP::Client::Error, "TLS certificate hostname doesn't match requested")
32
36
  end
33
37
  channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
34
38
  Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
@@ -36,22 +40,35 @@ module AMQP
36
40
 
37
41
  def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
38
42
  @socket = socket
39
- @channel_max = channel_max
43
+ @channel_max = channel_max.zero? ? 65_536 : channel_max
40
44
  @frame_max = frame_max
41
45
  @heartbeat = heartbeat
42
46
  @channels = {}
43
47
  @closed = false
44
48
  @replies = Queue.new
49
+ @write_lock = Mutex.new
45
50
  Thread.new { read_loop } if read_loop_thread
46
51
  end
47
52
 
48
53
  attr_reader :frame_max
49
54
 
55
+ def inspect
56
+ "#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
57
+ end
58
+
50
59
  def channel(id = nil)
60
+ raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
61
+ raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
62
+
51
63
  if id
52
64
  ch = @channels[id] ||= Channel.new(self, id)
53
65
  else
54
- id = 1.upto(@channel_max) { |i| break i unless @channels.key? i }
66
+ id = nil
67
+ 1.upto(@channel_max) do |i|
68
+ break id = i unless @channels.key? i
69
+ end
70
+ raise AMQP::Client::Error, "Max channels reached" if id.nil?
71
+
55
72
  ch = @channels[id] = Channel.new(self, id)
56
73
  end
57
74
  ch.open
@@ -70,9 +87,10 @@ module AMQP
70
87
  def close(reason = "", code = 200)
71
88
  return if @closed
72
89
 
90
+ @closed = true
73
91
  write_bytes FrameBytes.connection_close(code, reason)
92
+ @channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
74
93
  expect(:close_ok)
75
- @closed = true
76
94
  end
77
95
 
78
96
  def closed?
@@ -80,47 +98,51 @@ module AMQP
80
98
  end
81
99
 
82
100
  def write_bytes(*bytes)
83
- @socket.write(*bytes)
101
+ if @socket.is_a? OpenSSL::SSL::SSLSocket
102
+ @write_lock.synchronize do
103
+ @socket.write(*bytes)
104
+ end
105
+ else
106
+ @socket.write(*bytes)
107
+ end
84
108
  rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
85
- raise AMQP::Client::Error.new("Could not write to socket", cause: e)
109
+ raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
86
110
  end
87
111
 
88
112
  # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
89
113
  def read_loop
114
+ # read more often than write so that channel errors crop up early
115
+ Thread.current.priority += 1
90
116
  socket = @socket
91
117
  frame_max = @frame_max
92
- buffer = String.new(capacity: frame_max)
118
+ frame_start = String.new(capacity: 7)
119
+ frame_buffer = String.new(capacity: frame_max)
93
120
  loop do
94
- begin
95
- socket.readpartial(frame_max, buffer)
96
- rescue IOError, OpenSSL::OpenSSLError, SystemCallError
97
- break
121
+ socket.read(7, frame_start)
122
+ type, channel_id, frame_size = frame_start.unpack("C S> L>")
123
+ if frame_size > frame_max
124
+ raise AMQP::Client::Error, "Frame size #{frame_size} is larger than negotiated max frame size #{frame_max}"
98
125
  end
99
126
 
100
- pos = 0
101
- while pos < buffer.bytesize
102
- buffer += socket.read(pos + 8 - buffer.bytesize) if pos + 8 > buffer.bytesize
103
- type, channel_id, frame_size = buffer.byteslice(pos, 7).unpack("C S> L>")
104
- if frame_size > frame_max
105
- raise AMQP::Client::Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}"
106
- end
127
+ # read the frame content
128
+ socket.read(frame_size, frame_buffer)
107
129
 
108
- frame_end_pos = pos + 7 + frame_size
109
- buffer += socket.read(frame_end_pos - buffer.bytesize + 1) if frame_end_pos + 1 > buffer.bytesize
110
- frame_end = buffer[frame_end_pos].ord
111
- raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
130
+ # make sure that the frame end is correct
131
+ frame_end = socket.readchar.ord
132
+ raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
112
133
 
113
- buf = buffer.byteslice(pos, frame_size + 8)
114
- pos += frame_size + 8
115
- parse_frame(type, channel_id, frame_size, buf) || return
116
- end
134
+ # parse the frame, will return false if a close frame was received
135
+ parse_frame(type, channel_id, frame_size, frame_buffer) || return
117
136
  end
137
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
138
+ warn "AMQP-Client read error: #{e.inspect}"
139
+ nil # ignore read errors
118
140
  ensure
119
141
  @closed = true
120
142
  @replies.close
121
143
  begin
122
144
  @socket.close
123
- rescue IOError
145
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError
124
146
  nil
125
147
  end
126
148
  end
@@ -130,20 +152,26 @@ module AMQP
130
152
  def parse_frame(type, channel_id, frame_size, buf)
131
153
  case type
132
154
  when 1 # method frame
133
- class_id, method_id = buf.unpack("@7 S> S>")
155
+ class_id, method_id = buf.unpack("S> S>")
134
156
  case class_id
135
157
  when 10 # connection
136
158
  raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
137
159
 
138
160
  case method_id
139
161
  when 50 # connection#close
140
- code, text_len = buf.unpack("@11 S> C")
141
- text = buf.byteslice(14, text_len).force_encoding("utf-8")
142
- error_class_id, error_method_id = buf.byteslice(14 + text_len, 4).unpack("S> S>")
143
- warn "Connection closed #{code} #{text} #{error_class_id} #{error_method_id}"
144
- write_bytes FrameBytes.connection_close_ok
162
+ @closed = true
163
+ code, text_len = buf.unpack("@4 S> C")
164
+ text = buf.byteslice(7, text_len).force_encoding("utf-8")
165
+ error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
166
+ @channels.each_value { |ch| ch.closed!(code, text, error_class_id, error_method_id) }
167
+ begin
168
+ write_bytes FrameBytes.connection_close_ok
169
+ rescue AMQP::Client::Error
170
+ nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
171
+ end
145
172
  return false
146
173
  when 51 # connection#close-ok
174
+ @closed = true
147
175
  @replies.push [:close_ok]
148
176
  return false
149
177
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
@@ -153,13 +181,15 @@ module AMQP
153
181
  when 11 # channel#open-ok
154
182
  @channels[channel_id].reply [:channel_open_ok]
155
183
  when 40 # channel#close
156
- reply_code, reply_text_len = buf.unpack("@11 S> C")
157
- reply_text = buf.byteslice(14, reply_text_len).force_encoding("utf-8")
158
- classid, methodid = buf.byteslice(14 + reply_text_len, 4).unpack("S> S>")
184
+ reply_code, reply_text_len = buf.unpack("@4 S> C")
185
+ reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
186
+ classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
159
187
  channel = @channels.delete(channel_id)
160
188
  channel.closed!(reply_code, reply_text, classid, methodid)
189
+ write_bytes FrameBytes.channel_close_ok(channel_id)
161
190
  when 41 # channel#close-ok
162
- @channels[channel_id].reply [:channel_close_ok]
191
+ channel = @channels.delete(channel_id)
192
+ channel.reply [:channel_close_ok]
163
193
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
164
194
  end
165
195
  when 40 # exchange
@@ -177,16 +207,16 @@ module AMQP
177
207
  when 50 # queue
178
208
  case method_id
179
209
  when 11 # declare-ok
180
- queue_name_len = buf.unpack1("@11 C")
181
- queue_name = buf.byteslice(12, queue_name_len).force_encoding("utf-8")
182
- message_count, consumer_count = buf.byteslice(12 + queue_name_len, 8).unpack1("L> L>")
210
+ queue_name_len = buf.unpack1("@4 C")
211
+ queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
212
+ message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
183
213
  @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
184
214
  when 21 # bind-ok
185
215
  @channels[channel_id].reply [:queue_bind_ok]
186
216
  when 31 # purge-ok
187
217
  @channels[channel_id].reply [:queue_purge_ok]
188
218
  when 41 # delete-ok
189
- message_count = buf.unpack1("@11 L>")
219
+ message_count = buf.unpack1("@4 L>")
190
220
  @channels[channel_id].reply [:queue_delete, message_count]
191
221
  when 51 # unbind-ok
192
222
  @channels[channel_id].reply [:queue_unbind_ok]
@@ -197,22 +227,22 @@ module AMQP
197
227
  when 11 # qos-ok
198
228
  @channels[channel_id].reply [:basic_qos_ok]
199
229
  when 21 # consume-ok
200
- tag_len = buf.unpack1("@11 C")
201
- tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
230
+ tag_len = buf.unpack1("@4 C")
231
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
202
232
  @channels[channel_id].reply [:basic_consume_ok, tag]
203
233
  when 30 # cancel
204
- tag_len = buf.unpack1("@11 C")
205
- tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
206
- no_wait = buf[12 + tag_len].ord
207
- @channels[channel_id].consumers.fetch(tag).close
234
+ tag_len = buf.unpack1("@4 C")
235
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
236
+ no_wait = buf[5 + tag_len].ord
237
+ @channels[channel_id].close_consumer(tag)
208
238
  write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
209
239
  when 31 # cancel-ok
210
- tag_len = buf.unpack1("@11 C")
211
- tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
240
+ tag_len = buf.unpack1("@4 C")
241
+ tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
212
242
  @channels[channel_id].reply [:basic_cancel_ok, tag]
213
243
  when 50 # return
214
- reply_code, reply_text_len = buf.unpack("@11 S> C")
215
- pos = 14
244
+ reply_code, reply_text_len = buf.unpack("@4 S> C")
245
+ pos = 7
216
246
  reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
217
247
  pos += reply_text_len
218
248
  exchange_len = buf[pos].ord
@@ -224,9 +254,9 @@ module AMQP
224
254
  routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
225
255
  @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
226
256
  when 60 # deliver
227
- ctag_len = buf[11].ord
228
- consumer_tag = buf.byteslice(12, ctag_len).force_encoding("utf-8")
229
- pos = 12 + ctag_len
257
+ ctag_len = buf[4].ord
258
+ consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
259
+ pos = 5 + ctag_len
230
260
  delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
231
261
  pos += 8 + 1 + 1
232
262
  exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
@@ -234,38 +264,27 @@ module AMQP
234
264
  rk_len = buf[pos].ord
235
265
  pos += 1
236
266
  routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
237
- loop do
238
- if (consumer = @channels[channel_id].consumers[consumer_tag])
239
- consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
240
- break
241
- else
242
- Thread.pass
243
- end
244
- end
267
+ @channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
245
268
  when 71 # get-ok
246
- delivery_tag, redelivered, exchange_len = buf.unpack("@11 Q> C C")
247
- pos = 21
269
+ delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
270
+ pos = 14
248
271
  exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
249
272
  pos += exchange_len
250
273
  routing_key_len = buf[pos].ord
251
274
  pos += 1
252
275
  routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
253
276
  pos += routing_key_len
254
- message_count = buf.byteslice(pos, 4).unpack1("L>")
255
- redelivered = redelivered == 1
256
- @channels[channel_id].reply [:basic_get_ok, delivery_tag, exchange, routing_key, message_count, redelivered]
277
+ _message_count = buf.byteslice(pos, 4).unpack1("L>")
278
+ @channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
257
279
  when 72 # get-empty
258
- @channels[channel_id].reply [:basic_get_empty]
280
+ @channels[channel_id].basic_get_empty
259
281
  when 80 # ack
260
- delivery_tag, multiple = buf.unpack("@11 Q> C")
261
- @channels[channel_id].confirm [:ack, delivery_tag, multiple]
262
- when 90 # reject
263
- delivery_tag, requeue = buf.unpack("@11 Q> C")
264
- @channels[channel_id].confirm [:reject, delivery_tag, requeue == 1]
282
+ delivery_tag, multiple = buf.unpack("@4 Q> C")
283
+ @channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
265
284
  when 111 # recover-ok
266
285
  @channels[channel_id].reply [:basic_recover_ok]
267
286
  when 120 # nack
268
- delivery_tag, multiple, requeue = buf.unpack("@11 Q> C C")
287
+ delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
269
288
  @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
270
289
  else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
271
290
  end
@@ -288,20 +307,19 @@ module AMQP
288
307
  else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
289
308
  end
290
309
  when 2 # header
291
- body_size = buf.unpack1("@11 Q>")
292
- properties = Properties.decode(buf.byteslice(19, buf.bytesize - 20))
293
- @channels[channel_id].reply [:header, body_size, properties]
310
+ body_size = buf.unpack1("@4 Q>")
311
+ properties = Properties.decode(buf.byteslice(12, buf.bytesize - 12))
312
+ @channels[channel_id].header_delivered body_size, properties
294
313
  when 3 # body
295
- body = buf.byteslice(7, frame_size)
296
- @channels[channel_id].reply [:body, body]
314
+ @channels[channel_id].body_delivered buf
297
315
  else raise AMQP::Client::UnsupportedFrameType, type
298
316
  end
299
317
  true
300
318
  end
301
319
 
302
320
  def expect(expected_frame_type)
303
- frame_type, args = @replies.shift
304
- frame_type == expected_frame_type || raise(UnexpectedFrame.new(expected_frame_type, frame_type))
321
+ frame_type, args = @replies.pop
322
+ frame_type == expected_frame_type || raise(AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type))
305
323
  args
306
324
  end
307
325
 
@@ -317,7 +335,7 @@ module AMQP
317
335
  end
318
336
 
319
337
  type, channel_id, frame_size = buf.unpack("C S> L>")
320
- frame_end = buf.unpack1("@#{frame_size + 7} C")
338
+ frame_end = buf[frame_size + 7].ord
321
339
  raise UnexpectedFrameEndError, frame_end if frame_end != 206
322
340
 
323
341
  case type
@@ -329,7 +347,8 @@ module AMQP
329
347
 
330
348
  case method_id
331
349
  when 10 # connection#start
332
- properties = CLIENT_PROPERTIES.merge({ connection_name: options[:connection_name] })
350
+ conn_name = options[:connection_name] || $PROGRAM_NAME
351
+ properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
333
352
  socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
334
353
  when 30 # connection#tune
335
354
  channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
@@ -363,7 +382,7 @@ module AMQP
363
382
  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
364
383
  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
365
384
  rescue StandardError => e
366
- warn "amqp-client: Could not enable TCP keepalive on socket. #{e.inspect}"
385
+ warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
367
386
  end
368
387
 
369
388
  def self.port_from_env
@@ -7,7 +7,7 @@ module AMQP
7
7
  end
8
8
 
9
9
  def reject(requeue: false)
10
- channel.basic_reject(delivery_tag, requeue)
10
+ channel.basic_reject(delivery_tag, requeue: requeue)
11
11
  end
12
12
  end
13
13
 
@@ -38,7 +38,8 @@ module AMQP
38
38
  end
39
39
 
40
40
  if delivery_mode
41
- headers.is_a?(Integer) || raise(ArgumentError, "delivery_mode must be an int")
41
+ delivery_mode.is_a?(Integer) || raise(ArgumentError, "delivery_mode must be an int")
42
+ delivery_mode.between?(0, 2) || raise(ArgumentError, "delivery_mode must be be between 0 and 2")
42
43
 
43
44
  flags |= (1 << 12)
44
45
  arr << delivery_mode
@@ -69,7 +70,8 @@ module AMQP
69
70
  end
70
71
 
71
72
  if expiration
72
- expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string")
73
+ self.expiration = expiration.to_s if expiration.is_a?(Integer)
74
+ expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
73
75
 
74
76
  flags |= (1 << 8)
75
77
  arr << expiration.bytesize << expiration
@@ -85,7 +87,7 @@ module AMQP
85
87
  end
86
88
 
87
89
  if timestamp
88
- timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be a time")
90
+ timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
89
91
 
90
92
  flags |= (1 << 6)
91
93
  arr << timestamp.to_i
@@ -2,6 +2,6 @@
2
2
 
3
3
  module AMQP
4
4
  class Client
5
- VERSION = "0.2.1"
5
+ VERSION = "1.0.0"
6
6
  end
7
7
  end
data/lib/amqp/client.rb CHANGED
@@ -12,31 +12,41 @@ module AMQP
12
12
  @options = options
13
13
 
14
14
  @queues = {}
15
+ @exchanges = {}
15
16
  @subscriptions = Set.new
16
17
  @connq = SizedQueue.new(1)
17
18
  end
18
19
 
20
+ # Opens an AMQP connection, does not try to reconnect
19
21
  def connect(read_loop_thread: true)
20
- Connection.connect(@uri, **@options.merge(read_loop_thread: read_loop_thread))
22
+ Connection.connect(@uri, read_loop_thread: read_loop_thread, **@options)
21
23
  end
22
24
 
25
+ # Opens an AMQP connection using the high level API, will try to reconnect
23
26
  def start
24
27
  @stopped = false
25
- Thread.new do
28
+ Thread.new(connect(read_loop_thread: false)) do |conn|
29
+ Thread.abort_on_exception = true # Raising an unhandled exception is a bug
26
30
  loop do
27
31
  break if @stopped
28
32
 
29
- conn = connect(read_loop_thread: false)
33
+ conn ||= connect(read_loop_thread: false)
30
34
  Thread.new do
31
35
  # restore connection in another thread, read_loop have to run
32
36
  conn.channel(1) # reserve channel 1 for publishes
33
- @subscriptions.each { |args| subscribe(*args) }
37
+ @subscriptions.each do |queue_name, no_ack, prefetch, wt, args, blk|
38
+ ch = conn.channel
39
+ ch.basic_qos(prefetch)
40
+ ch.basic_consume(queue_name, no_ack: no_ack, worker_threads: wt, arguments: args, &blk)
41
+ end
34
42
  @connq << conn
35
43
  end
36
44
  conn.read_loop # blocks until connection is closed, then reconnect
37
- rescue => e
45
+ rescue AMQP::Client::Error => e
38
46
  warn "AMQP-Client reconnect error: #{e.inspect}"
39
47
  sleep @options[:reconnect_interval] || 1
48
+ ensure
49
+ conn = nil
40
50
  end
41
51
  end
42
52
  self
@@ -49,21 +59,58 @@ module AMQP
49
59
  nil
50
60
  end
51
61
 
52
- def queue(name, arguments: {})
62
+ def queue(name, durable: true, exclusive: false, auto_delete: false, arguments: {})
53
63
  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
54
64
 
55
65
  @queues.fetch(name) do
56
66
  with_connection do |conn|
57
67
  conn.with_channel do |ch| # use a temp channel in case the declaration fails
58
- ch.queue_declare(name, arguments: arguments)
68
+ ch.queue_declare(name, durable: durable, exclusive: exclusive, auto_delete: auto_delete, arguments: arguments)
59
69
  end
60
70
  end
61
71
  @queues[name] = Queue.new(self, name)
62
72
  end
63
73
  end
64
74
 
75
+ def exchange(name, type, durable: true, auto_delete: false, internal: false, arguments: {})
76
+ @exchanges.fetch(name) do
77
+ with_connection do |conn|
78
+ conn.with_channel do |ch|
79
+ ch.exchange_declare(name, type, durable: durable, auto_delete: auto_delete, internal: internal, arguments: arguments)
80
+ end
81
+ end
82
+ @exchanges[name] = Exchange.new(self, name)
83
+ end
84
+ end
85
+
86
+ # High level representation of an exchange
87
+ class Exchange
88
+ def initialize(client, name)
89
+ @client = client
90
+ @name = name
91
+ end
92
+
93
+ def publish(body, routing_key, arguments: {})
94
+ @client.publish(body, @name, routing_key, arguments: arguments)
95
+ end
96
+
97
+ # Bind to another exchange
98
+ def bind(exchange, routing_key, arguments: {})
99
+ @client.exchange_bind(@name, exchange, routing_key, arguments: arguments)
100
+ end
101
+
102
+ # Unbind from another exchange
103
+ def unbind(exchange, routing_key, arguments: {})
104
+ @client.exchange_unbind(@name, exchange, routing_key, arguments: arguments)
105
+ end
106
+
107
+ def delete
108
+ @client.delete_exchange(@name)
109
+ end
110
+ end
111
+
65
112
  def subscribe(queue_name, no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
66
- @subscriptions.add? [queue_name, no_ack, prefetch, arguments, blk]
113
+ @subscriptions.add? [queue_name, no_ack, prefetch, worker_threads, arguments, blk]
67
114
 
68
115
  with_connection do |conn|
69
116
  ch = conn.channel
@@ -74,28 +121,49 @@ module AMQP
74
121
  end
75
122
  end
76
123
 
124
+ # Publish a (persistent) message and wait for confirmation
77
125
  def publish(body, exchange, routing_key, **properties)
78
126
  with_connection do |conn|
79
- # Use channel 1 for publishes
127
+ properties = { delivery_mode: 2 }.merge!(properties)
80
128
  conn.channel(1).basic_publish_confirm(body, exchange, routing_key, **properties)
81
- rescue
82
- conn.channel(1) # reopen channel 1 if it raised
83
- raise
84
129
  end
85
- rescue => e
86
- warn "AMQP-Client error publishing, retrying (#{e.inspect})"
87
- retry
88
130
  end
89
131
 
90
- def bind(queue, exchange, routing_key, **headers)
132
+ # Publish a (persistent) message but don't wait for a confirmation
133
+ def publish_and_forget(body, exchange, routing_key, **properties)
91
134
  with_connection do |conn|
92
- conn.channel(1).queue_bind(queue, exchange, routing_key, **headers)
135
+ properties = { delivery_mode: 2 }.merge!(properties)
136
+ conn.channel(1).basic_publish(body, exchange, routing_key, **properties)
93
137
  end
94
138
  end
95
139
 
96
- def unbind(queue, exchange, routing_key, **headers)
140
+ def wait_for_confirms
97
141
  with_connection do |conn|
98
- conn.channel(1).queue_unbind(queue, exchange, routing_key, **headers)
142
+ conn.channel(1).wait_for_confirms
143
+ end
144
+ end
145
+
146
+ def bind(queue, exchange, routing_key, arguments: {})
147
+ with_connection do |conn|
148
+ conn.channel(1).queue_bind(queue, exchange, routing_key, arguments: arguments)
149
+ end
150
+ end
151
+
152
+ def unbind(queue, exchange, routing_key, arguments: {})
153
+ with_connection do |conn|
154
+ conn.channel(1).queue_unbind(queue, exchange, routing_key, arguments: arguments)
155
+ end
156
+ end
157
+
158
+ def exchange_bind(destination, source, routing_key, arguments: {})
159
+ with_connection do |conn|
160
+ conn.channel(1).exchange_bind(destination, source, routing_key, arguments: arguments)
161
+ end
162
+ end
163
+
164
+ def exchange_unbind(destination, source, routing_key, arguments: {})
165
+ with_connection do |conn|
166
+ conn.channel(1).exchange_unbind(destination, source, routing_key, arguments: arguments)
99
167
  end
100
168
  end
101
169
 
@@ -105,9 +173,17 @@ module AMQP
105
173
  end
106
174
  end
107
175
 
108
- def delete_queue(queue)
176
+ def delete_queue(name)
177
+ with_connection do |conn|
178
+ conn.channel(1).queue_delete(name)
179
+ @queues.delete(name)
180
+ end
181
+ end
182
+
183
+ def delete_exchange(name)
109
184
  with_connection do |conn|
110
- conn.channel(1).queue_delete(queue)
185
+ conn.channel(1).exchange_delete(name)
186
+ @exchanges.delete(name)
111
187
  end
112
188
  end
113
189
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amqp-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carl Hörberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-19 00:00:00.000000000 Z
11
+ date: 2021-08-27 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Work in progress
14
14
  email:
@@ -61,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
61
61
  - !ruby/object:Gem::Version
62
62
  version: '0'
63
63
  requirements: []
64
- rubygems_version: 3.2.15
64
+ rubygems_version: 3.2.22
65
65
  signing_key:
66
66
  specification_version: 4
67
67
  summary: AMQP 0-9-1 client