amqp-client 0.1.0 → 0.2.0

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,91 @@
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
+ context = OpenSSL::SSL::SSLContext.new
29
+ context.verify_mode = OpenSSL::SSL::VERIFY_PEER unless [false, "false", "none"].include? options[:verify_peer]
30
+ socket = OpenSSL::SSL::SSLSocket.new(socket, context)
31
+ socket.sync_close = true # closing the TLS socket also closes the TCP socket
32
+ end
33
+ channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
34
+ Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
35
+ end
36
+
37
+ def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
7
38
  @socket = socket
8
39
  @channel_max = channel_max
9
40
  @frame_max = frame_max
10
41
  @heartbeat = heartbeat
11
42
  @channels = {}
12
43
  @closed = false
13
- @rpc = Queue.new
14
- Thread.new { read_loop }
44
+ @replies = Queue.new
45
+ Thread.new { read_loop } if read_loop_thread
15
46
  end
16
47
 
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
48
+ attr_reader :frame_max
49
+
50
+ def channel(id = nil)
51
+ if id
52
+ ch = @channels[id] ||= Channel.new(self, id)
53
+ else
54
+ id = 1.upto(@channel_max) { |i| break i unless @channels.key? i }
55
+ ch = @channels[id] = Channel.new(self, id)
56
+ end
21
57
  ch.open
22
58
  end
23
59
 
60
+ # Declare a new channel, yield, and then close the channel
61
+ def with_channel
62
+ ch = channel
63
+ begin
64
+ yield ch
65
+ ensure
66
+ ch.close
67
+ end
68
+ end
69
+
24
70
  def close(reason = "", code = 200)
71
+ return if @closed
72
+
25
73
  write_bytes FrameBytes.connection_close(code, reason)
26
74
  expect(:close_ok)
27
75
  @closed = true
28
76
  end
29
77
 
30
- def write_bytes(bytes)
31
- @socket.write bytes
78
+ def closed?
79
+ @closed
32
80
  end
33
81
 
34
- private
82
+ def write_bytes(*bytes)
83
+ @socket.write(*bytes)
84
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
85
+ raise AMQP::Client::Error.new("Could not write to socket", cause: e)
86
+ end
35
87
 
88
+ # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
36
89
  def read_loop
37
90
  socket = @socket
38
91
  frame_max = @frame_max
@@ -40,23 +93,31 @@ module AMQP
40
93
  loop do
41
94
  begin
42
95
  socket.readpartial(frame_max, buffer)
43
- rescue IOError, EOFError
96
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError
44
97
  break
45
98
  end
46
99
 
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
100
+ pos = 0
101
+ while pos < buffer.bytesize
102
+ buffer += socket.read(pos + 8 - buffer.bytesize) if pos + 8 > buffer.bytesize
103
+ type, channel_id, frame_size = buffer.byteslice(pos, 7).unpack("C S> L>")
104
+ if frame_size > frame_max
105
+ raise AMQP::Client::Error, "Frame size #{frame_size} larger than negotiated max frame size #{frame_max}"
106
+ end
107
+
108
+ frame_end_pos = pos + 7 + frame_size
109
+ buffer += socket.read(frame_end_pos - buffer.bytesize + 1) if frame_end_pos + 1 > buffer.bytesize
110
+ frame_end = buffer[frame_end_pos].ord
111
+ raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
52
112
 
53
- buf = buffer.byteslice(buf_pos, frame_size + 8)
54
- buf_pos += frame_size + 8
113
+ buf = buffer.byteslice(pos, frame_size + 8)
114
+ pos += frame_size + 8
55
115
  parse_frame(type, channel_id, frame_size, buf) || return
56
116
  end
57
117
  end
58
118
  ensure
59
119
  @closed = true
120
+ @replies.close
60
121
  begin
61
122
  @socket.close
62
123
  rescue IOError
@@ -64,6 +125,8 @@ module AMQP
64
125
  end
65
126
  end
66
127
 
