amqp-client 0.2.2 → 1.0.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/.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
|