amqp-client 0.1.0 → 0.2.3

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.
@@ -6,17 +6,25 @@ module AMQP
6
6
  # AMQP Channel
7
7
  class Channel
8
8
  def initialize(connection, id)
9
- @rpc = Queue.new
9
+ @replies = ::Queue.new
10
10
  @connection = connection
11
11
  @id = id
12
- @closed = false
12
+ @consumers = {}
13
+ @confirm = nil
14
+ @last_confirmed = 0
15
+ @closed = nil
16
+ @on_return = nil
17
+ @open = false
13
18
  end
14
19
 
15
- attr_reader :id
20
+ attr_reader :id, :consumers
16
21
 
17
22
  def open
23
+ return self if @open
24
+
18
25
  write_bytes FrameBytes.channel_open(@id)
19
26
  expect(:channel_open_ok)
27
+ @open = true
20
28
  self
21
29
  end
22
30
 
@@ -25,15 +33,44 @@ module AMQP
25
33
 
26
34
  write_bytes FrameBytes.channel_close(@id, reason, code)
27
35
  expect :channel_close_ok
28
- @closed = true
36
+ @closed = [code, reason]
37
+ end
38
+
39
+ # Called when closed by server
40
+ def closed!(code, reason, classid, methodid)
41
+ write_bytes FrameBytes.channel_close_ok(@id)
42
+ @closed = [code, reason, classid, methodid]
43
+ @replies.close
44
+ @consumers.each { |_, q| q.close }
45
+ @consumers.clear
46
+ end
47
+
48
+ def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, **args)
49
+ write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, args)
50
+ expect :exchange_declare_ok
29
51
  end
30
52
 
31
- def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, **args)
53
+ def exchange_delete(name, if_unused: false, no_wait: false)
54
+ write_bytes FrameBytes.exchange_delete(@id, name, if_unused, no_wait)
55
+ expect :exchange_delete_ok
56
+ end
57
+
58
+ def exchange_bind(destination, source, binding_key, arguments = {})
59
+ write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
60
+ expect :exchange_bind_ok
61
+ end
62
+
63
+ def exchange_unbind(destination, source, binding_key, arguments = {})
64
+ write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
65
+ expect :exchange_unbind_ok
66
+ end
67
+
68
+ def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
32
69
  durable = false if name.empty?
33
70
  exclusive = true if name.empty?
34
71
  auto_delete = true if name.empty?
35
72
 
36
- write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete)
73
+ write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
37
74
  name, message_count, consumer_count = expect(:queue_declare_ok)
38
75
  {
39
76
  queue_name: name,
@@ -42,60 +79,230 @@ module AMQP
42
79
  }
43
80
  end
44
81
 
45
- def basic_get(queue_name, no_ack: true)
46
- return if @closed
82
+ def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
83
+ write_bytes FrameBytes.queue_delete(@id, name, if_unused, if_empty, no_wait)
84
+ message_count, = expect :queue_delete
85
+ message_count
86
+ end
87
+
88
+ def queue_bind(name, exchange, binding_key, arguments = {})
89
+ write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
90
+ expect :queue_bind_ok
91
+ end
92
+
93
+ def queue_purge(name, no_wait: false)
94
+ write_bytes FrameBytes.queue_purge(@id, name, no_wait)
95
+ expect :queue_purge_ok unless no_wait
96
+ end
97
+
98
+ def queue_unbind(name, exchange, binding_key, arguments = {})
99
+ write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
100
+ expect :queue_unbind_ok
101
+ end
47
102
 
103
+ def basic_get(queue_name, no_ack: true)
48
104
  write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
49
- resp = @rpc.shift
50
- frame, = resp
105
+ frame, *rest = @replies.shift
51
106
  case frame
52
107
  when :basic_get_ok
53
- _, exchange_name, routing_key, redelivered = resp
108
+ delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
54
109
  body_size, properties = expect(:header)
55
110
  pos = 0