128
+ private
129
+
67
130
  def parse_frame(type, channel_id, frame_size, buf)
68
131
  case type
69
132
  when 1 # method frame
@@ -75,67 +138,255 @@ module AMQP
75
138
  case method_id
76
139
  when 50 # connection#close
77
140
  code, text_len = buf.unpack("@11 S> C")
78
- text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
141
+ text = buf.byteslice(14, text_len).force_encoding("utf-8")
142
+ error_class_id, error_method_id = buf.byteslice(14 + text_len, 4).unpack("S> S>")
79
143
  warn "Connection closed #{code} #{text} #{error_class_id} #{error_method_id}"
80
144
  write_bytes FrameBytes.connection_close_ok
81
145
  return false
82
146
  when 51 # connection#close-ok
83
- @rpc.push [:close_ok]
147
+ @replies.push [:close_ok]
84
148
  return false
85
149
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
86
150
  end
87
151
  when 20 # channel
88
152
  case method_id
89
153
  when 11 # channel#open-ok
90
- @channels[channel_id].push [:channel_open_ok]
154
+ @channels[channel_id].reply [:channel_open_ok]
91
155
  when 40 # channel#close
156
+ reply_code, reply_text_len = buf.unpack("@11 S> C")
157
+ reply_text = buf.byteslice(14, reply_text_len).force_encoding("utf-8")
158
+ classid, methodid = buf.byteslice(14 + reply_text_len, 4).unpack("S> S>")
92
159
  channel = @channels.delete(channel_id)
93
- channel&.closed!
160
+ channel.closed!(reply_code, reply_text, classid, methodid)
94
161
  when 41 # channel#close-ok
95
- @channels[channel_id].push [:channel_close_ok]
162
+ @channels[channel_id].reply [:channel_close_ok]
163
+ else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
164
+ end
165
+ when 40 # exchange
166
+ case method_id
167
+ when 11 # declare-ok
168
+ @channels[channel_id].reply [:exchange_declare_ok]
169
+ when 21 # delete-ok
170
+ @channels[channel_id].reply [:exchange_delete_ok]
171
+ when 31 # bind-ok
172
+ @channels[channel_id].reply [:exchange_bind_ok]
173
+ when 51 # unbind-ok
174
+ @channels[channel_id].reply [:exchange_unbind_ok]
96
175
  else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
97
176
  end
98
177
  when 50 # queue
99
178
  case method_id
100
- when 11 # queue#declare-ok
179
+ when 11 # declare-ok
101
180
  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
181
+ queue_name = buf.byteslice(12, queue_name_len).force_encoding("utf-8")
182
+ message_count, consumer_count = buf.byteslice(12 + queue_name_len, 8).unpack1("L> L>")
183
+ @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
184
+ when 21 # bind-ok
185
+ @channels[channel_id].reply [:queue_bind_ok]
186
+ when 31 # purge-ok
187
+ @channels[channel_id].reply [:queue_purge_ok]
188
+ when 41 # delete-ok
189
+ message_count = buf.unpack1("@11 L>")
190
+ @channels[channel_id].reply [:queue_delete, message_count]
191
+ when 51 # unbind-ok
192
+ @channels[channel_id].reply [:queue_unbind_ok]
193
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
105
194
  end
106
195
  when 60 # basic
107
196
  case method_id
