amqp-client 0.1.0 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|