amqp-client 1.0.0 → 1.1.1
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/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
|