197
+ when 11 # qos-ok
198
+ @channels[channel_id].reply [:basic_qos_ok]
199
+ when 21 # consume-ok
200
+ tag_len = buf.unpack1("@11 C")
201
+ tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
202
+ @channels[channel_id].reply [:basic_consume_ok, tag]
203
+ when 30 # cancel
204
+ tag_len = buf.unpack1("@11 C")
205
+ tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
206
+ no_wait = buf[12 + tag_len].ord
207
+ @channels[channel_id].consumers.fetch(tag).close
208
+ write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
209
+ when 31 # cancel-ok
210
+ tag_len = buf.unpack1("@11 C")
211
+ tag = buf.byteslice(12, tag_len).force_encoding("utf-8")
212
+ @channels[channel_id].reply [:basic_cancel_ok, tag]
213
+ when 50 # return
214
+ reply_code, reply_text_len = buf.unpack("@11 S> C")
215
+ pos = 14
216
+ reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
217
+ pos += reply_text_len
218
+ exchange_len = buf[pos].ord
219
+ pos += 1
220
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
221
+ pos += exchange_len
222
+ routing_key_len = buf[pos].ord
223
+ pos += 1
224
+ routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
225
+ @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
226
+ when 60 # deliver
227
+ ctag_len = buf[11].ord
228
+ consumer_tag = buf.byteslice(12, ctag_len).force_encoding("utf-8")
229
+ pos = 12 + ctag_len
230
+ delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
231
+ pos += 8 + 1 + 1
232
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
233
+ pos += exchange_len
234
+ rk_len = buf[pos].ord
235
+ pos += 1
236
+ routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
237
+ loop do
238
+ if (consumer = @channels[channel_id].consumers[consumer_tag])
239
+ consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
240
+ break
241
+ else
242
+ Thread.pass
243
+ end
244
+ end
108
245
  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")
246
+ delivery_tag, redelivered, exchange_len = buf.unpack("@11 Q> C C")
247
+ pos = 21
248
+ exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
249
+ pos += exchange_len
250
+ routing_key_len = buf[pos].ord
113
251
  pos += 1
114
- routing_key = buf.byteslice(pos, routing_key_len)
252
+ routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
115
253
  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]
254
+ message_count = buf.byteslice(pos, 4).unpack1("L>")
255
+ redelivered = redelivered == 1
256
+ @channels[channel_id].reply [:basic_get_ok, delivery_tag, exchange, routing_key, message_count, redelivered]
118
257
  when 72 # get-empty
119
- @channels[channel_id].push [:basic_get_empty]
120
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
258
+ @channels[channel_id].reply [:basic_get_empty]
259
+ when 80 # ack
260
+ delivery_tag, multiple = buf.unpack("@11 Q> C")
261
+ @channels[channel_id].confirm [:ack, delivery_tag, multiple]
262
+ when 90 # reject
263
+ delivery_tag, requeue = buf.unpack("@11 Q> C")
264
+ @channels[channel_id].confirm [:reject, delivery_tag, requeue == 1]
265
+ when 111 # recover-ok
266
+ @channels[channel_id].reply [:basic_recover_ok]
267
+ when 120 # nack
268
+ delivery_tag, multiple, requeue = buf.unpack("@11 Q> C C")
269
+ @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
270
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
271
+ end
272
+ when 85 # confirm
273
+ case method_id
274
+ when 11 # select-ok
275
+ @channels[channel_id].reply [:confirm_select_ok]
276
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
277
+ end
278
+ when 90 # tx
279
+ case method_id
280
+ when 11 # select-ok
281
+ @channels[channel_id].reply [:tx_select_ok]
282
+ when 21 # commit-ok
283
+ @channels[channel_id].reply [:tx_commit_ok]
284
+ when 31 # rollback-ok
285
+ @channels[channel_id].reply [:tx_rollback_ok]
286
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
121
287
  end
122
- else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
288
+ else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
123
289
  end
124
290
  when 2 # header
125
291
  body_size = buf.unpack1("@11 Q>")
126
- @channels[channel_id].push [:header, body_size, nil]
292
+ properties = Properties.decode(buf.byteslice(19, buf.bytesize - 20))
293
+ @channels[channel_id].reply [:header, body_size, properties]
127
294
  when 3 # body
128
295
  body = buf.byteslice(7, frame_size)
129
- @channels[channel_id].push [:body, body]
296
+ @channels[channel_id].reply [:body, body]
130
297
  else raise AMQP::Client::UnsupportedFrameType, type
131
298
  end
132
299
  true
133
300
  end
134
301
 
135
302
  def expect(expected_frame_type)
