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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +4 -4
- data/lib/amqp/client/channel.rb +125 -93
- data/lib/amqp/client/connection.rb +106 -87
- data/lib/amqp/client/message.rb +1 -1
- data/lib/amqp/client/properties.rb +5 -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,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
|
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
@@ -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
|
-
|
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
|
-
@
|
45
|
-
@
|
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,
|
49
|
-
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)
|
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
|
-
|
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
|
-
|
106
|
-
|
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
|
129
|
-
write_bytes FrameBytes.basic_publish(id, exchange, routing_key,
|
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
|
-
|
135
|
+
@unconfirmed.push @confirm += 1 if @confirm
|
136
|
+
return
|
133
137
|
end
|
134
138
|
|
135
|
-
write_bytes FrameBytes.basic_publish(id, exchange, routing_key,
|
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
|
-
|
150
|
-
|
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
|
-
|
164
|
-
yield
|
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
|
-
|
170
|
-
yield(
|
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
|
-
|
219
|
-
|
220
|
-
return true if @
|
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
|
-
|
224
|
-
|
225
|
-
|
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
|
250
|
-
@
|
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
|
254
|
-
|
255
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
272
|
-
@
|
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
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
300
|
-
|
301
|
-
|
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
|
-
|
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 || (
|
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 =
|
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.
|
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
|
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
|
-
|
118
|
+
frame_start = String.new(capacity: 7)
|
119
|
+
frame_buffer = String.new(capacity: frame_max)
|
93
120
|
loop do
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
101
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
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
|
-
|
114
|
-
|
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("
|
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
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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("@
|
157
|
-
reply_text = buf.byteslice(
|
158
|
-
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>")
|
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
|
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("@
|
181
|
-
queue_name = buf.byteslice(
|
182
|
-
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>")
|
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("@
|
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("@
|
201
|
-
tag = buf.byteslice(
|
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("@
|
205
|
-
tag = buf.byteslice(
|
206
|
-
no_wait = buf[
|
207
|
-
@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)
|
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("@
|
211
|
-
tag = buf.byteslice(
|
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("@
|
215
|
-
pos =
|
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[
|
228
|
-
consumer_tag = buf.byteslice(
|
229
|
-
pos =
|
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
|
-
|
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("@
|
247
|
-
pos =
|
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
|
-
|
255
|
-
|
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].
|
280
|
+
@channels[channel_id].basic_get_empty
|
259
281
|
when 80 # ack
|
260
|
-
delivery_tag, multiple = buf.unpack("@
|
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("@
|
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("@
|
292
|
-
properties = Properties.decode(buf.byteslice(
|
293
|
-
@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
|
294
313
|
when 3 # body
|
295
|
-
|
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.
|
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
|
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
|
-
|
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 "
|
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
|
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,8 @@ module AMQP
|
|
69
70
|
end
|
70
71
|
|
71
72
|
if expiration
|
72
|
-
expiration
|
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
|
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
|
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
|