amqp-client 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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