56
- body = ""
111
+ body = String.new("", capacity: body_size)
57
112
  while pos < body_size
58
- body_part = expect(:body)
113
+ body_part, = expect(:body)
59
114
  body += body_part
60
115
  pos += body_part.bytesize
61
116
  end
62
- Message.new(exchange_name, routing_key, properties, body, redelivered)
63
- when :basic_get_empty
64
- nil
65
- else raise AMQP::Client::UnexpectedFrame, %i[basic_get_ok basic_get_empty], frame
117
+ Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
118
+ when :basic_get_empty then nil
119
+ when nil then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
120
+ else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
66
121
  end
67
122
  end
68
123
 
69
- def basic_publish(exchange, routing_key, body, properties = {})
70
- raise AMQP::Client::ChannelClosedError, @id if @closed
124
+ def basic_publish(body, exchange, routing_key, **properties)
125
+ frame_max = @connection.frame_max - 8
126
+ id = @id
71
127
 
72
- write_bytes FrameBytes.basic_publish(@id, exchange, routing_key)
73
- write_bytes FrameBytes.header(@id, body.bytesize, properties)
128
+ if 0 < body.bytesize && body.bytesize <= frame_max
129
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
130
+ FrameBytes.header(id, body.bytesize, properties),
131
+ FrameBytes.body(id, body)
132
+ return @confirm ? @confirm += 1 : nil
133
+ end
74
134
 
75
- # body frames, splitted on frame size
135
+ write_bytes FrameBytes.basic_publish(id, exchange, routing_key, properties.delete(:mandatory) || false),
136
+ FrameBytes.header(id, body.bytesize, properties)
76
137
  pos = 0
77
- while pos < body.bytesize
78
- len = [4096, body.bytesize - pos].min
138
+ while pos < body.bytesize # split body into multiple frame_max frames
139
+ len = [frame_max, body.bytesize - pos].min
79
140
  body_part = body.byteslice(pos, len)
80
- write_bytes FrameBytes.body(@id, body_part)
141
+ write_bytes FrameBytes.body(id, body_part)
81
142
  pos += len
82
143
  end
