amqp-client 0.1.0 → 0.2.0

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,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