amqp-client 0.2.3 → 1.0.2

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