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.
@@ -8,404 +8,449 @@ 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, read_loop_thread: true, **options)
14
- uri = URI.parse(uri)
15
- tls = uri.scheme == "amqps"
16
- port = port_from_env || uri.port || (tls ? 5671 : 5672)
17
- host = uri.host || "localhost"
18
- user = uri.user || "guest"
19
- password = uri.password || "guest"
20
- vhost = URI.decode_www_form_component(uri.path[1..-1] || "/")
21
- options = URI.decode_www_form(uri.query || "").map! { |k, v| [k.to_sym, v] }.to_h.merge(options)
22
-
23
- socket = Socket.tcp host, port, connect_timeout: 20, resolv_timeout: 5
24
- enable_tcp_keepalive(socket)
25
- if tls
26
- cert_store = OpenSSL::X509::Store.new
27
- cert_store.set_default_paths
28
- context = OpenSSL::SSL::SSLContext.new
29
- context.cert_store = cert_store
30
- context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless [false, "false", "none"].include? options[:verify_peer]
31
- socket = OpenSSL::SSL::SSLSocket.new(socket, context)
32
- socket.sync_close = true # closing the TLS socket also closes the TCP socket
33
- socket.hostname = host # SNI host
34
- socket.connect
35
- socket.post_connection_check(host) || raise(AMQP::Client::Error, "TLS certificate hostname doesn't match requested")
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
- def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
42
- @socket = socket
43
- @channel_max = channel_max.zero? ? 65_536 : channel_max
44
- @frame_max = frame_max
45
- @heartbeat = heartbeat
46
- @channels = {}
47
- @closed = false
48
- @replies = Queue.new
49
- @write_lock = Mutex.new
50
- Thread.new { read_loop } if read_loop_thread
51
- end
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
- attr_reader :frame_max
68
+ # The max frame size negotiated between the client and the broker
69
+ # @return [Integer]
70
+ attr_reader :frame_max
54
71
 
55
- def inspect
56
- "#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
57
- end
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
- def channel(id = nil)
60
- raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
61
- raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
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
- if id
64
- ch = @channels[id] ||= Channel.new(self, id)
65
- else
66
- id = nil
67
- 1.upto(@channel_max) do |i|
68
- break id = i unless @channels.key? i
69
- end
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
- ch = @channels[id] = Channel.new(self, id)
94
+ ch = @channels[id] = Channel.new(self, id)
95
+ end
96
+ ch.open
73
97
  end
74
- ch.open
75
- end
76
98
 
77
- # Declare a new channel, yield, and then close the channel
78
- def with_channel
79
- ch = channel
80
- begin
81
- yield ch
82
- ensure
83
- ch.close
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
- def close(reason = "", code = 200)
88
- return if @closed
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
- @closed = true
91
- write_bytes FrameBytes.connection_close(code, reason)
92
- @channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
93
- expect(:close_ok)
94
- end
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
- def closed?
97
- @closed
98
- end
125
+ # True if the connection is closed
126
+ # @return [Boolean]
127
+ def closed?
128
+ @closed
129
+ end
99
130
 
100
- def write_bytes(*bytes)
101
- if @socket.is_a? OpenSSL::SSL::SSLSocket
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
- else
106
- @socket.write(*bytes)
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
- # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
113
- def read_loop
114
- # read more often than write so that channel errors crop up early
115
- Thread.current.priority += 1
116
- socket = @socket
117
- frame_max = @frame_max
118
- frame_start = String.new(capacity: 7)
119
- frame_buffer = String.new(capacity: frame_max)
120
- loop do
121
- socket.read(7, frame_start)
122
- type, channel_id, frame_size = frame_start.unpack("C S> L>")
123
- if frame_size > frame_max
124
- raise AMQP::Client::Error, "Frame size #{frame_size} is larger than negotiated max frame size #{frame_max}"
125
- end
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
- # read the frame content
128
- socket.read(frame_size, frame_buffer)
158
+ # read the frame content
159
+ socket.read(frame_size, frame_buffer)
129
160
 
