amqp-client 0.3.0 → 1.1.0
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 +43 -0
- data/Gemfile +4 -0
- data/README.md +59 -26
- data/Rakefile +3 -1
- data/amqp-client.gemspec +1 -1
- data/lib/amqp/client/channel.rb +490 -258
- data/lib/amqp/client/connection.rb +439 -343
- data/lib/amqp/client/errors.rb +46 -25
- data/lib/amqp/client/exchange.rb +66 -0
- data/lib/amqp/client/frames.rb +524 -522
- data/lib/amqp/client/message.rb +103 -7
- data/lib/amqp/client/properties.rb +230 -166
- data/lib/amqp/client/queue.rb +72 -0
- data/lib/amqp/client/table.rb +117 -108
- data/lib/amqp/client/version.rb +2 -1
- data/lib/amqp/client.rb +171 -56
- data/sig/amqp-client.rbs +264 -0
- metadata +9 -4
@@ -8,389 +8,485 @@ 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
|
-
|
22
|
-
|
23
|
-
options
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
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)
|
43
|
-
@socket = socket
|
44
|
-
@channel_max = channel_max
|
45
|
-
@frame_max = frame_max
|
46
|
-
@heartbeat = heartbeat
|
47
|
-
@channels = {}
|
48
|
-
@closed = false
|
49
|
-
@replies = Queue.new
|
50
|
-
Thread.new { read_loop } if read_loop_thread
|
51
|
-
end
|
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)
|
52
37
|
|
53
|
-
|
38
|
+
socket = open_socket(host, port, tls, options)
|
39
|
+
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, options)
|
54
40
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
61
51
|
end
|
62
|
-
ch.open
|
63
|
-
end
|
64
52
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
ensure
|
71
|
-
ch.close
|
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)
|
72
58
|
end
|
73
|
-
end
|
74
59
|
|
75
|
-
|
76
|
-
return
|
60
|
+
# The max frame size negotiated between the client and the broker
|
61
|
+
# @return [Integer]
|
62
|
+
attr_reader :frame_max
|
77
63
|
|
78
|
-
|
79
|
-
|
80
|
-
@
|
81
|
-
|
64
|
+
# Custom inspect
|
65
|
+
# @return [String]
|
66
|
+
# @api private
|
67
|
+
def inspect
|
68
|
+
"#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
|
69
|
+
end
|
82
70
|
|
83
|
-
|
84
|
-
@
|
85
|
-
|
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
|
86
77
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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?
|
92
85
|
|
93
|
-
|
94
|
-
def read_loop
|
95
|
-
socket = @socket
|
96
|
-
frame_max = @frame_max
|
97
|
-
buffer = String.new(capacity: frame_max)
|
98
|
-
loop do
|
99
|
-
begin
|
100
|
-
socket.readpartial(frame_max, buffer)
|
101
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
102
|
-
break
|
86
|
+
ch = @channels[id] = Channel.new(self, id)
|
103
87
|
end
|
88
|
+
ch.open
|
89
|
+
end
|
104
90
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
101
|
+
end
|
112
102
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
117
109
|
|
118
|
-
|
119
|
-
|
120
|
-
|
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)
|
121
117
|
end
|
122
|
-
end
|
123
|
-
ensure
|
124
|
-
@closed = true
|
125
|
-
@replies.close
|
126
|
-
begin
|
127
|
-
@socket.close
|
128
|
-
rescue IOError
|
129
118
|
nil
|
130
119
|
end
|
131
|
-
end
|
132
120
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
write_bytes FrameBytes.connection_close_ok
|
150
|
-
return false
|
151
|
-
when 51 # connection#close-ok
|
152
|
-
@replies.push [:close_ok]
|
153
|
-
return false
|
154
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
155
|
-
end
|
156
|
-
when 20 # channel
|
157
|
-
case method_id
|
158
|
-
when 11 # channel#open-ok
|
159
|
-
@channels[channel_id].reply [:channel_open_ok]
|
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>")
|
164
|
-
channel = @channels.delete(channel_id)
|
165
|
-
channel.closed!(reply_code, reply_text, classid, methodid)
|
166
|
-
when 41 # channel#close-ok
|
167
|
-
channel = @channels.delete(channel_id)
|
168
|
-
channel.reply [:channel_close_ok]
|
169
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
170
|
-
end
|
171
|
-
when 40 # exchange
|
172
|
-
case method_id
|
173
|
-
when 11 # declare-ok
|
174
|
-
@channels[channel_id].reply [:exchange_declare_ok]
|
175
|
-
when 21 # delete-ok
|
176
|
-
@channels[channel_id].reply [:exchange_delete_ok]
|
177
|
-
when 31 # bind-ok
|
178
|
-
@channels[channel_id].reply [:exchange_bind_ok]
|
179
|
-
when 51 # unbind-ok
|
180
|
-
@channels[channel_id].reply [:exchange_unbind_ok]
|
181
|
-
else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
|
182
|
-
end
|
183
|
-
when 50 # queue
|
184
|
-
case method_id
|
185
|
-
when 11 # declare-ok
|
186
|
-
queue_name_len = buf.unpack1("@11 C")
|
187
|
-
queue_name = buf.byteslice(12, queue_name_len).force_encoding("utf-8")
|
188
|
-
message_count, consumer_count = buf.byteslice(12 + queue_name_len, 8).unpack1("L> L>")
|
189
|
-
@channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
|
190
|
-
when 21 # bind-ok
|
191
|
-
@channels[channel_id].reply [:queue_bind_ok]
|
192
|
-
when 31 # purge-ok
|
193
|
-
@channels[channel_id].reply [:queue_purge_ok]
|
194
|
-
when 41 # delete-ok
|
195
|
-
message_count = buf.unpack1("@11 L>")
|
196
|
-
@channels[channel_id].reply [:queue_delete, message_count]
|
197
|
-
when 51 # unbind-ok
|
198
|
-
@channels[channel_id].reply [:queue_unbind_ok]
|
199
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
200
|
-
end
|
201
|
-
when 60 # basic
|
202
|
-
case method_id
|
203
|
-
when 11 # qos-ok
|
204
|
-
@channels[channel_id].reply [:basic_qos_ok]
|
205
|
-
when 21 # consume-ok
|
206
|
-
tag_len = buf.unpack1("@11 C")
|
207
|
-
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
208
|
-
@channels[channel_id].reply [:basic_consume_ok, tag]
|
209
|
-
when 30 # cancel
|
210
|
-
tag_len = buf.unpack1("@11 C")
|
211
|
-
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
212
|
-
no_wait = buf[12 + tag_len].ord
|
213
|
-
@channels[channel_id].consumers.fetch(tag).close
|
214
|
-
write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
|
215
|
-
when 31 # cancel-ok
|
216
|
-
tag_len = buf.unpack1("@11 C")
|
217
|
-
tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
|
218
|
-
@channels[channel_id].reply [:basic_cancel_ok, tag]
|
219
|
-
when 50 # return
|
220
|
-
reply_code, reply_text_len = buf.unpack("@11 S> C")
|
221
|
-
pos = 14
|
222
|
-
reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
|
223
|
-
pos += reply_text_len
|
224
|
-
exchange_len = buf[pos].ord
|
225
|
-
pos += 1
|
226
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
227
|
-
pos += exchange_len
|
228
|
-
routing_key_len = buf[pos].ord
|
229
|
-
pos += 1
|
230
|
-
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
231
|
-
@channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
|
232
|
-
when 60 # deliver
|
233
|
-
ctag_len = buf[11].ord
|
234
|
-
consumer_tag = buf.byteslice(12, ctag_len).force_encoding("utf-8")
|
235
|
-
pos = 12 + ctag_len
|
236
|
-
delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
|
237
|
-
pos += 8 + 1 + 1
|
238
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
239
|
-
pos += exchange_len
|
240
|
-
rk_len = buf[pos].ord
|
241
|
-
pos += 1
|
242
|
-
routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
|
243
|
-
loop do
|
244
|
-
if (consumer = @channels[channel_id].consumers[consumer_tag])
|
245
|
-
consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
|
246
|
-
break
|
247
|
-
else
|
248
|
-
Thread.pass
|
249
|
-
end
|
250
|
-
end
|
251
|
-
when 71 # get-ok
|
252
|
-
delivery_tag, redelivered, exchange_len = buf.unpack("@11 Q> C C")
|
253
|
-
pos = 21
|
254
|
-
exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
|
255
|
-
pos += exchange_len
|
256
|
-
routing_key_len = buf[pos].ord
|
257
|
-
pos += 1
|
258
|
-
routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
|
259
|
-
pos += routing_key_len
|
260
|
-
message_count = buf.byteslice(pos, 4).unpack1("L>")
|
261
|
-
redelivered = redelivered == 1
|
262
|
-
@channels[channel_id].reply [:basic_get_ok, delivery_tag, exchange, routing_key, message_count, redelivered]
|
263
|
-
when 72 # get-empty
|
264
|
-
@channels[channel_id].reply [:basic_get_empty]
|
265
|
-
when 80 # ack
|
266
|
-
delivery_tag, multiple = buf.unpack("@11 Q> C")
|
267
|
-
@channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
|
268
|
-
when 111 # recover-ok
|
269
|
-
@channels[channel_id].reply [:basic_recover_ok]
|
270
|
-
when 120 # nack
|
271
|
-
delivery_tag, multiple, requeue = buf.unpack("@11 Q> C C")
|
272
|
-
@channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
|
273
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
274
|
-
end
|
275
|
-
when 85 # confirm
|
276
|
-
case method_id
|
277
|
-
when 11 # select-ok
|
278
|
-
@channels[channel_id].reply [:confirm_select_ok]
|
279
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
280
|
-
end
|
281
|
-
when 90 # tx
|
282
|
-
case method_id
|
283
|
-
when 11 # select-ok
|
284
|
-
@channels[channel_id].reply [:tx_select_ok]
|
285
|
-
when 21 # commit-ok
|
286
|
-
@channels[channel_id].reply [:tx_commit_ok]
|
287
|
-
when 31 # rollback-ok
|
288
|
-
@channels[channel_id].reply [:tx_rollback_ok]
|
289
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
290
|
-
end
|
291
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
121
|
+
# True if the connection is closed
|
122
|
+
# @return [Boolean]
|
123
|
+
def closed?
|
124
|
+
!@closed.nil?
|
125
|
+
end
|
126
|
+
|
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
|
134
|
+
@write_lock.synchronize do
|
135
|
+
warn "AMQP-Client unblocked by broker" if blocked
|
136
|
+
@socket.write(*bytes)
|
292
137
|
end
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
when 3 # body
|
298
|
-
body = buf.byteslice(7, frame_size)
|
299
|
-
@channels[channel_id].reply [:body, body]
|
300
|
-
else raise AMQP::Client::UnsupportedFrameType, type
|
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}"
|
301
142
|
end
|
302
|
-
true
|
303
|
-
end
|
304
143
|
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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}")
|
158
|
+
|
159
|
+
# read the frame content
|
160
|
+
socket.read(frame_size, frame_buffer)
|
310
161
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
162
|
+
# make sure that the frame end is correct
|
163
|
+
frame_end = socket.readchar.ord
|
164
|
+
raise UnexpectedFrameEnd, frame_end if frame_end != 206
|
165
|
+
|
166
|
+
# parse the frame, will return false if a close frame was received
|
167
|
+
parse_frame(type, channel_id, frame_buffer) || return
|
168
|
+
end
|
169
|
+
nil
|
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
|
316
176
|
begin
|
317
|
-
|
318
|
-
|
319
|
-
|
177
|
+
@write_lock.synchronize do
|
178
|
+
@socket.close
|
179
|
+
end
|
180
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
181
|
+
nil
|
320
182
|
end
|
183
|
+
end
|
321
184
|
|
322
|
-
|
323
|
-
frame_end = buf.unpack1("@#{frame_size + 7} C")
|
324
|
-
raise UnexpectedFrameEndError, frame_end if frame_end != 206
|
185
|
+
private
|
325
186
|
|
187
|
+
def parse_frame(type, channel_id, buf)
|
326
188
|
case type
|
327
189
|
when 1 # method frame
|
328
|
-
class_id, method_id = buf.unpack("
|
190
|
+
class_id, method_id = buf.unpack("S> S>")
|
329
191
|
case class_id
|
330
192
|
when 10 # connection
|
331
|
-
raise
|
193
|
+
raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
332
194
|
|
333
195
|
case method_id
|
334
|
-
when 10 # connection#start
|
335
|
-
conn_name = options[:connection_name] || $PROGRAM_NAME
|
336
|
-
properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
|
337
|
-
socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
|
338
|
-
when 30 # connection#tune
|
339
|
-
channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
|
340
|
-
channel_max = [channel_max, 2048].min
|
341
|
-
frame_max = [frame_max, 131_072].min
|
342
|
-
heartbeat = [heartbeat, 0].min
|
343
|
-
socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
|
344
|
-
socket.write FrameBytes.connection_open(vhost)
|
345
|
-
when 41 # connection#open-ok
|
346
|
-
return [channel_max, frame_max, heartbeat]
|
347
196
|
when 50 # connection#close
|
348
|
-
code, text_len = buf.unpack("@
|
349
|
-
text
|
350
|
-
|
351
|
-
|
352
|
-
|
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
|
266
|
+
end
|
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
|
353
332
|
end
|
354
|
-
|
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
|
355
350
|
end
|
356
|
-
|
351
|
+
when 2 # header
|
352
|
+
body_size = buf.unpack1("@4 Q>")
|
353
|
+
properties = Properties.decode(buf.byteslice(12, buf.bytesize - 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
|
357
358
|
end
|
359
|
+
true
|
358
360
|
end
|
359
|
-
rescue StandardError
|
360
|
-
socket.close rescue nil
|
361
|
-
raise
|
362
|
-
end
|
363
361
|
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
|
369
|
-
rescue StandardError => e
|
370
|
-
warn "amqp-client: Could not enable TCP keepalive on socket. #{e.inspect}"
|
371
|
-
end
|
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
|
372
366
|
|
373
|
-
|
374
|
-
|
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
|
375
372
|
|
376
|
-
port
|
377
|
-
|
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
|
395
|
+
|
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"])
|
378
472
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
395
491
|
end
|
396
492
|
end
|