144
+ @confirm += 1 if @confirm
145
+ end
146
+
147
+ def basic_publish_confirm(body, exchange, routing_key, **properties)
148
+ confirm_select(no_wait: true)
149
+ id = basic_publish(body, exchange, routing_key, **properties)
150
+ wait_for_confirm(id)
151
+ end
152
+
153
+ # Consume from a queue
154
+ # worker_threads: 0 => blocking, messages are executed in the thread calling this method
155
+ def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
156
+ worker_threads: 1)
157
+ write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
158
+ tag, = expect(:basic_consume_ok)
159
+ q = @consumers[tag] = ::Queue.new
160
+ msgs = ::Queue.new
161
+ Thread.new { recv_deliveries(tag, q, msgs) }
162
+ if worker_threads.zero?
163
+ while (msg = msgs.shift)
164
+ yield msg
165
+ end
166
+ else
167
+ threads = Array.new(worker_threads) do
168
+ Thread.new do
169
+ while (msg = msgs.shift)
170
+ yield(msg)
171
+ end
172
+ end
173
+ end
174
+ [tag, threads]
175
+ end
176
+ end
177
+
178
+ def basic_cancel(consumer_tag, no_wait: false)
179
+ consumer = @consumers.fetch(consumer_tag)
180
+ return if consumer.closed?
181
+
182
+ write_bytes FrameBytes.basic_cancel(@id, consumer_tag)
183
+ expect(:basic_cancel_ok) unless no_wait
184
+ consumer.close
185
+ end
186
+
187
+ def basic_qos(prefetch_count, prefetch_size: 0, global: false)
188
+ write_bytes FrameBytes.basic_qos(@id, prefetch_size, prefetch_count, global)
189
+ expect :basic_qos_ok
190
+ end
191
+
192
+ def basic_ack(delivery_tag, multiple: false)
193
+ write_bytes FrameBytes.basic_ack(@id, delivery_tag, multiple)
194
+ end
195
+
196
+ def basic_nack(delivery_tag, multiple: false, requeue: false)
197
+ write_bytes FrameBytes.basic_nack(@id, delivery_tag, multiple, requeue)
198
+ end
199
+
200
+ def basic_reject(delivery_tag, requeue: false)
201
+ write_bytes FrameBytes.basic_reject(@id, delivery_tag, requeue)
202
+ end
203
+
204
+ def basic_recover(requeue: false)
205
+ write_bytes FrameBytes.basic_recover(@id, requeue: requeue)
206
+ expect :basic_recover_ok
207
+ end
208
+
209
+ def confirm_select(no_wait: false)
210
+ return if @confirm
211
+
212
+ write_bytes FrameBytes.confirm_select(@id, no_wait)
213
+ expect :confirm_select_ok unless no_wait
214
+ @confirms = ::Queue.new
215
+ @confirm = 0
216
+ end
217
+
218
+ def wait_for_confirm(id)
219
+ raise ArgumentError, "Confirm id has to a positive number" unless id&.positive?
220
+ return true if @last_confirmed >= id
221
+
222
+ loop do
223
+ ack, delivery_tag, multiple = @confirms.shift || break
224
+ @last_confirmed = delivery_tag
225
+ return ack if delivery_tag == id || (delivery_tag > id && multiple)
226
+ end
227
+ false
228
+ end
229
+
230
+ def tx_select
231
+ write_bytes FrameBytes.tx_select(@id)
232
+ expect :tx_select_ok
233
+ end
234
+
235
+ def tx_commit
236
+ write_bytes FrameBytes.tx_commit(@id)
237
+ expect :tx_commit_ok
238
+ end
239
+
240
+ def tx_rollback
241
+ write_bytes FrameBytes.tx_rollback(@id)
242
+ expect :tx_rollback_ok
243
+ end
244
+
245
+ def reply(args)
246
+ @replies.push(args)
247
+ end
248
+
249
+ def confirm(args)
250
+ @confirms.push(args)
251
+ end
252
+
253
+ def message_returned(reply_code, reply_text, exchange, routing_key)
254
+ Thread.new do
255
+ body_size, properties = expect(:header)
256
+ body = String.new("", capacity: body_size)
257
+ while body.bytesize < body_size
258
+ body_part, = expect(:body)
259
+ body += body_part
260
+ end
261
+ msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
262
+
263
+ if @on_return
264
+ @on_return.call(msg)
265
+ else
266
+ puts "[WARN] Message returned: #{msg.inspect}"
267
+ end
268
+ end
83
269
  end
84
270
 
85
- def push(*args)
86
- @rpc.push(*args)
271
+ def on_return(&block)
272
+ @on_return = block
87
273
  end
88
274
 
89
275
  private
90
276
 
91
- def write_bytes(bytes)
92
- @connection.write_bytes bytes
277
+ def recv_deliveries(consumer_tag, deliver_queue, msgs)
278
+ loop do
279
+ _, delivery_tag, redelivered, exchange, routing_key = deliver_queue.shift || raise(ClosedQueueError)
280
+ body_size, properties = expect(:header)
281
+ body = String.new("", capacity: body_size)
282
+ while body.bytesize < body_size
283
+ body_part, = expect(:body)
284
+ body += body_part
285
+ end
286
+ msgs.push Message.new(self, delivery_tag, exchange, routing_key, properties, body, redelivered, consumer_tag)
287
+ end
288
+ ensure
289
+ msgs.close
290
+ end
291
+
292
+ def write_bytes(*bytes)
293
+ raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if @closed
294
+
295
+ @connection.write_bytes(*bytes)
93
296
  end
94
297
 
95
298
  def expect(expected_frame_type)
