amqp-client 1.0.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +25 -0
- data/.github/workflows/main.yml +1 -2
- data/.rubocop.yml +13 -2
- data/.rubocop_todo.yml +14 -58
- data/.yardopts +1 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +4 -0
- data/README.md +62 -25
- data/Rakefile +3 -1
- data/amqp-client.gemspec +1 -1
- data/lib/amqp/client/channel.rb +487 -275
- data/lib/amqp/client/connection.rb +438 -357
- data/lib/amqp/client/errors.rb +46 -25
- data/lib/amqp/client/exchange.rb +66 -0
- data/lib/amqp/client/frame_bytes.rb +533 -0
- data/lib/amqp/client/message.rb +103 -7
- data/lib/amqp/client/properties.rb +263 -166
- data/lib/amqp/client/queue.rb +72 -0
- data/lib/amqp/client/table.rb +137 -107
- data/lib/amqp/client/version.rb +2 -1
- data/lib/amqp/client.rb +148 -97
- data/sig/amqp-client.rbs +264 -0
- metadata +9 -4
- data/lib/amqp/client/frames.rb +0 -531
@@ -3,409 +3,490 @@
|
|
3
3
|
require "socket"
|
4
4
|
require "uri"
|
5
5
|
require "openssl"
|
6
|
-
require_relative "./
|
6
|
+
require_relative "./frame_bytes"
|
7
7
|
require_relative "./channel"
|
8
8
|
require_relative "./errors"
|
9
9
|
|
10
10
|
module AMQP
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
options
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
11
|
+
class Client
|
12
|
+
# Represents a single established AMQP connection
|
13
|
+
class Connection
|
14
|
+
# Establish a connection to an AMQP broker
|
15
|
+
# @param uri [String] URL on the format amqp://username:password@hostname/vhost, use amqps:// for encrypted connection
|
16
|
+
# @param read_loop_thread [Boolean] If true run {#read_loop} in a background thread,
|
17
|
+
# otherwise the user have to run it explicitly, without {#read_loop} the connection won't function
|
18
|
+
# @option options [Boolean] connection_name (PROGRAM_NAME) Set a name for the connection to be able to identify
|
19
|
+
# the client from the broker
|
20
|
+
# @option options [Boolean] verify_peer (true) Verify broker's TLS certificate, set to false for self-signed certs
|
21
|
+
# @option options [Integer] connect_timeout (30) TCP connection timeout
|
22
|
+
# @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
|
23
|
+
# @option options [Integer] frame_max (131_072) Maximum frame size,
|
24
|
+
# the smallest of the client's and the broker's values will be used
|
25
|
+
# @option options [Integer] channel_max (2048) Maxium number of channels the client will be allowed to have open.
|
26
|
+
# Maxium allowed is 65_536. The smallest of the client's and the broker's value will be used.
|
27
|
+
# @return [Connection]
|
28
|
+
def initialize(uri = "", read_loop_thread: true, **options)
|
29
|
+
uri = URI.parse(uri)
|
30
|
+
tls = uri.scheme == "amqps"
|
31
|
+
port = port_from_env || uri.port || (tls ? 5671 : 5672)
|
32
|
+
host = uri.host || "localhost"
|
33
|
+
user = uri.user || "guest"
|
34
|
+
password = uri.password || "guest"
|
35
|
+
vhost = URI.decode_www_form_component(uri.path[1..] || "/")
|
36
|
+
options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
|
37
|
+
|
38
|
+
socket = open_socket(host, port, tls, options)
|
39
|
+
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, options)
|
40
|
+
|
41
|
+
@socket = socket
|
42
|
+
@channel_max = channel_max.zero? ? 65_536 : channel_max
|
43
|
+
@frame_max = frame_max
|
44
|
+
@heartbeat = heartbeat
|
45
|
+
@channels = {}
|
46
|
+
@closed = nil
|
47
|
+
@replies = ::Queue.new
|
48
|
+
@write_lock = Mutex.new
|
49
|
+
@blocked = nil
|
50
|
+
Thread.new { read_loop } if read_loop_thread
|
36
51
|
end
|
37
|
-
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
|
38
|
-
Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
|
39
|
-
end
|
40
52
|
|
41
|
-
|
42
|
-
@
|
43
|
-
@
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
@closed = false
|
48
|
-
@replies = Queue.new
|
49
|
-
@write_lock = Mutex.new
|
50
|
-
Thread.new { read_loop } if read_loop_thread
|
51
|
-
end
|
53
|
+
# Alias for {#initialize}
|
54
|
+
# @see #initialize
|
55
|
+
# @deprecated
|
56
|
+
def self.connect(uri, read_loop_thread: true, **options)
|
57
|
+
new(uri, read_loop_thread: read_loop_thread, **options)
|
58
|
+
end
|
52
59
|
|
53
|
-
|
60
|
+
# The max frame size negotiated between the client and the broker
|
61
|
+
# @return [Integer]
|
62
|
+
attr_reader :frame_max
|
54
63
|
|
55
|
-
|
56
|
-
|
57
|
-
|
64
|
+
# Custom inspect
|
65
|
+
# @return [String]
|
66
|
+
# @api private
|
67
|
+
def inspect
|
68
|
+
"#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
|
69
|
+
end
|
58
70
|
|
59
|
-
|
60
|
-
|
61
|
-
|
71
|
+
# Open an AMQP channel
|
72
|
+
# @param id [Integer, nil] If nil a new channel will be opened, otherwise an already open channel might be reused
|
73
|
+
# @return [Channel]
|
74
|
+
def channel(id = nil)
|
75
|
+
raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
|
76
|
+
raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
|
62
77
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
raise AMQP::Client::Error, "Max channels reached" if id.nil?
|
78
|
+
if id
|
79
|
+
ch = @channels[id] ||= Channel.new(self, id)
|
80
|
+
else
|
81
|
+
1.upto(@channel_max) do |i|
|
82
|
+
break id = i unless @channels.key? i
|
83
|
+
end
|
84
|
+
raise Error, "Max channels reached" if id.nil?
|
71
85
|
|
72
|
-
|
86
|
+
ch = @channels[id] = Channel.new(self, id)
|
87
|
+
end
|
88
|
+
ch.open
|
73
89
|
end
|
74
|
-
ch.open
|
75
|
-
end
|
76
90
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
91
|
+
# Declare a new channel, yield, and then close the channel
|
92
|
+
# @yield [Channel]
|
93
|
+
# @return [Object] Whatever was returned by the block
|
94
|
+
def with_channel
|
95
|
+
ch = channel
|
96
|
+
begin
|
97
|
+
yield ch
|
98
|
+
ensure
|
99
|
+
ch.close
|
100
|
+
end
|
84
101
|
end
|
85
|
-
end
|
86
102
|
|
87
|
-
|
88
|
-
|
103
|
+
# Gracefully close a connection
|
104
|
+
# @param reason [String] A reason to close the connection can be logged by the broker
|
105
|
+
# @param code [Integer]
|
106
|
+
# @return [nil]
|
107
|
+
def close(reason: "", code: 200)
|
108
|
+
return if @closed
|
89
109
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
110
|
+
@closed = [code, reason]
|
111
|
+
@channels.each_value { |ch| ch.closed!(:connection, code, reason, 0, 0) }
|
112
|
+
if @blocked
|
113
|
+
@socket.close
|
114
|
+
else
|
115
|
+
write_bytes FrameBytes.connection_close(code, reason)
|
116
|
+
expect(:close_ok)
|
117
|
+
end
|
118
|
+
nil
|
119
|
+
end
|
95
120
|
|
96
|
-
|
97
|
-
@
|
98
|
-
|
121
|
+
# True if the connection is closed
|
122
|
+
# @return [Boolean]
|
123
|
+
def closed?
|
124
|
+
!@closed.nil?
|
125
|
+
end
|
99
126
|
|
100
|
-
|
101
|
-
|
127
|
+
# Write byte array(s) directly to the socket (thread-safe)
|
128
|
+
# @param bytes [String] One or more byte arrays
|
129
|
+
# @return [Integer] number of bytes written
|
130
|
+
# @api private
|
131
|
+
def write_bytes(*bytes)
|
132
|
+
blocked = @blocked
|
133
|
+
warn "AMQP-Client blocked by broker: #{blocked}" if blocked
|
102
134
|
@write_lock.synchronize do
|
135
|
+
warn "AMQP-Client unblocked by broker" if blocked
|
103
136
|
@socket.write(*bytes)
|
104
137
|
end
|
105
|
-
|
106
|
-
|
138
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
139
|
+
raise Error::ConnectionClosed.new(*@closed) if @closed
|
140
|
+
|
141
|
+
raise Error, "Could not write to socket, #{e.message}"
|
107
142
|
end
|
108
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
109
|
-
raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
|
110
|
-
end
|
111
143
|
|
112
|
-
|
113
|
-
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
144
|
+
# Reads from the socket, required for any kind of progress.
|
145
|
+
# Blocks until the connection is closed. Normally run as a background thread automatically.
|
146
|
+
# @return [nil]
|
147
|
+
def read_loop
|
148
|
+
# read more often than write so that channel errors crop up early
|
149
|
+
Thread.current.priority += 1
|
150
|
+
socket = @socket
|
151
|
+
frame_max = @frame_max
|
152
|
+
frame_start = String.new(capacity: 7)
|
153
|
+
frame_buffer = String.new(capacity: frame_max)
|
154
|
+
loop do
|
155
|
+
socket.read(7, frame_start)
|
156
|
+
type, channel_id, frame_size = frame_start.unpack("C S> L>")
|
157
|
+
frame_max >= frame_size || raise(Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}")
|
126
158
|
|
127
|
-
|
128
|
-
|
159
|
+
# read the frame content
|
160
|
+
socket.read(frame_size, frame_buffer)
|
129
161
|
|
130
|
-
|
131
|
-
|
132
|
-
|
162
|
+
# make sure that the frame end is correct
|
163
|
+
frame_end = socket.readchar.ord
|
164
|
+
raise UnexpectedFrameEnd, frame_end if frame_end != 206
|
133
165
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
138
|
-
warn "AMQP-Client read error: #{e.inspect}"
|
139
|
-
nil # ignore read errors
|
140
|
-
ensure
|
141
|
-
@closed = true
|
142
|
-
@replies.close
|
143
|
-
begin
|
144
|
-
@socket.close
|
145
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
166
|
+
# parse the frame, will return false if a close frame was received
|
167
|
+
parse_frame(type, channel_id, frame_buffer) || return
|
168
|
+
end
|
146
169
|
nil
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
case class_id
|
157
|
-
when 10 # connection
|
158
|
-
raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
159
|
-
|
160
|
-
case method_id
|
161
|
-
when 50 # connection#close
|
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
|
172
|
-
return false
|
173
|
-
when 51 # connection#close-ok
|
174
|
-
@closed = true
|
175
|
-
@replies.push [:close_ok]
|
176
|
-
return false
|
177
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
178
|
-
end
|
179
|
-
when 20 # channel
|
180
|
-
case method_id
|
181
|
-
when 11 # channel#open-ok
|
182
|
-
@channels[channel_id].reply [:channel_open_ok]
|
183
|
-
when 40 # channel#close
|
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>")
|
187
|
-
channel = @channels.delete(channel_id)
|
188
|
-
channel.closed!(reply_code, reply_text, classid, methodid)
|
189
|
-
write_bytes FrameBytes.channel_close_ok(channel_id)
|
190
|
-
when 41 # channel#close-ok
|
191
|
-
channel = @channels.delete(channel_id)
|
192
|
-
channel.reply [:channel_close_ok]
|
193
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
194
|
-
end
|
195
|
-
when 40 # exchange
|
196
|
-
case method_id
|
197
|
-
when 11 # declare-ok
|
198
|
-
@channels[channel_id].reply [:exchange_declare_ok]
|
199
|
-
when 21 # delete-ok
|
200
|
-
@channels[channel_id].reply [:exchange_delete_ok]
|
201
|
-
when 31 # bind-ok
|
202
|
-
@channels[channel_id].reply [:exchange_bind_ok]
|
203
|
-
when 51 # unbind-ok
|
204
|
-
@channels[channel_id].reply [:exchange_unbind_ok]
|
205
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
206
|
-
end
|
207
|
-
when 50 # queue
|
208
|
-
case method_id
|
209
|
-
when 11 # declare-ok
|
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>")
|
213
|
-
@channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
|
214
|
-
when 21 # bind-ok
|
215
|
-
@channels[channel_id].reply [:queue_bind_ok]
|
216
|
-
when 31 # purge-ok
|
217
|
-
@channels[channel_id].reply [:queue_purge_ok]
|
218
|
-
when 41 # delete-ok
|
219
|
-
message_count = buf.unpack1("@4 L>")
|
220
|
-
@channels[channel_id].reply [:queue_delete, message_count]
|
221
|
-
when 51 # unbind-ok
|
222
|
-
@channels[channel_id].reply [:queue_unbind_ok]
|
223
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
224
|
-
end
|
225
|
-
when 60 # basic
|
226
|
-
case method_id
|
227
|
-
when 11 # qos-ok
|
228
|
-
@channels[channel_id].reply [:basic_qos_ok]
|
229
|
-
when 21 # consume-ok
|
230
|
-
tag_len = buf.unpack1("@4 C")
|
231
|
-
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
232
|
-
@channels[channel_id].reply [:basic_consume_ok, tag]
|
233
|
-
when 30 # cancel
|
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)
|
238
|
-
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
|
239
|
-
when 31 # cancel-ok
|
240
|
-
tag_len = buf.unpack1("@4 C")
|
241
|
-
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
242
|
-
@channels[channel_id].reply [:basic_cancel_ok, tag]
|
243
|
-
when 50 # return
|
244
|
-
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
245
|
-
pos = 7
|
246
|
-
reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
|
247
|
-
pos += reply_text_len
|
248
|
-
exchange_len = buf[pos].ord
|
249
|
-
pos += 1
|
250
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
251
|
-
pos += exchange_len
|
252
|
-
routing_key_len = buf[pos].ord
|
253
|
-
pos += 1
|
254
|
-
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
255
|
-
@channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
|
256
|
-
when 60 # deliver
|
257
|
-
ctag_len = buf[4].ord
|
258
|
-
consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
|
259
|
-
pos = 5 + ctag_len
|
260
|
-
delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
|
261
|
-
pos += 8 + 1 + 1
|
262
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
263
|
-
pos += exchange_len
|
264
|
-
rk_len = buf[pos].ord
|
265
|
-
pos += 1
|
266
|
-
routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
|
267
|
-
@channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
|
268
|
-
when 71 # get-ok
|
269
|
-
delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
|
270
|
-
pos = 14
|
271
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
272
|
-
pos += exchange_len
|
273
|
-
routing_key_len = buf[pos].ord
|
274
|
-
pos += 1
|
275
|
-
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
276
|
-
pos += routing_key_len
|
277
|
-
_message_count = buf.byteslice(pos, 4).unpack1("L>")
|
278
|
-
@channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
|
279
|
-
when 72 # get-empty
|
280
|
-
@channels[channel_id].basic_get_empty
|
281
|
-
when 80 # ack
|
282
|
-
delivery_tag, multiple = buf.unpack("@4 Q> C")
|
283
|
-
@channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
|
284
|
-
when 111 # recover-ok
|
285
|
-
@channels[channel_id].reply [:basic_recover_ok]
|
286
|
-
when 120 # nack
|
287
|
-
delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
|
288
|
-
@channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
|
289
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
290
|
-
end
|
291
|
-
when 85 # confirm
|
292
|
-
case method_id
|
293
|
-
when 11 # select-ok
|
294
|
-
@channels[channel_id].reply [:confirm_select_ok]
|
295
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
296
|
-
end
|
297
|
-
when 90 # tx
|
298
|
-
case method_id
|
299
|
-
when 11 # select-ok
|
300
|
-
@channels[channel_id].reply [:tx_select_ok]
|
301
|
-
when 21 # commit-ok
|
302
|
-
@channels[channel_id].reply [:tx_commit_ok]
|
303
|
-
when 31 # rollback-ok
|
304
|
-
@channels[channel_id].reply [:tx_rollback_ok]
|
305
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
170
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
171
|
+
@closed ||= [400, "read error: #{e.message}"]
|
172
|
+
nil # ignore read errors
|
173
|
+
ensure
|
174
|
+
@closed ||= [400, "unknown"]
|
175
|
+
@replies.close
|
176
|
+
begin
|
177
|
+
@write_lock.synchronize do
|
178
|
+
@socket.close
|
306
179
|
end
|
307
|
-
|
180
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
181
|
+
nil
|
308
182
|
end
|
309
|
-
when 2 # header
|
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
|
313
|
-
when 3 # body
|
314
|
-
@channels[channel_id].body_delivered buf
|
315
|
-
else raise AMQP::Client::UnsupportedFrameType, type
|
316
183
|
end
|
317
|
-
true
|
318
|
-
end
|
319
|
-
|
320
|
-
def expect(expected_frame_type)
|
321
|
-
frame_type, args = @replies.pop
|
322
|
-
frame_type == expected_frame_type || raise(AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type))
|
323
|
-
args
|
324
|
-
end
|
325
184
|
|
326
|
-
|
327
|
-
channel_max, frame_max, heartbeat = nil
|
328
|
-
socket.write "AMQP\x00\x00\x09\x01"
|
329
|
-
buf = String.new(capacity: 4096)
|
330
|
-
loop do
|
331
|
-
begin
|
332
|
-
socket.readpartial(4096, buf)
|
333
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
334
|
-
raise AMQP::Client::Error, "Could not establish AMQP connection: #{e.message}"
|
335
|
-
end
|
336
|
-
|
337
|
-
type, channel_id, frame_size = buf.unpack("C S> L>")
|
338
|
-
frame_end = buf[frame_size + 7].ord
|
339
|
-
raise UnexpectedFrameEndError, frame_end if frame_end != 206
|
185
|
+
private
|
340
186
|
|
187
|
+
def parse_frame(type, channel_id, buf)
|
341
188
|
case type
|
342
189
|
when 1 # method frame
|
343
|
-
class_id, method_id = buf.unpack("
|
190
|
+
class_id, method_id = buf.unpack("S> S>")
|
344
191
|
case class_id
|
345
192
|
when 10 # connection
|
346
|
-
raise
|
193
|
+
raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
347
194
|
|
348
195
|
case method_id
|
349
|
-
when 10 # connection#start
|
350
|
-
conn_name = options[:connection_name] || $PROGRAM_NAME
|
351
|
-
properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
|
352
|
-
socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
|
353
|
-
when 30 # connection#tune
|
354
|
-
channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
|
355
|
-
channel_max = [channel_max, 2048].min
|
356
|
-
frame_max = [frame_max, 131_072].min
|
357
|
-
heartbeat = [heartbeat, 0].min
|
358
|
-
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
359
|
-
socket.write FrameBytes.connection_open(vhost)
|
360
|
-
when 41 # connection#open-ok
|
361
|
-
return [channel_max, frame_max, heartbeat]
|
362
196
|
when 50 # connection#close
|
363
|
-
code, text_len = buf.unpack("@
|
364
|
-
text
|
365
|
-
|
366
|
-
|
367
|
-
|
197
|
+
code, text_len = buf.unpack("@4 S> C")
|
198
|
+
text = buf.byteslice(7, text_len).force_encoding("utf-8")
|
199
|
+
error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
|
200
|
+
@closed = [code, text, error_class_id, error_method_id]
|
201
|
+
@channels.each_value { |ch| ch.closed!(:connection, code, text, error_class_id, error_method_id) }
|
202
|
+
begin
|
203
|
+
write_bytes FrameBytes.connection_close_ok
|
204
|
+
rescue Error
|
205
|
+
nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
|
206
|
+
end
|
207
|
+
return false
|
208
|
+
when 51 # connection#close-ok
|
209
|
+
@replies.push [:close_ok]
|
210
|
+
return false
|
211
|
+
when 60 # connection#blocked
|
212
|
+
reason_len = buf.getbyte(4)
|
213
|
+
reason = buf.byteslice(5, reason_len).force_encoding("utf-8")
|
214
|
+
@blocked = reason
|
215
|
+
@write_lock.lock
|
216
|
+
when 61 # connection#unblocked
|
217
|
+
@blocked = nil
|
218
|
+
@write_lock.unlock
|
219
|
+
else raise Error::UnsupportedMethodFrame, class_id, method_id
|
220
|
+
end
|
221
|
+
when 20 # channel
|
222
|
+
case method_id
|
223
|
+
when 11 # channel#open-ok
|
224
|
+
@channels[channel_id].reply [:channel_open_ok]
|
225
|
+
when 40 # channel#close
|
226
|
+
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
227
|
+
reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
|
228
|
+
classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
|
229
|
+
channel = @channels.delete(channel_id)
|
230
|
+
channel.closed!(:channel, reply_code, reply_text, classid, methodid)
|
231
|
+
write_bytes FrameBytes.channel_close_ok(channel_id)
|
232
|
+
when 41 # channel#close-ok
|
233
|
+
channel = @channels.delete(channel_id)
|
234
|
+
channel.reply [:channel_close_ok]
|
235
|
+
else raise Error::UnsupportedMethodFrame, class_id, method_id
|
236
|
+
end
|
237
|
+
when 40 # exchange
|
238
|
+
case method_id
|
239
|
+
when 11 # declare-ok
|
240
|
+
@channels[channel_id].reply [:exchange_declare_ok]
|
241
|
+
when 21 # delete-ok
|
242
|
+
@channels[channel_id].reply [:exchange_delete_ok]
|
243
|
+
when 31 # bind-ok
|
244
|
+
@channels[channel_id].reply [:exchange_bind_ok]
|
245
|
+
when 51 # unbind-ok
|
246
|
+
@channels[channel_id].reply [:exchange_unbind_ok]
|
247
|
+
else raise Error::UnsupportedMethodFrame, class_id, method_id
|
248
|
+
end
|
249
|
+
when 50 # queue
|
250
|
+
case method_id
|
251
|
+
when 11 # declare-ok
|
252
|
+
queue_name_len = buf.getbyte(4)
|
253
|
+
queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
|
254
|
+
message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
|
255
|
+
@channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
|
256
|
+
when 21 # bind-ok
|
257
|
+
@channels[channel_id].reply [:queue_bind_ok]
|
258
|
+
when 31 # purge-ok
|
259
|
+
@channels[channel_id].reply [:queue_purge_ok]
|
260
|
+
when 41 # delete-ok
|
261
|
+
message_count = buf.unpack1("@4 L>")
|
262
|
+
@channels[channel_id].reply [:queue_delete, message_count]
|
263
|
+
when 51 # unbind-ok
|
264
|
+
@channels[channel_id].reply [:queue_unbind_ok]
|
265
|
+
else raise Error::UnsupportedMethodFrame.new class_id, method_id
|
368
266
|
end
|
369
|
-
|
267
|
+
when 60 # basic
|
268
|
+
case method_id
|
269
|
+
when 11 # qos-ok
|
270
|
+
@channels[channel_id].reply [:basic_qos_ok]
|
271
|
+
when 21 # consume-ok
|
272
|
+
tag_len = buf.getbyte(4)
|
273
|
+
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
274
|
+
@channels[channel_id].reply [:basic_consume_ok, tag]
|
275
|
+
when 30 # cancel
|
276
|
+
tag_len = buf.getbyte(4)
|
277
|
+
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
278
|
+
no_wait = buf.getbyte(5 + tag_len) == 1
|
279
|
+
@channels[channel_id].close_consumer(tag)
|
280
|
+
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait
|
281
|
+
when 31 # cancel-ok
|
282
|
+
tag_len = buf.getbyte(4)
|
283
|
+
tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
|
284
|
+
@channels[channel_id].reply [:basic_cancel_ok, tag]
|
285
|
+
when 50 # return
|
286
|
+
reply_code, reply_text_len = buf.unpack("@4 S> C")
|
287
|
+
pos = 7
|
288
|
+
reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
|
289
|
+
pos += reply_text_len
|
290
|
+
exchange_len = buf.getbyte(pos)
|
291
|
+
pos += 1
|
292
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
293
|
+
pos += exchange_len
|
294
|
+
routing_key_len = buf.getbyte(pos)
|
295
|
+
pos += 1
|
296
|
+
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
297
|
+
@channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
|
298
|
+
when 60 # deliver
|
299
|
+
ctag_len = buf.getbyte(4)
|
300
|
+
consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
|
301
|
+
pos = 5 + ctag_len
|
302
|
+
delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
|
303
|
+
pos += 8 + 1 + 1
|
304
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
305
|
+
pos += exchange_len
|
306
|
+
rk_len = buf.getbyte(pos)
|
307
|
+
pos += 1
|
308
|
+
routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
|
309
|
+
@channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
|
310
|
+
when 71 # get-ok
|
311
|
+
delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
|
312
|
+
pos = 14
|
313
|
+
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
314
|
+
pos += exchange_len
|
315
|
+
routing_key_len = buf.getbyte(pos)
|
316
|
+
pos += 1
|
317
|
+
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
318
|
+
# pos += routing_key_len
|
319
|
+
# message_count = buf.byteslice(pos, 4).unpack1("L>")
|
320
|
+
@channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
|
321
|
+
when 72 # get-empty
|
322
|
+
@channels[channel_id].basic_get_empty
|
323
|
+
when 80 # ack
|
324
|
+
delivery_tag, multiple = buf.unpack("@4 Q> C")
|
325
|
+
@channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
|
326
|
+
when 111 # recover-ok
|
327
|
+
@channels[channel_id].reply [:basic_recover_ok]
|
328
|
+
when 120 # nack
|
329
|
+
delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
|
330
|
+
@channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
|
331
|
+
else raise Error::UnsupportedMethodFrame.new class_id, method_id
|
332
|
+
end
|
333
|
+
when 85 # confirm
|
334
|
+
case method_id
|
335
|
+
when 11 # select-ok
|
336
|
+
@channels[channel_id].reply [:confirm_select_ok]
|
337
|
+
else raise Error::UnsupportedMethodFrame.new class_id, method_id
|
338
|
+
end
|
339
|
+
when 90 # tx
|
340
|
+
case method_id
|
341
|
+
when 11 # select-ok
|
342
|
+
@channels[channel_id].reply [:tx_select_ok]
|
343
|
+
when 21 # commit-ok
|
344
|
+
@channels[channel_id].reply [:tx_commit_ok]
|
345
|
+
when 31 # rollback-ok
|
346
|
+
@channels[channel_id].reply [:tx_rollback_ok]
|
347
|
+
else raise Error::UnsupportedMethodFrame.new class_id, method_id
|
348
|
+
end
|
349
|
+
else raise Error::UnsupportedMethodFrame.new class_id, method_id
|
370
350
|
end
|
371
|
-
|
351
|
+
when 2 # header
|
352
|
+
body_size = buf.unpack1("@4 Q>")
|
353
|
+
properties = Properties.decode(buf, 12)
|
354
|
+
@channels[channel_id].header_delivered body_size, properties
|
355
|
+
when 3 # body
|
356
|
+
@channels[channel_id].body_delivered buf
|
357
|
+
else raise Error::UnsupportedFrameType, type
|
372
358
|
end
|
359
|
+
true
|
373
360
|
end
|
374
|
-
rescue StandardError
|
375
|
-
socket.close rescue nil
|
376
|
-
raise
|
377
|
-
end
|
378
361
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
362
|
+
def expect(expected_frame_type)
|
363
|
+
frame_type, args = @replies.pop
|
364
|
+
if frame_type.nil?
|
365
|
+
return if expected_frame_type == :close_ok
|
366
|
+
|
367
|
+
raise(Error::ConnectionClosed, "while waiting for #{expected_frame_type}")
|
368
|
+
end
|
369
|
+
frame_type == expected_frame_type || raise(Error::UnexpectedFrame.new(expected_frame_type, frame_type))
|
370
|
+
args
|
371
|
+
end
|
387
372
|
|
388
|
-
|
389
|
-
return
|
373
|
+
# Connect to the host/port, optionally establish a TLS connection
|
374
|
+
# @return [Socket]
|
375
|
+
# @return [OpenSSL::SSL::SSLSocket]
|
376
|
+
def open_socket(host, port, tls, options)
|
377
|
+
connect_timeout = options.fetch(:connect_timeout, 30).to_i
|
378
|
+
socket = Socket.tcp host, port, connect_timeout: connect_timeout
|
379
|
+
enable_tcp_keepalive(socket)
|
380
|
+
if tls
|
381
|
+
cert_store = OpenSSL::X509::Store.new
|
382
|
+
cert_store.set_default_paths
|
383
|
+
context = OpenSSL::SSL::SSLContext.new
|
384
|
+
context.cert_store = cert_store
|
385
|
+
verify_peer = [false, "false", "none"].include? options[:verify_peer]
|
386
|
+
context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless verify_peer
|
387
|
+
socket = OpenSSL::SSL::SSLSocket.new(socket, context)
|
388
|
+
socket.sync_close = true # closing the TLS socket also closes the TCP socket
|
389
|
+
socket.hostname = host # SNI host
|
390
|
+
socket.connect
|
391
|
+
socket.post_connection_check(host) || raise(Error, "TLS certificate hostname doesn't match requested")
|
392
|
+
end
|
393
|
+
socket
|
394
|
+
end
|
390
395
|
|
391
|
-
|
392
|
-
|
396
|
+
# Negotiate a connection
|
397
|
+
# @return [Array<Integer, Integer, Integer>] channel_max, frame_max, heartbeat
|
398
|
+
def establish(socket, user, password, vhost, options)
|
399
|
+
channel_max, frame_max, heartbeat = nil
|
400
|
+
socket.write "AMQP\x00\x00\x09\x01"
|
401
|
+
buf = String.new(capacity: 4096)
|
402
|
+
loop do
|
403
|
+
begin
|
404
|
+
socket.readpartial(4096, buf)
|
405
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
406
|
+
raise Error, "Could not establish AMQP connection: #{e.message}"
|
407
|
+
end
|
408
|
+
|
409
|
+
type, channel_id, frame_size = buf.unpack("C S> L>")
|
410
|
+
frame_end = buf.getbyte(frame_size + 7)
|
411
|
+
raise UnexpectedFrameEndError, frame_end if frame_end != 206
|
412
|
+
|
413
|
+
case type
|
414
|
+
when 1 # method frame
|
415
|
+
class_id, method_id = buf.unpack("@7 S> S>")
|
416
|
+
case class_id
|
417
|
+
when 10 # connection
|
418
|
+
raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
419
|
+
|
420
|
+
case method_id
|
421
|
+
when 10 # connection#start
|
422
|
+
conn_name = options[:connection_name] || $PROGRAM_NAME
|
423
|
+
properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
|
424
|
+
socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
|
425
|
+
when 30 # connection#tune
|
426
|
+
channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
|
427
|
+
channel_max = 65_536 if channel_max.zero?
|
428
|
+
channel_max = [channel_max, options.fetch(:channel_max, 2048).to_i].min
|
429
|
+
frame_max = [frame_max, options.fetch(:frame_max, 131_072).to_i].min
|
430
|
+
heartbeat = [heartbeat, options.fetch(:heartbeat, 0).to_i].min
|
431
|
+
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
432
|
+
socket.write FrameBytes.connection_open(vhost)
|
433
|
+
when 41 # connection#open-ok
|
434
|
+
return [channel_max, frame_max, heartbeat]
|
435
|
+
when 50 # connection#close
|
436
|
+
code, text_len = buf.unpack("@11 S> C")
|
437
|
+
text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
|
438
|
+
socket.write FrameBytes.connection_close_ok
|
439
|
+
raise Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
|
440
|
+
else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
|
441
|
+
end
|
442
|
+
else raise Error, "Unexpected class/method: #{class_id} #{method_id}"
|
443
|
+
end
|
444
|
+
else raise Error, "Unexpected frame type: #{type}"
|
445
|
+
end
|
446
|
+
end
|
447
|
+
rescue StandardError => e
|
448
|
+
begin
|
449
|
+
socket.close
|
450
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
451
|
+
nil
|
452
|
+
end
|
453
|
+
raise e
|
454
|
+
end
|
455
|
+
|
456
|
+
# Enable TCP keepalive, which is prefered to heartbeats
|
457
|
+
# @return [void]
|
458
|
+
def enable_tcp_keepalive(socket)
|
459
|
+
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
|
460
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
|
461
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
|
462
|
+
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
|
463
|
+
rescue StandardError => e
|
464
|
+
warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
|
465
|
+
end
|
466
|
+
|
467
|
+
# Fetch the AMQP port number from ENV
|
468
|
+
# @return [Integer] A port number
|
469
|
+
# @return [nil] When the environment variable AMQP_PORT isn't set
|
470
|
+
def port_from_env
|
471
|
+
return unless (port = ENV["AMQP_PORT"])
|
393
472
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
473
|
+
port.to_i
|
474
|
+
end
|
475
|
+
|
476
|
+
CLIENT_PROPERTIES = {
|
477
|
+
capabilities: {
|
478
|
+
authentication_failure_close: true,
|
479
|
+
publisher_confirms: true,
|
480
|
+
consumer_cancel_notify: true,
|
481
|
+
exchange_exchange_bindings: true,
|
482
|
+
"basic.nack": true,
|
483
|
+
"connection.blocked": true
|
484
|
+
},
|
485
|
+
product: "amqp-client.rb",
|
486
|
+
platform: RUBY_DESCRIPTION,
|
487
|
+
version: VERSION,
|
488
|
+
information: "http://github.com/cloudamqp/amqp-client.rb"
|
489
|
+
}.freeze
|
490
|
+
end
|
410
491
|
end
|
411
492
|
end
|