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