amqp-client 1.0.0 → 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 +10 -0
- data/README.md +4 -0
- data/lib/amqp/client/channel.rb +480 -275
- data/lib/amqp/client/connection.rb +401 -356
- 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 -194
- 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 +128 -95
- metadata +6 -2
@@ -8,404 +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
|
-
options
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
11
|
+
class Client
|
12
|
+
# Represents a single established AMQP connection
|
13
|
+
class Connection
|
14
|
+
# Establish a connection to an AMQP broker
|
15
|
+
# @param uri [String] URL on the format amqp://username:password@hostname/vhost, use amqps:// for encrypted connection
|
16
|
+
# @param read_loop_thread [Boolean] 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)
|
35
|
+
|
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)
|
36
52
|
end
|
37
|
-
channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
|
38
|
-
Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
|
39
|
-
end
|
40
53
|
|
41
|
-
|
42
|
-
@
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
66
|
+
end
|
52
67
|
|
53
|
-
|
68
|
+
# The max frame size negotiated between the client and the broker
|
69
|
+
# @return [Integer]
|
70
|
+
attr_reader :frame_max
|
54
71
|
|
55
|
-
|
56
|
-
|
57
|
-
|
72
|
+
# Custom inspect
|
73
|
+
# @return [String]
|
74
|
+
# @api private
|
75
|
+
def inspect
|
76
|
+
"#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
|
77
|
+
end
|
58
78
|
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
62
85
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
raise AMQP::Client::Error, "Max channels reached" if id.nil?
|
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
|
91
|
+
end
|
92
|
+
raise Error, "Max channels reached" if id.nil?
|
71
93
|
|
72
|
-
|
94
|
+
ch = @channels[id] = Channel.new(self, id)
|
95
|
+
end
|
96
|
+
ch.open
|
73
97
|
end
|
74
|
-
ch.open
|
75
|
-
end
|
76
98
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
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
|
108
|
+
end
|
84
109
|
end
|
85
|
-
end
|
86
110
|
|
87
|
-
|
88
|
-
|
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
|
89
117
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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)
|
122
|
+
nil
|
123
|
+
end
|
95
124
|
|
96
|
-
|
97
|
-
@
|
98
|
-
|
125
|
+
# True if the connection is closed
|
126
|
+
# @return [Boolean]
|
127
|
+
def closed?
|
128
|
+
@closed
|
129
|
+
end
|
99
130
|
|
100
|
-
|
101
|
-
|
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)
|
102
136
|
@write_lock.synchronize do
|
103
137
|
@socket.write(*bytes)
|
104
138
|
end
|
105
|
-
|
106
|
-
|
139
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
140
|
+
raise Error, "Could not write to socket, #{e.message}"
|
107
141
|
end
|
108
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
109
|
-
raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
|
110
|
-
end
|
111
142
|
|
112
|
-
|
113
|
-
|
114
|
-
#
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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}")
|
126
157
|
|
127
|
-
|
128
|
-
|
158
|
+
# read the frame content
|
159
|
+
socket.read(frame_size, frame_buffer)
|
129
160
|
|
130
|
-
|
131
|
-
|
132
|
-
|
161
|
+
# make sure that the frame end is correct
|
162
|
+
frame_end = socket.readchar.ord
|
163
|
+
raise UnexpectedFrameEnd, frame_end if frame_end != 206
|
133
164
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
138
|
-
warn "AMQP-Client read error: #{e.inspect}"
|
139
|
-
nil # ignore read errors
|
140
|
-
ensure
|
141
|
-
@closed = true
|
142
|
-
@replies.close
|
143
|
-
begin
|
144
|
-
@socket.close
|
145
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
165
|
+
# parse the frame, will return false if a close frame was received
|
166
|
+
parse_frame(type, channel_id, frame_buffer) || return
|
167
|
+
end
|
146
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
|
175
|
+
begin
|
176
|
+
@socket.close
|
177
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
178
|
+
nil
|
179
|
+
end
|
147
180
|
end
|
148
|
-
end
|
149
181
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
182
|
+
private
|
183
|
+
|
184
|
+
def parse_frame(type, channel_id, buf)
|
185
|
+
case type
|
186
|
+
when 1 # method frame
|
187
|
+
class_id, method_id = buf.unpack("S> S>")
|
188
|
+
case class_id
|
189
|
+
when 10 # connection
|
190
|
+
raise Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
191
|
+
|
192
|
+
case method_id
|
193
|
+
when 50 # connection#close
|
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
|
171
210
|
end
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
when
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
when
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
when 21 # commit-ok
|
302
|
-
@channels[channel_id].reply [:tx_commit_ok]
|
303
|
-
when 31 # rollback-ok
|
304
|
-
@channels[channel_id].reply [:tx_rollback_ok]
|
305
|
-
else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
|
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
|
256
|
+
end
|
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
|
306
340
|
end
|
307
|
-
|
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
|
308
348
|
end
|
309
|
-
|
310
|
-
body_size = buf.unpack1("@4 Q>")
|
311
|
-
properties = Properties.decode(buf.byteslice(12, buf.bytesize - 12))
|
312
|
-
@channels[channel_id].header_delivered body_size, properties
|
313
|
-
when 3 # body
|
314
|
-
@channels[channel_id].body_delivered buf
|
315
|
-
else raise AMQP::Client::UnsupportedFrameType, type
|
349
|
+
true
|
316
350
|
end
|
317
|
-
true
|
318
|
-
end
|
319
351
|
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
end
|
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
|
325
356
|
|
326
|
-
|
327
|
-
channel_max, frame_max, heartbeat = nil
|
328
|
-
socket.write "AMQP\x00\x00\x09\x01"
|
329
|
-
buf = String.new(capacity: 4096)
|
330
|
-
loop do
|
331
|
-
begin
|
332
|
-
socket.readpartial(4096, buf)
|
333
|
-
rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
|
334
|
-
raise AMQP::Client::Error, "Could not establish AMQP connection: #{e.message}"
|
357
|
+
raise(Error::ConnectionClosed, "while waiting for #{expected_frame_type}")
|
335
358
|
end
|
359
|
+
frame_type == expected_frame_type || raise(Error::UnexpectedFrame.new(expected_frame_type, frame_type))
|
360
|
+
args
|
361
|
+
end
|
336
362
|
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
340
375
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
case class_id
|
345
|
-
when 10 # connection
|
346
|
-
raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
|
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
|
347
379
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
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}"
|
368
410
|
end
|
369
|
-
else raise
|
411
|
+
else raise Error, "Unexpected frame type: #{type}"
|
370
412
|
end
|
371
|
-
else raise AMQP::Client::Error, "Unexpected frame type: #{type}"
|
372
413
|
end
|
414
|
+
rescue StandardError => e
|
415
|
+
begin
|
416
|
+
socket.close
|
417
|
+
rescue IOError, OpenSSL::OpenSSLError, SystemCallError
|
418
|
+
nil
|
419
|
+
end
|
420
|
+
raise e
|
373
421
|
end
|
374
|
-
rescue StandardError
|
375
|
-
socket.close rescue nil
|
376
|
-
raise
|
377
|
-
end
|
378
422
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
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
|
387
431
|
|
388
|
-
|
389
|
-
|
432
|
+
def self.port_from_env
|
433
|
+
return unless (port = ENV["AMQP_PORT"])
|
390
434
|
|
391
|
-
|
392
|
-
|
435
|
+
port.to_i
|
436
|
+
end
|
393
437
|
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
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
|
410
455
|
end
|
411
456
|
end
|