amqp-client 0.3.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +4 -4
- data/lib/amqp/client/channel.rb +94 -74
- data/lib/amqp/client/connection.rb +95 -80
- data/lib/amqp/client/message.rb +1 -1
- data/lib/amqp/client/properties.rb +4 -3
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +97 -21
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5e6dd3fef130c286e97eadd9bb6d3d4de0fa67599361604cec3560879b9b4bdf
|
4
|
+
data.tar.gz: c2ce00f897c68d3085427854b08cc98d3487b40bb5327ce38bb216c98357e366
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9b62e8ee74ec542be5617b08fbd0e36e5fc16294f99cd0ec61781c7c7425005c426e8358a74abf45a1a24ecb88451b94ef50ee7524403ac7097748b2e94b55b5
|
7
|
+
data.tar.gz: 5c3e63369578923e20b441fbe9728b374e18150c75ed155291b802c42f80f668b704b39e80b144b90346a8d769d9b71f031b9fca712e6ac52c100a88e46380e3
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,26 @@
|
|
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
|
+
|
3
24
|
## [0.3.0] - 2021-08-20
|
4
25
|
|
5
26
|
- Channel#wait_for_confirms is a smarter way of waiting for publish confirms
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# AMQP::Client
|
2
2
|
|
3
|
-
An AMQP 0-9-1 client
|
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
|
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
|
50
|
-
# between
|
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
|
data/lib/amqp/client/channel.rb
CHANGED
@@ -16,16 +16,22 @@ module AMQP
|
|
16
16
|
@confirm = nil
|
17
17
|
@unconfirmed = ::Queue.new
|
18
18
|
@unconfirmed_empty = ::Queue.new
|
19
|
+
@basic_gets = ::Queue.new
|
19
20
|
end
|
20
21
|
|
21
|
-
|
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
|
22
28
|
|
23
29
|
def open
|
24
30
|
return self if @open
|
25
31
|
|
32
|
+
@open = true
|
26
33
|
write_bytes FrameBytes.channel_open(@id)
|
27
34
|
expect(:channel_open_ok)
|
28
|
-
@open = true
|
29
35
|
self
|
30
36
|
end
|
31
37
|
|
@@ -33,21 +39,25 @@ module AMQP
|
|
33
39
|
return if @closed
|
34
40
|
|
35
41
|
write_bytes FrameBytes.channel_close(@id, reason, code)
|
36
|
-
expect :channel_close_ok
|
37
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)
|
38
48
|
end
|
39
49
|
|
40
|
-
# Called when closed by server
|
50
|
+
# Called when channel is closed by server
|
41
51
|
def closed!(code, reason, classid, methodid)
|
42
|
-
write_bytes FrameBytes.channel_close_ok(@id)
|
43
52
|
@closed = [code, reason, classid, methodid]
|
44
53
|
@replies.close
|
45
|
-
@
|
46
|
-
@
|
54
|
+
@basic_gets.close
|
55
|
+
@unconfirmed_empty.close
|
56
|
+
@consumers.each_value(&:close)
|
47
57
|
end
|
48
58
|
|
49
|
-
def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false,
|
50
|
-
write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal,
|
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)
|
51
61
|
expect :exchange_declare_ok
|
52
62
|
end
|
53
63
|
|
@@ -56,16 +66,18 @@ module AMQP
|
|
56
66
|
expect :exchange_delete_ok
|
57
67
|
end
|
58
68
|
|
59
|
-
def exchange_bind(destination, source, binding_key, arguments
|
69
|
+
def exchange_bind(destination, source, binding_key, arguments: {})
|
60
70
|
write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
|
61
71
|
expect :exchange_bind_ok
|
62
72
|
end
|
63
73
|
|
64
|
-
def exchange_unbind(destination, source, binding_key, arguments
|
74
|
+
def exchange_unbind(destination, source, binding_key, arguments: {})
|
65
75
|
write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
|
66
76
|
expect :exchange_unbind_ok
|
67
77
|
end
|
68
78
|
|
79
|
+
QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
|
80
|
+
|
69
81
|
def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
|
70
82
|
durable = false if name.empty?
|
71
83
|
exclusive = true if name.empty?
|
@@ -73,11 +85,8 @@ module AMQP
|
|
73
85
|
|
74
86
|
write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
|
75
87
|
name, message_count, consumer_count = expect(:queue_declare_ok)
|
76
|
-
|
77
|
-
|
78
|
-
message_count: message_count,
|
79
|
-
consumer_count: consumer_count
|
80
|
-
}
|
88
|
+
|
89
|
+
QueueOk.new(name, message_count, consumer_count)
|
81
90
|
end
|
82
91
|
|
83
92
|
def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
|
@@ -86,7 +95,7 @@ module AMQP
|
|
86
95
|
message_count
|
87
96
|
end
|
88
97
|
|
89
|
-
def queue_bind(name, exchange, binding_key, arguments
|
98
|
+
def queue_bind(name, exchange, binding_key, arguments: {})
|
90
99
|
write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
|
91
100
|
expect :queue_bind_ok
|
92
101
|
end
|
@@ -96,29 +105,17 @@ module AMQP
|
|
96
105
|
expect :queue_purge_ok unless no_wait
|
97
106
|
end
|
98
107
|
|
99
|
-
def queue_unbind(name, exchange, binding_key, arguments
|
108
|
+
def queue_unbind(name, exchange, binding_key, arguments: {})
|
100
109
|
write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
|
101
110
|
expect :queue_unbind_ok
|
102
111
|
end
|
103
112
|
|
104
113
|
def basic_get(queue_name, no_ack: true)
|
105
114
|
write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
|
106
|
-
|
107
|
-
|
108
|
-
when :basic_get_ok
|
109
|
-
delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
|
110
|
-
body_size, properties = expect(:header)
|
111
|
-
pos = 0
|
112
|
-
body = String.new("", capacity: body_size)
|
113
|
-
while pos < body_size
|
114
|
-
body_part, = expect(:body)
|
115
|
-
body += body_part
|
116
|
-
pos += body_part.bytesize
|
117
|
-
end
|
118
|
-
Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
|
115
|
+
case (msg = @basic_gets.pop)
|
116
|
+
when Message then msg
|
119
117
|
when :basic_get_empty then nil
|
120
118
|
when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
|
121
|
-
else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
|
122
119
|
end
|
123
120
|
end
|
124
121
|
|
@@ -126,8 +123,12 @@ module AMQP
|
|
126
123
|
frame_max = @connection.frame_max - 8
|
127
124
|
id = @id
|
128
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
|
129
130
|
|
130
|
-
if
|
131
|
+
if body.bytesize.between?(1, frame_max)
|
131
132
|
write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
|
132
133
|
FrameBytes.header(id, body.bytesize, properties),
|
133
134
|
FrameBytes.body(id, body)
|
@@ -156,22 +157,19 @@ module AMQP
|
|
156
157
|
|
157
158
|
# Consume from a queue
|
158
159
|
# worker_threads: 0 => blocking, messages are executed in the thread calling this method
|
159
|
-
def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
|
160
|
-
worker_threads: 1)
|
160
|
+
def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
|
161
161
|
write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
|
162
162
|
tag, = expect(:basic_consume_ok)
|
163
163
|
q = @consumers[tag] = ::Queue.new
|
164
|
-
msgs = ::Queue.new
|
165
|
-
Thread.new { recv_deliveries(tag, q, msgs) }
|
166
164
|
if worker_threads.zero?
|
167
|
-
|
168
|
-
yield
|
165
|
+
loop do
|
166
|
+
yield (q.pop || break)
|
169
167
|
end
|
170
168
|
else
|
171
169
|
threads = Array.new(worker_threads) do
|
172
170
|
Thread.new do
|
173
|
-
|
174
|
-
yield(
|
171
|
+
loop do
|
172
|
+
yield (q.pop || break)
|
175
173
|
end
|
176
174
|
end
|
177
175
|
end
|
@@ -222,9 +220,14 @@ module AMQP
|
|
222
220
|
def wait_for_confirms
|
223
221
|
return true if @unconfirmed.empty?
|
224
222
|
|
225
|
-
@unconfirmed_empty.pop
|
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
|
226
228
|
end
|
227
229
|
|
230
|
+
# Called by Connection when received ack/nack from server
|
228
231
|
def confirm(args)
|
229
232
|
ack_or_nack, delivery_tag, multiple = *args
|
230
233
|
loop do
|
@@ -239,7 +242,7 @@ module AMQP
|
|
239
242
|
return unless @unconfirmed.empty?
|
240
243
|
|
241
244
|
@unconfirmed_empty.num_waiting.times do
|
242
|
-
@unconfirmed_empty << ack_or_nack == :ack
|
245
|
+
@unconfirmed_empty << (ack_or_nack == :ack)
|
243
246
|
end
|
244
247
|
end
|
245
248
|
|
@@ -258,47 +261,66 @@ module AMQP
|
|
258
261
|
expect :tx_rollback_ok
|
259
262
|
end
|
260
263
|
|
264
|
+
def on_return(&block)
|
265
|
+
@on_return = block
|
266
|
+
end
|
267
|
+
|
261
268
|
def reply(args)
|
262
269
|
@replies.push(args)
|
263
270
|
end
|
264
271
|
|
265
272
|
def message_returned(reply_code, reply_text, exchange, routing_key)
|
266
|
-
|
267
|
-
|
268
|
-
body = String.new("", capacity: body_size)
|
269
|
-
while body.bytesize < body_size
|
270
|
-
body_part, = expect(:body)
|
271
|
-
body += body_part
|
272
|
-
end
|
273
|
-
msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
|
273
|
+
@next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
|
274
|
+
end
|
274
275
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
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
|
279
|
+
|
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
|
280
291
|
end
|
281
292
|
end
|
282
293
|
|
283
|
-
def
|
284
|
-
@
|
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
|
285
304
|
end
|
286
305
|
|
287
306
|
private
|
288
307
|
|
289
|
-
def
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
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}"
|
297
315
|
end
|
298
|
-
|
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
|
299
321
|
end
|
300
322
|
ensure
|
301
|
-
|
323
|
+
@next_msg = @next_body = @next_body_size = nil
|
302
324
|
end
|
303
325
|
|
304
326
|
def write_bytes(*bytes)
|
@@ -308,13 +330,11 @@ module AMQP
|
|
308
330
|
end
|
309
331
|
|
310
332
|
def expect(expected_frame_type)
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
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
|
315
336
|
|
316
|
-
|
317
|
-
end
|
337
|
+
args
|
318
338
|
end
|
319
339
|
end
|
320
340
|
end
|
@@ -10,9 +10,7 @@ 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
16
|
port = port_from_env || uri.port || (tls ? 5671 : 5672)
|
@@ -34,6 +32,7 @@ module AMQP
|
|
34
32
|
socket.sync_close = true # closing the TLS socket also closes the TCP socket
|
35
33
|
socket.hostname = host # SNI host
|
36
34
|
socket.connect
|
35
|
+
socket.post_connection_check(host) || raise(AMQP::Client::Error, "TLS certificate hostname doesn't match requested")
|
37
36
|
end
|
38
37
|
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
|
39
38
|
Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
|
@@ -41,22 +40,35 @@ module AMQP
|
|
41
40
|
|
42
41
|
def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
|
43
42
|
@socket = socket
|
44
|
-
@channel_max = channel_max
|
43
|
+
@channel_max = channel_max.zero? ? 65_536 : channel_max
|
45
44
|
@frame_max = frame_max
|
46
45
|
@heartbeat = heartbeat
|
47
46
|
@channels = {}
|
48
47
|
@closed = false
|
49
48
|
@replies = Queue.new
|
49
|
+
@write_lock = Mutex.new
|
50
50
|
Thread.new { read_loop } if read_loop_thread
|
51
51
|
end
|
52
52
|
|
53
53
|
attr_reader :frame_max
|
54
54
|
|
55
|
+
def inspect
|
56
|
+
"#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
|
57
|
+
end
|
58
|
+
|
55
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
|
+
|
56
63
|
if id
|
57
64
|
ch = @channels[id] ||= Channel.new(self, id)
|
58
65
|
else
|
59
|
-
id =
|
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
|
+
|
60
72
|
ch = @channels[id] = Channel.new(self, id)
|
61
73
|
end
|
62
74
|
ch.open
|
@@ -75,9 +87,10 @@ module AMQP
|
|
75
87
|
def close(reason = "", code = 200)
|
76
88
|
return if @closed
|
77
89
|
|
90
|
+
@closed = true
|
78
91
|
write_bytes FrameBytes.connection_close(code, reason)
|
92
|
+
@channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
|
79
93
|
expect(:close_ok)
|
80
|
-
@closed = true
|
81
94
|
end
|
82
95
|
|
83
96
|
def closed?
|
@@ -85,47 +98,51 @@ module AMQP
|
|
85
98
|
end
|
86
99
|
|
87
100
|
def write_bytes(*bytes)
|
88
|
-
@socket.
|
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
|
89
108
|
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
90
|
-
raise AMQP::Client::Error
|
109
|
+
raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
|
91
110
|
end
|
92
111
|
|
93
112
|
# Reads from the socket, required for any kind of progress. Blocks until the connection is closed
|
94
113
|
def read_loop
|
114
|
+
# read more often than write so that channel errors crop up early
|
115
|
+
Thread.current.priority += 1
|
95
116
|
socket = @socket
|
96
117
|
frame_max = @frame_max
|
97
|
-
|
118
|
+
frame_start = String.new(capacity: 7)
|
119
|
+
frame_buffer = String.new(capacity: frame_max)
|
98
120
|
loop do
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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}"
|
103
125
|
end
|
104
126
|
|
105
|
-
|
106
|
-
|
107
|
-
buffer += socket.read(pos + 8 - buffer.bytesize) if pos + 8 > buffer.bytesize
|
108
|
-
type, channel_id, frame_size = buffer.byteslice(pos, 7).unpack("C S> L>")
|
109
|
-
if frame_size > frame_max
|
110
|
-
raise AMQP::Client::Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}"
|
111
|
-
end
|
127
|
+
# read the frame content
|
128
|
+
socket.read(frame_size, frame_buffer)
|
112
129
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
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
|
117
133
|
|
118
|
-
|
119
|
-
|
120
|
-
parse_frame(type, channel_id, frame_size, buf) || return
|
121
|
-
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
|
122
136
|
end
|
137
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
138
|
+
warn "AMQP-Client read error: #{e.inspect}"
|
139
|
+
nil # ignore read errors
|
123
140
|
ensure
|
124
141
|
@closed = true
|
125
142
|
@replies.close
|
126
143
|
begin
|
127
144
|
@socket.close
|
128
|
-
rescue IOError
|
145
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
129
146
|
nil
|
130
147
|
end
|
131
148
|
end
|
@@ -135,20 +152,26 @@ module AMQP
|
|
135
152
|
def parse_frame(type, channel_id, frame_size, buf)
|
136
153
|
case type
|
137
154
|
when 1 # method frame
|
138
|
-
class_id, method_id = buf.unpack("
|
155
|
+
class_id, method_id = buf.unpack("S> S>")
|
139
156
|
case class_id
|
140
157
|
when 10 # connection
|
141
158
|
raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
142
159
|
|
143
160
|
case method_id
|
144
161
|
when 50 # connection#close
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
150
172
|
return false
|
151
173
|
when 51 # connection#close-ok
|
174
|
+
@closed = true
|
152
175
|
@replies.push [:close_ok]
|
153
176
|
return false
|
154
177
|
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
@@ -158,11 +181,12 @@ module AMQP
|
|
158
181
|
when 11 # channel#open-ok
|
159
182
|
@channels[channel_id].reply [:channel_open_ok]
|
160
183
|
when 40 # channel#close
|
161
|
-
reply_code, reply_text_len = buf.unpack("@
|
162
|
-
reply_text = buf.byteslice(
|
163
|
-
classid, methodid = buf.byteslice(
|
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>")
|
164
187
|
channel = @channels.delete(channel_id)
|
165
188
|
channel.closed!(reply_code, reply_text, classid, methodid)
|
189
|
+
write_bytes FrameBytes.channel_close_ok(channel_id)
|
166
190
|
when 41 # channel#close-ok
|
167
191
|
channel = @channels.delete(channel_id)
|
168
192
|
channel.reply [:channel_close_ok]
|
@@ -183,16 +207,16 @@ module AMQP
|
|
183
207
|
when 50 # queue
|
184
208
|
case method_id
|
185
209
|
when 11 # declare-ok
|
186
|
-
queue_name_len = buf.unpack1("@
|
187
|
-
queue_name = buf.byteslice(
|
188
|
-
message_count, consumer_count = buf.byteslice(
|
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>")
|
189
213
|
@channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
|
190
214
|
when 21 # bind-ok
|
191
215
|
@channels[channel_id].reply [:queue_bind_ok]
|
192
216
|
when 31 # purge-ok
|
193
217
|
@channels[channel_id].reply [:queue_purge_ok]
|
194
218
|
when 41 # delete-ok
|
195
|
-
message_count = buf.unpack1("@
|
219
|
+
message_count = buf.unpack1("@4 L>")
|
196
220
|
@channels[channel_id].reply [:queue_delete, message_count]
|
197
221
|
when 51 # unbind-ok
|
198
222
|
@channels[channel_id].reply [:queue_unbind_ok]
|
@@ -203,22 +227,22 @@ module AMQP
|
|
203
227
|
when 11 # qos-ok
|
204
228
|
@channels[channel_id].reply [:basic_qos_ok]
|
205
229
|
when 21 # consume-ok
|
206
|
-
tag_len = buf.unpack1("@
|
207
|
-
tag = buf.byteslice(
|
230
|
+
tag_len = buf.unpack1("@4 C")
|
231
|
+
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
208
232
|
@channels[channel_id].reply [:basic_consume_ok, tag]
|
209
233
|
when 30 # cancel
|
210
|
-
tag_len = buf.unpack1("@
|
211
|
-
tag = buf.byteslice(
|
212
|
-
no_wait = buf[
|
213
|
-
@channels[channel_id].
|
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)
|
214
238
|
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
|
215
239
|
when 31 # cancel-ok
|
216
|
-
tag_len = buf.unpack1("@
|
217
|
-
tag = buf.byteslice(
|
240
|
+
tag_len = buf.unpack1("@4 C")
|
241
|
+
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
218
242
|
@channels[channel_id].reply [:basic_cancel_ok, tag]
|
219
243
|
when 50 # return
|
220
|
-
reply_code, reply_text_len = buf.unpack("@
|
221
|
-
pos =
|
244
|
+
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
245
|
+
pos = 7
|
222
246
|
reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
|
223
247
|
pos += reply_text_len
|
224
248
|
exchange_len = buf[pos].ord
|
@@ -230,9 +254,9 @@ module AMQP
|
|
230
254
|
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
231
255
|
@channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
|
232
256
|
when 60 # deliver
|
233
|
-
ctag_len = buf[
|
234
|
-
consumer_tag = buf.byteslice(
|
235
|
-
pos =
|
257
|
+
ctag_len = buf[4].ord
|
258
|
+
consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
|
259
|
+
pos = 5 + ctag_len
|
236
260
|
delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
|
237
261
|
pos += 8 + 1 + 1
|
238
262
|
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
@@ -240,35 +264,27 @@ module AMQP
|
|
240
264
|
rk_len = buf[pos].ord
|
241
265
|
pos += 1
|
242
266
|
routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
|
243
|
-
|
244
|
-
if (consumer = @channels[channel_id].consumers[consumer_tag])
|
245
|
-
consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
|
246
|
-
break
|
247
|
-
else
|
248
|
-
Thread.pass
|
249
|
-
end
|
250
|
-
end
|
267
|
+
@channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
|
251
268
|
when 71 # get-ok
|
252
|
-
delivery_tag, redelivered, exchange_len = buf.unpack("@
|
253
|
-
pos =
|
269
|
+
delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
|
270
|
+
pos = 14
|
254
271
|
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
255
272
|
pos += exchange_len
|
256
273
|
routing_key_len = buf[pos].ord
|
257
274
|
pos += 1
|
258
275
|
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
259
276
|
pos += routing_key_len
|
260
|
-
|
261
|
-
|
262
|
-
@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)
|
263
279
|
when 72 # get-empty
|
264
|
-
@channels[channel_id].
|
280
|
+
@channels[channel_id].basic_get_empty
|
265
281
|
when 80 # ack
|
266
|
-
delivery_tag, multiple = buf.unpack("@
|
282
|
+
delivery_tag, multiple = buf.unpack("@4 Q> C")
|
267
283
|
@channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
|
268
284
|
when 111 # recover-ok
|
269
285
|
@channels[channel_id].reply [:basic_recover_ok]
|
270
286
|
when 120 # nack
|
271
|
-
delivery_tag, multiple, requeue = buf.unpack("@
|
287
|
+
delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
|
272
288
|
@channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
|
273
289
|
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
274
290
|
end
|
@@ -291,20 +307,19 @@ module AMQP
|
|
291
307
|
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
292
308
|
end
|
293
309
|
when 2 # header
|
294
|
-
body_size = buf.unpack1("@
|
295
|
-
properties = Properties.decode(buf.byteslice(
|
296
|
-
@channels[channel_id].
|
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
|
297
313
|
when 3 # body
|
298
|
-
|
299
|
-
@channels[channel_id].reply [:body, body]
|
314
|
+
@channels[channel_id].body_delivered buf
|
300
315
|
else raise AMQP::Client::UnsupportedFrameType, type
|
301
316
|
end
|
302
317
|
true
|
303
318
|
end
|
304
319
|
|
305
320
|
def expect(expected_frame_type)
|
306
|
-
frame_type, args = @replies.
|
307
|
-
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))
|
308
323
|
args
|
309
324
|
end
|
310
325
|
|
@@ -320,7 +335,7 @@ module AMQP
|
|
320
335
|
end
|
321
336
|
|
322
337
|
type, channel_id, frame_size = buf.unpack("C S> L>")
|
323
|
-
frame_end = buf
|
338
|
+
frame_end = buf[frame_size + 7].ord
|
324
339
|
raise UnexpectedFrameEndError, frame_end if frame_end != 206
|
325
340
|
|
326
341
|
case type
|
@@ -367,7 +382,7 @@ module AMQP
|
|
367
382
|
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
|
368
383
|
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
|
369
384
|
rescue StandardError => e
|
370
|
-
warn "
|
385
|
+
warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
|
371
386
|
end
|
372
387
|
|
373
388
|
def self.port_from_env
|
data/lib/amqp/client/message.rb
CHANGED
@@ -38,7 +38,8 @@ module AMQP
|
|
38
38
|
end
|
39
39
|
|
40
40
|
if delivery_mode
|
41
|
-
|
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,7 @@ module AMQP
|
|
69
70
|
end
|
70
71
|
|
71
72
|
if expiration
|
72
|
-
expiration = expiration.to_s if expiration.is_a?(Integer)
|
73
|
+
self.expiration = expiration.to_s if expiration.is_a?(Integer)
|
73
74
|
expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
|
74
75
|
|
75
76
|
flags |= (1 << 8)
|
@@ -86,7 +87,7 @@ module AMQP
|
|
86
87
|
end
|
87
88
|
|
88
89
|
if timestamp
|
89
|
-
timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be a
|
90
|
+
timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
|
90
91
|
|
91
92
|
flags |= (1 << 6)
|
92
93
|
arr << timestamp.to_i
|
data/lib/amqp/client/version.rb
CHANGED
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,
|
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
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
140
|
+
def wait_for_confirms
|
97
141
|
with_connection do |conn|
|
98
|
-
conn.channel(1).
|
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(
|
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).
|
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.
|
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-
|
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.
|
64
|
+
rubygems_version: 3.2.22
|
65
65
|
signing_key:
|
66
66
|
specification_version: 4
|
67
67
|
summary: AMQP 0-9-1 client
|