96
- frame_type, args = @rpc.shift
97
- frame_type == expected_frame_type || raise(UnexpectedFrame, expected_frame_type, frame_type)
98
- args
299
+ loop do
300
+ frame_type, *args = @replies.shift
301
+ raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
302
+ return args if frame_type == expected_frame_type
303
+
304
+ @replies.push [frame_type, *args]
305
+ end
99
306
  end
100
307
  end
101
308
  end
@@ -1,38 +1,96 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "socket"
4
+ require "uri"
5
+ require "openssl"
6
+ require_relative "./frames"
7
+ require_relative "./channel"
8
+ require_relative "./errors"
9
+
3
10
  module AMQP
4
- # AMQP Connection
11
+ # Represents a single AMQP connection
5
12
  class Connection
6
- def initialize(socket, channel_max, frame_max, heartbeat)
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)
7
43
  @socket = socket
8
44
  @channel_max = channel_max
9
45
  @frame_max = frame_max
10
46
  @heartbeat = heartbeat
11
47
  @channels = {}
12
48
  @closed = false
13
- @rpc = Queue.new
14
- Thread.new { read_loop }
49
+ @replies = Queue.new
50
+ Thread.new { read_loop } if read_loop_thread
15
51
  end
16
52
 
17
- def channel
18
- id = 1.upto(@channel_max) { |i| break i unless @channels.key? i }
19
- ch = Channel.new(self, id)
20
- @channels[id] = ch
53
+ attr_reader :frame_max
54
+
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)
61
+ end
21
62
  ch.open
22
63
  end
23
64
 
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
72
+ end
73
+ end
74
+
24
75
  def close(reason = "", code = 200)
76
+ return if @closed
77
+
25
78
  write_bytes FrameBytes.connection_close(code, reason)
26
79
  expect(:close_ok)
27
80
  @closed = true
28
81
  end
29
82
 
30
- def write_bytes(bytes)
31
- @socket.write bytes
83
+ def closed?
84
+ @closed
32
85
  end
33
86
 
34
- private
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
35
92
 
93
+ # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
36
94
  def read_loop
37
95
  socket = @socket
38
96
  frame_max = @frame_max
@@ -40,23 +98,31 @@ module AMQP
40
98
  loop do
41
99
  begin
42
100
  socket.readpartial(frame_max, buffer)
43
- rescue IOError, EOFError
101
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError
44
102
  break
45
103
  end
46
104
 
47
- buf_pos = 0
48
- while buf_pos < buffer.bytesize
49
- type, channel_id, frame_size = buffer.unpack("@#{buf_pos}C S> L>")
50
- frame_end = buffer.unpack1("@#{buf_pos + 7 + frame_size} C")
51
- raise AMQP::Client::UnexpectedFrameEnd if frame_end != 206
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
112
+
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
52
117
 
53
- buf = buffer.byteslice(buf_pos, frame_size + 8)
54
- buf_pos += frame_size + 8
118
+ buf = buffer.byteslice(pos, frame_size + 8)
119
+ pos += frame_size + 8
55
120
  parse_frame(type, channel_id, frame_size, buf) || return
56
121
  end
57
122
  end
58
123
  ensure
59
124
  @closed = true
125
+ @replies.close
60
126
  begin
61
127
  @socket.close
62
128
  rescue IOError
@@ -64,6 +130,8 @@ module AMQP
64
130
  end
65
131
  end
66
132
 
133
+ private
134
+
67
135
  def parse_frame(type, channel_id, frame_size, buf)
68
136
  case type
69
137
  when 1 # method frame
@@ -75,67 +143,255 @@ module AMQP
75
143
  case method_id
76
144
  when 50 # connection#close
77
145
  code, text_len = buf.unpack("@11 S> C")
78
- text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
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>")
79
148
  warn "Connection closed #{code} #{text} #{error_class_id} #{error_method_id}"
80
149
  write_bytes FrameBytes.connection_close_ok
81
150
  return false