136
- frame_type, args = @rpc.shift
137
- frame_type == expected_frame_type || raise(UnexpectedFrame, expected_frame_type, frame_type)
303
+ frame_type, args = @replies.shift
304
+ frame_type == expected_frame_type || raise(UnexpectedFrame.new(expected_frame_type, frame_type))
138
305
  args
139
306
  end
307
+
308
+ def self.establish(socket, user, password, vhost, **options)
309
+ channel_max, frame_max, heartbeat = nil
310
+ socket.write "AMQP\x00\x00\x09\x01"
311
+ buf = String.new(capacity: 4096)
312
+ loop do
313
+ begin
314
+ socket.readpartial(4096, buf)
315
+ rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
316
+ raise AMQP::Client::Error, "Could not establish AMQP connection: #{e.message}"
317
+ end
318
+
319
+ type, channel_id, frame_size = buf.unpack("C S> L>")
320
+ frame_end = buf.unpack1("@#{frame_size + 7} C")
321
+ raise UnexpectedFrameEndError, frame_end if frame_end != 206
322
+
323
+ case type
324
+ when 1 # method frame
325
+ class_id, method_id = buf.unpack("@7 S> S>")
326
+ case class_id
327
+ when 10 # connection
328
+ raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
329
+
330
+ case method_id
331
+ when 10 # connection#start
332
+ properties = CLIENT_PROPERTIES.merge({ connection_name: options[:connection_name] })
333
+ socket.write FrameBytes.connection_start_ok "\u0000#{user}\u0000#{password}", properties
334
+ when 30 # connection#tune
335
+ channel_max, frame_max, heartbeat = buf.unpack("@11 S> L> S>")
336
+ channel_max = [channel_max, 2048].min
337
+ frame_max = [frame_max, 131_072].min
338
+ heartbeat = [heartbeat, 0].min
339
+ socket.write FrameBytes.connection_tune_ok(channel_max, frame_max, heartbeat)
340
+ socket.write FrameBytes.connection_open(vhost)
341
+ when 41 # connection#open-ok
342
+ return [channel_max, frame_max, heartbeat]
343
+ when 50 # connection#close
344
+ code, text_len = buf.unpack("@11 S> C")
345
+ text, error_class_id, error_method_id = buf.unpack("@14 a#{text_len} S> S>")
346
+ socket.write FrameBytes.connection_close_ok
347
+ raise AMQP::Client::Error, "Could not establish AMQP connection: #{code} #{text} #{error_class_id} #{error_method_id}"
348
+ else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
349
+ end
350
+ else raise AMQP::Client::Error, "Unexpected class/method: #{class_id} #{method_id}"
351
+ end
352
+ else raise AMQP::Client::Error, "Unexpected frame type: #{type}"
353
+ end
354
+ end
355
+ rescue StandardError
356
+ socket.close rescue nil
357
+ raise
358
+ end
359
+
360
+ def self.enable_tcp_keepalive(socket)
361
+ socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
362
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
363
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
364
+ socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
365
+ rescue StandardError => e
366
+ warn "amqp-client: Could not enable TCP keepalive on socket. #{e.inspect}"
367
+ end
368
+
369
+ def self.port_from_env
370
+ return unless (port = ENV["AMQP_PORT"])
371
+
372
+ port.to_i
373
+ end
374
+
375
+ private_class_method :establish, :enable_tcp_keepalive, :port_from_env
376
+
377
+ CLIENT_PROPERTIES = {
378
+ capabilities: {
379
+ authentication_failure_close: true,
380
+ publisher_confirms: true,
381
+ consumer_cancel_notify: true,
382
+ exchange_exchange_bindings: true,
383
+ "basic.nack": true,
384
+ "connection.blocked": true
385
+ },
386
+ product: "amqp-client.rb",
387
+ platform: RUBY_DESCRIPTION,
388
+ version: AMQP::Client::VERSION,
389
+ information: "http://github.com/cloudamqp/amqp-client.rb"
390
+ }.freeze
140
391
  end
141
392
  end