amqp-client 0.1.0 → 0.2.3

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