130
- # make sure that the frame end is correct
131
- frame_end = socket.readchar.ord
132
- raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
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
- # parse the frame, will return false if a close frame was received
135
- parse_frame(type, channel_id, frame_size, frame_buffer) || return
136
- end
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
- private
151
-
152
- def parse_frame(type, channel_id, frame_size, buf)
153
- case type
154
- when 1 # method frame
155
- class_id, method_id = buf.unpack("S> S>")
156
- case class_id
157
- when 10 # connection
158
- raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
159
-
160
- case method_id
161
- when 50 # connection#close
162
- @closed = true
163
- code, text_len = buf.unpack("@4 S> C")
164
- text = buf.byteslice(7, text_len).force_encoding("utf-8")
165
- error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
166
- @channels.each_value { |ch| ch.closed!(code, text, error_class_id, error_method_id) }
167
- begin
168
- write_bytes FrameBytes.connection_close_ok
169
- rescue AMQP::Client::Error
170
- nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
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
- return false
173
- when 51 # connection#close-ok
174
- @closed = true
175
- @replies.push [:close_ok]
176
- return false
177
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
178
- end
179
- when 20 # channel
180
- case method_id
181
- when 11 # channel#open-ok
182
- @channels[channel_id].reply [:channel_open_ok]
183
- when 40 # channel#close
184
- reply_code, reply_text_len = buf.unpack("@4 S> C")
185
- reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
186
- classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
187
- channel = @channels.delete(channel_id)
188
- channel.closed!(reply_code, reply_text, classid, methodid)
189
- write_bytes FrameBytes.channel_close_ok(channel_id)
190
- when 41 # channel#close-ok
191
- channel = @channels.delete(channel_id)
192
- channel.reply [:channel_close_ok]
193
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
194
- end
195
- when 40 # exchange
196
- case method_id
197
- when 11 # declare-ok
198
- @channels[channel_id].reply [:exchange_declare_ok]
199
- when 21 # delete-ok
200
- @channels[channel_id].reply [:exchange_delete_ok]
201
- when 31 # bind-ok
202
- @channels[channel_id].reply [:exchange_bind_ok]
203
- when 51 # unbind-ok
204
- @channels[channel_id].reply [:exchange_unbind_ok]
205
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
206
- end
207
- when 50 # queue
208
- case method_id
209
- when 11 # declare-ok
210
- queue_name_len = buf.unpack1("@4 C")
211
- queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
212
- message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
213
- @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
214
- when 21 # bind-ok
215
- @channels[channel_id].reply [:queue_bind_ok]
216
- when 31 # purge-ok
217
- @channels[channel_id].reply [:queue_purge_ok]
218
- when 41 # delete-ok
219
- message_count = buf.unpack1("@4 L>")
220
- @channels[channel_id].reply [:queue_delete, message_count]
221
- when 51 # unbind-ok
222
- @channels[channel_id].reply [:queue_unbind_ok]
223
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
224
- end
225
- when 60 # basic
226
- case method_id
227
- when 11 # qos-ok
228
- @channels[channel_id].reply [:basic_qos_ok]
229
- when 21 # consume-ok
230
- tag_len = buf.unpack1("@4 C")
231
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
232
- @channels[channel_id].reply [:basic_consume_ok, tag]
233
- when 30 # cancel
234
- tag_len = buf.unpack1("@4 C")
235
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
236
- no_wait = buf[5 + tag_len].ord
237
- @channels[channel_id].close_consumer(tag)
238
- write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
239
- when 31 # cancel-ok
240
- tag_len = buf.unpack1("@4 C")
241
- tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
242
- @channels[channel_id].reply [:basic_cancel_ok, tag]
243
- when 50 # return
244
- reply_code, reply_text_len = buf.unpack("@4 S> C")
245
- pos = 7
246
- reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
247
- pos += reply_text_len
248
- exchange_len = buf[pos].ord
249
- pos += 1
250
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
251
- pos += exchange_len
252
- routing_key_len = buf[pos].ord
253
- pos += 1
254
- routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
255
- @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
256
- when 60 # deliver
257
- ctag_len = buf[4].ord
258
- consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
259
- pos = 5 + ctag_len
260
- delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
261
- pos += 8 + 1 + 1
262
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
263
- pos += exchange_len
264
- rk_len = buf[pos].ord
265
- pos += 1
266
- routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
267
- @channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
268
- when 71 # get-ok
269
- delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
270
- pos = 14
271
- exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
272
- pos += exchange_len
273
- routing_key_len = buf[pos].ord
274
- pos += 1
275
- routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
276
- pos += routing_key_len
277
- _message_count = buf.byteslice(pos, 4).unpack1("L>")
278
- @channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
279
- when 72 # get-empty
280
- @channels[channel_id].basic_get_empty
281
- when 80 # ack
282
- delivery_tag, multiple = buf.unpack("@4 Q> C")
283
- @channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
284
- when 111 # recover-ok
285
- @channels[channel_id].reply [:basic_recover_ok]
286
- when 120 # nack
287
- delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
288
- @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
289
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
290
- end
291
- when 85 # confirm
292
- case method_id
293
- when 11 # select-ok
294
- @channels[channel_id].reply [:confirm_select_ok]
295
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
296
- end
297
- when 90 # tx
298
- case method_id
299
- when 11 # select-ok
300
- @channels[channel_id].reply [:tx_select_ok]
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
- else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
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
- when 2 # header
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
- def expect(expected_frame_type)
321
- frame_type, args = @replies.pop
322
- frame_type == expected_frame_type || raise(AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type))
323
- args
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
- def self.establish(socket, user, password, vhost, **options)
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
- type, channel_id, frame_size = buf.unpack("C S> L>")
338
- frame_end = buf[frame_size + 7].ord
339
- raise UnexpectedFrameEndError, frame_end if frame_end != 206
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
- case type
342
- when 1 # method frame
343
- class_id, method_id = buf.unpack("@7 S> S>")
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
- case method_id
349
- when 10 # connection#start
350
- conn_name = options[:connection_name] || $PROGRAM_NAME
351
- properties = CLIENT_PROPERTIES.merge({ connection_name: conn_name })
352
- socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
353
- when 30 # connection#tune
354
- channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
355
- channel_max = [channel_max, 2048].min
356
- frame_max = [frame_max, 131_072].min
357
- heartbeat = [heartbeat, 0].min
358
- socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
359
- socket.write FrameBytes.connection_open(vhost)
360
- when 41 # connection#open-ok
361
- return [channel_max, frame_max, heartbeat]
362
- when 50 # connection#close
363
- code, text_len = buf.unpack("@11 S> C")
364
- text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
365
- socket.write FrameBytes.connection_close_ok
366
- raise AMQP::Client::Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
367
- else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
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 AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
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
- def self.enable_tcp_keepalive(socket)
380
- socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
381
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
382
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
383
- socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
384
- rescue StandardError => e
385
- warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
386
- end
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
- def self.port_from_env
389
- return unless (port = ENV["AMQP_PORT"])
432
+ def self.port_from_env
433
+ return unless (port = ENV["AMQP_PORT"])
390
434
 
391
- port.to_i
392
- end
435
+ port.to_i
436
+ end
393
437
 
394
- private_class_method :establish, :enable_tcp_keepalive, :port_from_env
395
-
396
- CLIENT_PROPERTIES = {
397
- capabilities: {
398
- authentication_failure_close: true,
399
- publisher_confirms: true,
400
- consumer_cancel_notify: true,
401
- exchange_exchange_bindings: true,
402
- "basic.nack": true,
403
- "connection.blocked": true
404
- },
405
- product: "amqp-client.rb",
406
- platform: RUBY_DESCRIPTION,
407
- version: AMQP::Client::VERSION,
408
- information: "http://github.com/cloudamqp/amqp-client.rb"
409
- }.freeze
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