amqp-client 0.1.0 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +19 -2
- data/.gitignore +1 -0
- data/.rubocop_todo.yml +39 -33
- data/CHANGELOG.md +18 -0
- data/README.md +31 -0
- data/lib/amqp-client.rb +3 -0
- data/lib/amqp/client.rb +145 -77
- data/lib/amqp/client/channel.rb +239 -32
- data/lib/amqp/client/connection.rb +298 -42
- data/lib/amqp/client/errors.rb +7 -2
- data/lib/amqp/client/frames.rb +361 -16
- data/lib/amqp/client/message.rb +11 -1
- data/lib/amqp/client/properties.rb +201 -0
- data/lib/amqp/client/table.rb +123 -0
- data/lib/amqp/client/version.rb +1 -1
- metadata +5 -3
- data/Gemfile.lock +0 -42
data/lib/amqp/client/channel.rb
CHANGED
@@ -6,17 +6,25 @@ module AMQP
|
|
6
6
|
# AMQP Channel
|
7
7
|
class Channel
|
8
8
|
def initialize(connection, id)
|
9
|
-
@
|
9
|
+
@replies = ::Queue.new
|
10
10
|
@connection = connection
|
11
11
|
@id = id
|
12
|
-
@
|
12
|
+
@consumers = {}
|
13
|
+
@confirm = nil
|
14
|
+
@last_confirmed = 0
|
15
|
+
@closed = nil
|
16
|
+
@on_return = nil
|
17
|
+
@open = false
|
13
18
|
end
|
14
19
|
|
15
|
-
attr_reader :id
|
20
|
+
attr_reader :id, :consumers
|
16
21
|
|
17
22
|
def open
|
23
|
+
return self if @open
|
24
|
+
|
18
25
|
write_bytes FrameBytes.channel_open(@id)
|
19
26
|
expect(:channel_open_ok)
|
27
|
+
@open = true
|
20
28
|
self
|
21
29
|
end
|
22
30
|
|
@@ -25,15 +33,44 @@ module AMQP
|
|
25
33
|
|
26
34
|
write_bytes FrameBytes.channel_close(@id, reason, code)
|
27
35
|
expect :channel_close_ok
|
28
|
-
@closed =
|
36
|
+
@closed = [code, reason]
|
37
|
+
end
|
38
|
+
|
39
|
+
# Called when closed by server
|
40
|
+
def closed!(code, reason, classid, methodid)
|
41
|
+
write_bytes FrameBytes.channel_close_ok(@id)
|
42
|
+
@closed = [code, reason, classid, methodid]
|
43
|
+
@replies.close
|
44
|
+
@consumers.each { |_, q| q.close }
|
45
|
+
@consumers.clear
|
46
|
+
end
|
47
|
+
|
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)
|
50
|
+
expect :exchange_declare_ok
|
29
51
|
end
|
30
52
|
|
31
|
-
def
|
53
|
+
def exchange_delete(name, if_unused: false, no_wait: false)
|
54
|
+
write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
|
55
|
+
expect :exchange_delete_ok
|
56
|
+
end
|
57
|
+
|
58
|
+
def exchange_bind(destination, source, binding_key, arguments = {})
|
59
|
+
write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
|
60
|
+
expect :exchange_bind_ok
|
61
|
+
end
|
62
|
+
|
63
|
+
def exchange_unbind(destination, source, binding_key, arguments = {})
|
64
|
+
write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
|
65
|
+
expect :exchange_unbind_ok
|
66
|
+
end
|
67
|
+
|
68
|
+
def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
|
32
69
|
durable = false if name.empty?
|
33
70
|
exclusive = true if name.empty?
|
34
71
|
auto_delete = true if name.empty?
|
35
72
|
|
36
|
-
write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete)
|
73
|
+
write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
|
37
74
|
name, message_count, consumer_count = expect(:queue_declare_ok)
|
38
75
|
{
|
39
76
|
queue_name: name,
|
@@ -42,60 +79,230 @@ module AMQP
|
|
42
79
|
}
|
43
80
|
end
|
44
81
|
|
45
|
-
def
|
46
|
-
|
82
|
+
def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
|
83
|
+
write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
|
84
|
+
message_count, = expect :queue_delete
|
85
|
+
message_count
|
86
|
+
end
|
87
|
+
|
88
|
+
def queue_bind(name, exchange, binding_key, arguments = {})
|
89
|
+
write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
|
90
|
+
expect :queue_bind_ok
|
91
|
+
end
|
92
|
+
|
93
|
+
def queue_purge(name, no_wait: false)
|
94
|
+
write_bytes FrameBytes.queue_purge(@id, name, no_wait)
|
95
|
+
expect :queue_purge_ok unless no_wait
|
96
|
+
end
|
97
|
+
|
98
|
+
def queue_unbind(name, exchange, binding_key, arguments = {})
|
99
|
+
write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
|
100
|
+
expect :queue_unbind_ok
|
101
|
+
end
|
47
102
|
|
103
|
+
def basic_get(queue_name, no_ack: true)
|
48
104
|
write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
|
49
|
-
|
50
|
-
frame, = resp
|
105
|
+
frame, *rest = @replies.shift
|
51
106
|
case frame
|
52
107
|
when :basic_get_ok
|
53
|
-
|
108
|
+
delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
|
54
109
|
body_size, properties = expect(:header)
|
55
110
|
pos = 0
|
56
|
-
body = ""
|
111
|
+
body = String.new("", capacity: body_size)
|
57
112
|
while pos < body_size
|
58
|
-
body_part = expect(:body)
|
113
|
+
body_part, = expect(:body)
|
59
114
|
body += body_part
|
60
115
|
pos += body_part.bytesize
|
61
116
|
end
|
62
|
-
Message.new(exchange_name, routing_key, properties, body, redelivered)
|
63
|
-
when :basic_get_empty
|
64
|
-
|
65
|
-
else raise AMQP::Client::UnexpectedFrame
|
117
|
+
Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
|
118
|
+
when :basic_get_empty then nil
|
119
|
+
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)
|
66
121
|
end
|
67
122
|
end
|
68
123
|
|
69
|
-
def basic_publish(exchange, routing_key,
|
70
|
-
|
124
|
+
def basic_publish(body, exchange, routing_key, **properties)
|
125
|
+
frame_max = @connection.frame_max - 8
|
126
|
+
id = @id
|
71
127
|
|
72
|
-
|
73
|
-
|
128
|
+
if 0 < body.bytesize && body.bytesize <= frame_max
|
129
|
+
write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
|
130
|
+
FrameBytes.header(id, body.bytesize, properties),
|
131
|
+
FrameBytes.body(id, body)
|
132
|
+
return @confirm ? @confirm += 1 : nil
|
133
|
+
end
|
74
134
|
|
75
|
-
|
135
|
+
write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
|
136
|
+
FrameBytes.header(id, body.bytesize, properties)
|
76
137
|
pos = 0
|
77
|
-
while pos < body.bytesize
|
78
|
-
len = [
|
138
|
+
while pos < body.bytesize # split body into multiple frame_max frames
|
139
|
+
len = [frame_max, body.bytesize - pos].min
|
79
140
|
body_part = body.byteslice(pos, len)
|
80
|
-
write_bytes FrameBytes.body(
|
141
|
+
write_bytes FrameBytes.body(id, body_part)
|
81
142
|
pos += len
|
82
143
|
end
|
144
|
+
@confirm += 1 if @confirm
|
145
|
+
end
|
146
|
+
|
147
|
+
def basic_publish_confirm(body, exchange, routing_key, **properties)
|
148
|
+
confirm_select(no_wait: true)
|
149
|
+
id = basic_publish(body, exchange, routing_key, **properties)
|
150
|
+
wait_for_confirm(id)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Consume from a queue
|
154
|
+
# 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)
|
157
|
+
write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
|
158
|
+
tag, = expect(:basic_consume_ok)
|
159
|
+
q = @consumers[tag] = ::Queue.new
|
160
|
+
msgs = ::Queue.new
|
161
|
+
Thread.new { recv_deliveries(tag, q, msgs) }
|
162
|
+
if worker_threads.zero?
|
163
|
+
while (msg = msgs.shift)
|
164
|
+
yield msg
|
165
|
+
end
|
166
|
+
else
|
167
|
+
threads = Array.new(worker_threads) do
|
168
|
+
Thread.new do
|
169
|
+
while (msg = msgs.shift)
|
170
|
+
yield(msg)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
[tag, threads]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def basic_cancel(consumer_tag, no_wait: false)
|
179
|
+
consumer = @consumers.fetch(consumer_tag)
|
180
|
+
return if consumer.closed?
|
181
|
+
|
182
|
+
write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
|
183
|
+
expect(:basic_cancel_ok) unless no_wait
|
184
|
+
consumer.close
|
185
|
+
end
|
186
|
+
|
187
|
+
def basic_qos(prefetch_count, prefetch_size: 0, global: false)
|
188
|
+
write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
|
189
|
+
expect :basic_qos_ok
|
190
|
+
end
|
191
|
+
|
192
|
+
def basic_ack(delivery_tag, multiple: false)
|
193
|
+
write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
|
194
|
+
end
|
195
|
+
|
196
|
+
def basic_nack(delivery_tag, multiple: false, requeue: false)
|
197
|
+
write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
|
198
|
+
end
|
199
|
+
|
200
|
+
def basic_reject(delivery_tag, requeue: false)
|
201
|
+
write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
|
202
|
+
end
|
203
|
+
|
204
|
+
def basic_recover(requeue: false)
|
205
|
+
write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
|
206
|
+
expect :basic_recover_ok
|
207
|
+
end
|
208
|
+
|
209
|
+
def confirm_select(no_wait: false)
|
210
|
+
return if @confirm
|
211
|
+
|
212
|
+
write_bytes FrameBytes.confirm_select(@id, no_wait)
|
213
|
+
expect :confirm_select_ok unless no_wait
|
214
|
+
@confirms = ::Queue.new
|
215
|
+
@confirm = 0
|
216
|
+
end
|
217
|
+
|
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
|
221
|
+
|
222
|
+
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)
|
226
|
+
end
|
227
|
+
false
|
228
|
+
end
|
229
|
+
|
230
|
+
def tx_select
|
231
|
+
write_bytes FrameBytes.tx_select(@id)
|
232
|
+
expect :tx_select_ok
|
233
|
+
end
|
234
|
+
|
235
|
+
def tx_commit
|
236
|
+
write_bytes FrameBytes.tx_commit(@id)
|
237
|
+
expect :tx_commit_ok
|
238
|
+
end
|
239
|
+
|
240
|
+
def tx_rollback
|
241
|
+
write_bytes FrameBytes.tx_rollback(@id)
|
242
|
+
expect :tx_rollback_ok
|
243
|
+
end
|
244
|
+
|
245
|
+
def reply(args)
|
246
|
+
@replies.push(args)
|
247
|
+
end
|
248
|
+
|
249
|
+
def confirm(args)
|
250
|
+
@confirms.push(args)
|
251
|
+
end
|
252
|
+
|
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)
|
262
|
+
|
263
|
+
if @on_return
|
264
|
+
@on_return.call(msg)
|
265
|
+
else
|
266
|
+
puts "[WARN] Message returned: #{msg.inspect}"
|
267
|
+
end
|
268
|
+
end
|
83
269
|
end
|
84
270
|
|
85
|
-
def
|
86
|
-
@
|
271
|
+
def on_return(&block)
|
272
|
+
@on_return = block
|
87
273
|
end
|
88
274
|
|
89
275
|
private
|
90
276
|
|
91
|
-
def
|
92
|
-
|
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
|
285
|
+
end
|
286
|
+
msgs.push Message.new(self, delivery_tag, exchange, routing_key, properties, body, redelivered, consumer_tag)
|
287
|
+
end
|
288
|
+
ensure
|
289
|
+
msgs.close
|
290
|
+
end
|
291
|
+
|
292
|
+
def write_bytes(*bytes)
|
293
|
+
raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
|
294
|
+
|
295
|
+
@connection.write_bytes(*bytes)
|
93
296
|
end
|
94
297
|
|
95
298
|
def expect(expected_frame_type)
|
96
|
-
|
97
|
-
|
98
|
-
|
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
|
303
|
+
|
304
|
+
@replies.push [frame_type, *args]
|
305
|
+
end
|
99
306
|
end
|
100
307
|
end
|
101
308
|
end
|
@@ -1,38 +1,96 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "socket"
|
4
|
+
require "uri"
|
5
|
+
require "openssl"
|
6
|
+
require_relative "./frames"
|
7
|
+
require_relative "./channel"
|
8
|
+
require_relative "./errors"
|
9
|
+
|
3
10
|
module AMQP
|
4
|
-
# AMQP
|
11
|
+
# Represents a single AMQP connection
|
5
12
|
class Connection
|
6
|
-
def
|
13
|
+
def self.connect(uri, **options)
|
14
|
+
read_loop_thread = options[:read_loop_thread] || true
|
15
|
+
|
16
|
+
uri = URI.parse(uri)
|
17
|
+
tls = uri.scheme == "amqps"
|
18
|
+
port = port_from_env || uri.port || (tls ? 5671 : 5672)
|
19
|
+
host = uri.host || "localhost"
|
20
|
+
user = uri.user || "guest"
|
21
|
+
password = uri.password || "guest"
|
22
|
+
vhost = URI.decode_www_form_component(uri.path[1..-1] || "/")
|
23
|
+
options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
|
24
|
+
|
25
|
+
socket = Socket.tcp host, port, connect_timeout: 20, resolv_timeout: 5
|
26
|
+
enable_tcp_keepalive(socket)
|
27
|
+
if tls
|
28
|
+
cert_store = OpenSSL::X509::Store.new
|
29
|
+
cert_store.set_default_paths
|
30
|
+
context = OpenSSL::SSL::SSLContext.new
|
31
|
+
context.cert_store = cert_store
|
32
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless [false, "false", "none"].include? options[:verify_peer]
|
33
|
+
socket = OpenSSL::SSL::SSLSocket.new(socket, context)
|
34
|
+
socket.sync_close = true # closing the TLS socket also closes the TCP socket
|
35
|
+
socket.hostname = host # SNI host
|
36
|
+
socket.connect
|
37
|
+
end
|
38
|
+
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
|
39
|
+
Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
|
7
43
|
@socket = socket
|
8
44
|
@channel_max = channel_max
|
9
45
|
@frame_max = frame_max
|
10
46
|
@heartbeat = heartbeat
|
11
47
|
@channels = {}
|
12
48
|
@closed = false
|
13
|
-
@
|
14
|
-
Thread.new { read_loop }
|
49
|
+
@replies = Queue.new
|
50
|
+
Thread.new { read_loop } if read_loop_thread
|
15
51
|
end
|
16
52
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
53
|
+
attr_reader :frame_max
|
54
|
+
|
55
|
+
def channel(id = nil)
|
56
|
+
if id
|
57
|
+
ch = @channels[id] ||= Channel.new(self, id)
|
58
|
+
else
|
59
|
+
id = 1.upto(@channel_max) { |i| break i unless @channels.key? i }
|
60
|
+
ch = @channels[id] = Channel.new(self, id)
|
61
|
+
end
|
21
62
|
ch.open
|
22
63
|
end
|
23
64
|
|
65
|
+
# Declare a new channel, yield, and then close the channel
|
66
|
+
def with_channel
|
67
|
+
ch = channel
|
68
|
+
begin
|
69
|
+
yield ch
|
70
|
+
ensure
|
71
|
+
ch.close
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
24
75
|
def close(reason = "", code = 200)
|
76
|
+
return if @closed
|
77
|
+
|
25
78
|
write_bytes FrameBytes.connection_close(code, reason)
|
26
79
|
expect(:close_ok)
|
27
80
|
@closed = true
|
28
81
|
end
|
29
82
|
|
30
|
-
def
|
31
|
-
@
|
83
|
+
def closed?
|
84
|
+
@closed
|
32
85
|
end
|
33
86
|
|
34
|
-
|
87
|
+
def write_bytes(*bytes)
|
88
|
+
@socket.write(*bytes)
|
89
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
90
|
+
raise AMQP::Client::Error.new("Could not write to socket", cause: e)
|
91
|
+
end
|
35
92
|
|
93
|
+
# Reads from the socket, required for any kind of progress. Blocks until the connection is closed
|
36
94
|
def read_loop
|
37
95
|
socket = @socket
|
38
96
|
frame_max = @frame_max
|
@@ -40,23 +98,31 @@ module AMQP
|
|
40
98
|
loop do
|
41
99
|
begin
|
42
100
|
socket.readpartial(frame_max, buffer)
|
43
|
-
rescue IOError,
|
101
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
44
102
|
break
|
45
103
|
end
|
46
104
|
|
47
|
-
|
48
|
-
while
|
49
|
-
|
50
|
-
|
51
|
-
|
105
|
+
pos = 0
|
106
|
+
while pos < buffer.bytesize
|
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
|
112
|
+
|
113
|
+
frame_end_pos = pos + 7 + frame_size
|
114
|
+
buffer += socket.read(frame_end_pos - buffer.bytesize + 1) if frame_end_pos + 1 > buffer.bytesize
|
115
|
+
frame_end = buffer[frame_end_pos].ord
|
116
|
+
raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
|
52
117
|
|
53
|
-
buf = buffer.byteslice(
|
54
|
-
|
118
|
+
buf = buffer.byteslice(pos, frame_size + 8)
|
119
|
+
pos += frame_size + 8
|
55
120
|
parse_frame(type, channel_id, frame_size, buf) || return
|
56
121
|
end
|
57
122
|
end
|
58
123
|
ensure
|
59
124
|
@closed = true
|
125
|
+
@replies.close
|
60
126
|
begin
|
61
127
|
@socket.close
|
62
128
|
rescue IOError
|
@@ -64,6 +130,8 @@ module AMQP
|
|
64
130
|
end
|
65
131
|
end
|
66
132
|
|
133
|
+
private
|
134
|
+
|
67
135
|
def parse_frame(type, channel_id, frame_size, buf)
|
68
136
|
case type
|
69
137
|
when 1 # method frame
|
@@ -75,67 +143,255 @@ module AMQP
|
|
75
143
|
case method_id
|
76
144
|
when 50 # connection#close
|
77
145
|
code, text_len = buf.unpack("@11 S> C")
|
78
|
-
text
|
146
|
+
text = buf.byteslice(14, text_len).force_encoding("utf-8")
|
147
|
+
error_class_id, error_method_id = buf.byteslice(14 + text_len, 4).unpack("S> S>")
|
79
148
|
warn "Connection closed #{code} #{text} #{error_class_id} #{error_method_id}"
|
80
149
|
write_bytes FrameBytes.connection_close_ok
|
81
150
|
return false
|
82
151
|
when 51 # connection#close-ok
|
83
|
-
@
|
152
|
+
@replies.push [:close_ok]
|
84
153
|
return false
|
85
154
|
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
86
155
|
end
|
87
156
|
when 20 # channel
|
88
157
|
case method_id
|
89
158
|
when 11 # channel#open-ok
|
90
|
-
@channels[channel_id].
|
159
|
+
@channels[channel_id].reply [:channel_open_ok]
|
91
160
|
when 40 # channel#close
|
161
|
+
reply_code, reply_text_len = buf.unpack("@11 S> C")
|
162
|
+
reply_text = buf.byteslice(14, reply_text_len).force_encoding("utf-8")
|
163
|
+
classid, methodid = buf.byteslice(14 + reply_text_len, 4).unpack("S> S>")
|
92
164
|
channel = @channels.delete(channel_id)
|
93
|
-
channel
|
165
|
+
channel.closed!(reply_code, reply_text, classid, methodid)
|
94
166
|
when 41 # channel#close-ok
|
95
|
-
@channels[channel_id].
|
167
|
+
@channels[channel_id].reply [:channel_close_ok]
|
168
|
+
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
169
|
+
end
|
170
|
+
when 40 # exchange
|
171
|
+
case method_id
|
172
|
+
when 11 # declare-ok
|
173
|
+
@channels[channel_id].reply [:exchange_declare_ok]
|
174
|
+
when 21 # delete-ok
|
175
|
+
@channels[channel_id].reply [:exchange_delete_ok]
|
176
|
+
when 31 # bind-ok
|
177
|
+
@channels[channel_id].reply [:exchange_bind_ok]
|
178
|
+
when 51 # unbind-ok
|
179
|
+
@channels[channel_id].reply [:exchange_unbind_ok]
|
96
180
|
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
97
181
|
end
|
98
182
|
when 50 # queue
|
99
183
|
case method_id
|
100
|
-
when 11 #
|
184
|
+
when 11 # declare-ok
|
101
185
|
queue_name_len = buf.unpack1("@11 C")
|
102
|
-
queue_name
|
103
|
-
|
104
|
-
|
186
|
+
queue_name = buf.byteslice(12, queue_name_len).force_encoding("utf-8")
|
187
|
+
message_count, consumer_count = buf.byteslice(12 + queue_name_len, 8).unpack1("L> L>")
|
188
|
+
@channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
|
189
|
+
when 21 # bind-ok
|
190
|
+
@channels[channel_id].reply [:queue_bind_ok]
|
191
|
+
when 31 # purge-ok
|
192
|
+
@channels[channel_id].reply [:queue_purge_ok]
|
193
|
+
when 41 # delete-ok
|
194
|
+
message_count = buf.unpack1("@11 L>")
|
195
|
+
@channels[channel_id].reply [:queue_delete, message_count]
|
196
|
+
when 51 # unbind-ok
|
197
|
+
@channels[channel_id].reply [:queue_unbind_ok]
|
198
|
+
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
105
199
|
end
|
106
200
|
when 60 # basic
|
107
201
|
case method_id
|
202
|
+
when 11 # qos-ok
|
203
|
+
@channels[channel_id].reply [:basic_qos_ok]
|
204
|
+
when 21 # consume-ok
|
205
|
+
tag_len = buf.unpack1("@11 C")
|
206
|
+
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
207
|
+
@channels[channel_id].reply [:basic_consume_ok, tag]
|
208
|
+
when 30 # cancel
|
209
|
+
tag_len = buf.unpack1("@11 C")
|
210
|
+
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
211
|
+
no_wait = buf[12 + tag_len].ord
|
212
|
+
@channels[channel_id].consumers.fetch(tag).close
|
213
|
+
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
|
214
|
+
when 31 # cancel-ok
|
215
|
+
tag_len = buf.unpack1("@11 C")
|
216
|
+
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
217
|
+
@channels[channel_id].reply [:basic_cancel_ok, tag]
|
218
|
+
when 50 # return
|
219
|
+
reply_code, reply_text_len = buf.unpack("@11 S> C")
|
220
|
+
pos = 14
|
221
|
+
reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
|
222
|
+
pos += reply_text_len
|
223
|
+
exchange_len = buf[pos].ord
|
224
|
+
pos += 1
|
225
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
226
|
+
pos += exchange_len
|
227
|
+
routing_key_len = buf[pos].ord
|
228
|
+
pos += 1
|
229
|
+
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
230
|
+
@channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
|
231
|
+
when 60 # deliver
|
232
|
+
ctag_len = buf[11].ord
|
233
|
+
consumer_tag = buf.byteslice(12, ctag_len).force_encoding("utf-8")
|
234
|
+
pos = 12 + ctag_len
|
235
|
+
delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
|
236
|
+
pos += 8 + 1 + 1
|
237
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
238
|
+
pos += exchange_len
|
239
|
+
rk_len = buf[pos].ord
|
240
|
+
pos += 1
|
241
|
+
routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
|
242
|
+
loop do
|
243
|
+
if (consumer = @channels[channel_id].consumers[consumer_tag])
|
244
|
+
consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
|
245
|
+
break
|
246
|
+
else
|
247
|
+
Thread.pass
|
248
|
+
end
|
249
|
+
end
|
108
250
|
when 71 # get-ok
|
109
|
-
delivery_tag, redelivered,
|
110
|
-
|
111
|
-
|
112
|
-
|
251
|
+
delivery_tag, redelivered, exchange_len = buf.unpack("@11 Q> C C")
|
252
|
+
pos = 21
|
253
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
254
|
+
pos += exchange_len
|
255
|
+
routing_key_len = buf[pos].ord
|
113
256
|
pos += 1
|
114
|
-
routing_key = buf.byteslice(pos, routing_key_len)
|
257
|
+
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
115
258
|
pos += routing_key_len
|
116
|
-
message_count = buf.unpack1("
|
117
|
-
|
259
|
+
message_count = buf.byteslice(pos, 4).unpack1("L>")
|
260
|
+
redelivered = redelivered == 1
|
261
|
+
@channels[channel_id].reply [:basic_get_ok, delivery_tag, exchange, routing_key, message_count, redelivered]
|
118
262
|
when 72 # get-empty
|
119
|
-
@channels[channel_id].
|
120
|
-
|
263
|
+
@channels[channel_id].reply [:basic_get_empty]
|
264
|
+
when 80 # ack
|
265
|
+
delivery_tag, multiple = buf.unpack("@11 Q> C")
|
266
|
+
@channels[channel_id].confirm [:ack, delivery_tag, multiple]
|
267
|
+
when 90 # reject
|
268
|
+
delivery_tag, requeue = buf.unpack("@11 Q> C")
|
269
|
+
@channels[channel_id].confirm [:reject, delivery_tag, requeue == 1]
|
270
|
+
when 111 # recover-ok
|
271
|
+
@channels[channel_id].reply [:basic_recover_ok]
|
272
|
+
when 120 # nack
|
273
|
+
delivery_tag, multiple, requeue = buf.unpack("@11 Q> C C")
|
274
|
+
@channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
|
275
|
+
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
276
|
+
end
|
277
|
+
when 85 # confirm
|
278
|
+
case method_id
|
279
|
+
when 11 # select-ok
|
280
|
+
@channels[channel_id].reply [:confirm_select_ok]
|
281
|
+
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
282
|
+
end
|
283
|
+
when 90 # tx
|
284
|
+
case method_id
|
285
|
+
when 11 # select-ok
|
286
|
+
@channels[channel_id].reply [:tx_select_ok]
|
287
|
+
when 21 # commit-ok
|
288
|
+
@channels[channel_id].reply [:tx_commit_ok]
|
289
|
+
when 31 # rollback-ok
|
290
|
+
@channels[channel_id].reply [:tx_rollback_ok]
|
291
|
+
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
121
292
|
end
|
122
|
-
else raise AMQP::Client::UnsupportedMethodFrame
|
293
|
+
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
123
294
|
end
|
124
295
|
when 2 # header
|
125
296
|
body_size = buf.unpack1("@11 Q>")
|
126
|
-
|
297
|
+
properties = Properties.decode(buf.byteslice(19, buf.bytesize - 20))
|
298
|
+
@channels[channel_id].reply [:header, body_size, properties]
|
127
299
|
when 3 # body
|
128
300
|
body = buf.byteslice(7, frame_size)
|
129
|
-
@channels[channel_id].
|
301
|
+
@channels[channel_id].reply [:body, body]
|
130
302
|
else raise AMQP::Client::UnsupportedFrameType, type
|
131
303
|
end
|
132
304
|
true
|
133
305
|
end
|
134
306
|
|
135
307
|
def expect(expected_frame_type)
|
136
|
-
frame_type, args = @
|
137
|
-
frame_type == expected_frame_type || raise(UnexpectedFrame
|
308
|
+
frame_type, args = @replies.shift
|
309
|
+
frame_type == expected_frame_type || raise(UnexpectedFrame.new(expected_frame_type, frame_type))
|
138
310
|
args
|
139
311
|
end
|
312
|
+
|
313
|
+
def self.establish(socket, user, password, vhost, **options)
|
314
|
+
channel_max, frame_max, heartbeat = nil
|
315
|
+
socket.write "AMQP\x00\x00\x09\x01"
|
316
|
+
buf = String.new(capacity: 4096)
|
317
|
+
loop do
|
318
|
+
begin
|
319
|
+
socket.readpartial(4096, buf)
|
320
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
321
|
+
raise AMQP::Client::Error, "Could not establish AMQP connection: #{e.message}"
|
322
|
+
end
|
323
|
+
|
324
|
+
type, channel_id, frame_size = buf.unpack("C S> L>")
|
325
|
+
frame_end = buf.unpack1("@#{frame_size + 7} C")
|
326
|
+
raise UnexpectedFrameEndError, frame_end if frame_end != 206
|
327
|
+
|
328
|
+
case type
|
329
|
+
when 1 # method frame
|
330
|
+
class_id, method_id = buf.unpack("@7 S> S>")
|
331
|
+
case class_id
|
332
|
+
when 10 # connection
|
333
|
+
raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
334
|
+
|
335
|
+
case method_id
|
336
|
+
when 10 # connection#start
|
337
|
+
properties = CLIENT_PROPERTIES.merge({ connection_name: options[:connection_name] })
|
338
|
+
socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
|
339
|
+
when 30 # connection#tune
|
340
|
+
channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
|
341
|
+
channel_max = [channel_max, 2048].min
|
342
|
+
frame_max = [frame_max, 131_072].min
|
343
|
+
heartbeat = [heartbeat, 0].min
|
344
|
+
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
345
|
+
socket.write FrameBytes.connection_open(vhost)
|
346
|
+
when 41 # connection#open-ok
|
347
|
+
return [channel_max, frame_max, heartbeat]
|
348
|
+
when 50 # connection#close
|
349
|
+
code, text_len = buf.unpack("@11 S> C")
|
350
|
+
text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
|
351
|
+
socket.write FrameBytes.connection_close_ok
|
352
|
+
raise AMQP::Client::Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
|
353
|
+
else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
|
354
|
+
end
|
355
|
+
else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
|
356
|
+
end
|
357
|
+
else raise AMQP::Client::Error, "Unexpected frame type: #{type}"
|
358
|
+
end
|
359
|
+
end
|
360
|
+
rescue StandardError
|
361
|
+
socket.close rescue nil
|
362
|
+
raise
|
363
|
+
end
|
364
|
+
|
365
|
+
def self.enable_tcp_keepalive(socket)
|
366
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
367
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
|
368
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
|
369
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
|
370
|
+
rescue StandardError => e
|
371
|
+
warn "amqp-client: Could not enable TCP keepalive on socket. #{e.inspect}"
|
372
|
+
end
|
373
|
+
|
374
|
+
def self.port_from_env
|
375
|
+
return unless (port = ENV["AMQP_PORT"])
|
376
|
+
|
377
|
+
port.to_i
|
378
|
+
end
|
379
|
+
|
380
|
+
private_class_method :establish, :enable_tcp_keepalive, :port_from_env
|
381
|
+
|
382
|
+
CLIENT_PROPERTIES = {
|
383
|
+
capabilities: {
|
384
|
+
authentication_failure_close: true,
|
385
|
+
publisher_confirms: true,
|
386
|
+
consumer_cancel_notify: true,
|
387
|
+
exchange_exchange_bindings: true,
|
388
|
+
"basic.nack": true,
|
389
|
+
"connection.blocked": true
|
390
|
+
},
|
391
|
+
product: "amqp-client.rb",
|
392
|
+
platform: RUBY_DESCRIPTION,
|
393
|
+
version: AMQP::Client::VERSION,
|
394
|
+
information: "http://github.com/cloudamqp/amqp-client.rb"
|
395
|
+
}.freeze
|
140
396
|
end
|
141
397
|
end
|