82
151
  when 51 # connection#close-ok
83
- @rpc.push [:close_ok]
152
+ @replies.push [:close_ok]
84
153
  return false
85
154
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
86
155
  end
87
156
  when 20 # channel
88
157
  case method_id
89
158
  when 11 # channel#open-ok
90
- @channels[channel_id].push [:channel_open_ok]
159
+ @channels[channel_id].reply [:channel_open_ok]
91
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>")
92
164
  channel = @channels.delete(channel_id)
93
- channel&.closed!
165
+ channel.closed!(reply_code, reply_text, classid, methodid)
94
166
  when 41 # channel#close-ok
95
- @channels[channel_id].push [: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]
96
180
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
97
181
  end
98
182
  when 50 # queue
99
183
  case method_id
100
- when 11 # queue#declare-ok
184
+ when 11 # declare-ok
101
185
  queue_name_len = buf.unpack1("@11 C")
102
- queue_name, message_count, consumer_count = buf.unpack("@12 a#{queue_name_len} L> L>")
103
- @channels[channel_id].push [:queue_declare_ok, queue_name, message_count, consumer_count]
104
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
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
105
199
  end
106
200
  when 60 # basic
107
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
108
250
  when 71 # get-ok
109
- delivery_tag, redelivered, exchange_name_len = buf.unpack("@11 Q> C C")
110
- exchange_name = buf.byteslice(21, exchange_name_len)
111
- pos = 21 + exchange_name_len
112
- routing_key_len = buf.unpack1("@#{pos} C")
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
113
256
  pos += 1
114
- routing_key = buf.byteslice(pos, routing_key_len)
257
+ routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
115
258
  pos += routing_key_len
116
- message_count = buf.unpack1("@#{pos} L>")
117
- @channels[channel_id].push [:basic_get_ok, delivery_tag, exchange_name, routing_key, message_count, redelivered == 1]
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]
118
262
  when 72 # get-empty
119
- @channels[channel_id].push [:basic_get_empty]
120
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
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
121
292
  end
122
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
293
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
123
294
  end
124
295
  when 2 # header
125
296
  body_size = buf.unpack1("@11 Q>")
126
- @channels[channel_id].push [:header, body_size, nil]
297
+ properties = Properties.decode(buf.byteslice(19, buf.bytesize - 20))
298
+ @channels[channel_id].reply [:header, body_size, properties]
127
299
  when 3 # body
128
300
  body = buf.byteslice(7, frame_size)
129
- @channels[channel_id].push [:body, body]
301
+ @channels[channel_id].reply [:body, body]
130
302
  else raise AMQP::Client::UnsupportedFrameType, type
131
303
  end
132
304
  true
133
305
  end
134
306
 
135
307
  def expect(expected_frame_type)
136
- frame_type, args = @rpc.shift
137
- frame_type == expected_frame_type || raise(UnexpectedFrame, expected_frame_type, frame_type)
308
+ frame_type, args = @replies.shift
309
+ frame_type == expected_frame_type || raise(UnexpectedFrame.new(expected_frame_type, frame_type))
138
310
  args
139
311
  end
312
+
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
318
+ 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}"
322
+ end
323
+
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
327
+
328
+ case type
329
+ when 1 # method frame
330
+ class_id, method_id = buf.unpack("@7 S> S>")
331
+ case class_id
332
+ when 10 # connection
333
+ raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
334
+
335
+ 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
+ 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}"
354
+ end
355
+ else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
356
+ end
357
+ else raise AMQP::Client::Error, "Unexpected frame type: #{type}"
358
+ end
359
+ end
360
+ rescue StandardError
361
+ socket.close rescue nil
362
+ raise
363
+ end
364
+
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
373
+
374
+ def self.port_from_env
375
+ return unless (port = ENV["AMQP_PORT"])
376
+
377
+ port.to_i
378
+ end
379
+
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
140
396
  end
141
397
  end