amqp-client 0.3.0 → 1.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/README.md +4 -4
- data/lib/amqp/client/channel.rb +94 -74
- data/lib/amqp/client/connection.rb +95 -80
- data/lib/amqp/client/message.rb +1 -1
- data/lib/amqp/client/properties.rb +4 -3
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +97 -21
- metadata +3 -3
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 5e6dd3fef130c286e97eadd9bb6d3d4de0fa67599361604cec3560879b9b4bdf
         | 
| 4 | 
            +
              data.tar.gz: c2ce00f897c68d3085427854b08cc98d3487b40bb5327ce38bb216c98357e366
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 9b62e8ee74ec542be5617b08fbd0e36e5fc16294f99cd0ec61781c7c7425005c426e8358a74abf45a1a24ecb88451b94ef50ee7524403ac7097748b2e94b55b5
         | 
| 7 | 
            +
              data.tar.gz: 5c3e63369578923e20b441fbe9728b374e18150c75ed155291b802c42f80f668b704b39e80b144b90346a8d769d9b71f031b9fca712e6ac52c100a88e46380e3
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,26 @@ | |
| 1 1 | 
             
            ## [Unreleased]
         | 
| 2 2 |  | 
| 3 | 
            +
            ## [1.0.0] - 2021-08-27
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            - Verify TLS certificate matches hostname
         | 
| 6 | 
            +
            - TLS thread-safety
         | 
| 7 | 
            +
            - Assemble Messages in the (single threaded) read_loop thread
         | 
| 8 | 
            +
            - Give read_loop_thread higher priority so that channel errors crop up faster
         | 
| 9 | 
            +
            - One less Thread required per Consumer
         | 
| 10 | 
            +
            - Read exactly one frame at a time, not trying to split/assemble frames over socket reads
         | 
| 11 | 
            +
            - Heafty speedup for message assembling with StringIO
         | 
| 12 | 
            +
            - Channel#queue_declare returns a struct for nicer API (still backward compatible)
         | 
| 13 | 
            +
            - AMQP::Client#publish_and_forget for fast, non confirmed publishes
         | 
| 14 | 
            +
            - Allow Properties#timestamp to be an integer (in addition to Time)
         | 
| 15 | 
            +
            - Bug fix allow Properties#expiration to be an Integer
         | 
| 16 | 
            +
            - Consistent use of named parameters
         | 
| 17 | 
            +
            - High level Exchange API
         | 
| 18 | 
            +
            - Don't try to reconnect if first connect fails
         | 
| 19 | 
            +
            - Bug fix: Close all channels when connection is closed by server
         | 
| 20 | 
            +
            - Raise error if run out of channels
         | 
| 21 | 
            +
            - Improved retry in high level client
         | 
| 22 | 
            +
            - Bug fix: Support channel_max 0
         | 
| 23 | 
            +
             | 
| 3 24 | 
             
            ## [0.3.0] - 2021-08-20
         | 
| 4 25 |  | 
| 5 26 | 
             
            - Channel#wait_for_confirms is a smarter way of waiting for publish confirms
         | 
    
        data/README.md
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            # AMQP::Client
         | 
| 2 2 |  | 
| 3 | 
            -
            An AMQP 0-9-1 client | 
| 3 | 
            +
            An AMQP 0-9-1 Ruby client, trying to keep things as simple as possible.
         | 
| 4 4 |  | 
| 5 5 | 
             
            ## Installation
         | 
| 6 6 |  | 
| @@ -34,7 +34,7 @@ msg = ch.basic_get q[:queue_name] | |
| 34 34 | 
             
            puts msg.body
         | 
| 35 35 | 
             
            ```
         | 
| 36 36 |  | 
| 37 | 
            -
            High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to  | 
| 37 | 
            +
            High level API, is an easier and safer API, that only deal with durable queues and persisted messages. All methods are blocking in the case of connection loss etc. It's also fully thread-safe. Don't expect it to have extreme throughput, but expect 100% delivery guarantees (messages might be delivered twice, in the unlikely event of connection loss between message publish and message confirmation by the server).
         | 
| 38 38 |  | 
| 39 39 | 
             
            ```ruby
         | 
| 40 40 | 
             
            amqp = AMQP::Client.new("amqp://localhost")
         | 
| @@ -46,8 +46,8 @@ q = amqp.queue("myqueue") | |
| 46 46 | 
             
            # Bind the queue to any exchange, with any binding key
         | 
| 47 47 | 
             
            q.bind("amq.topic", "my.events.*")
         | 
| 48 48 |  | 
| 49 | 
            -
            # The message will be reprocessed if the client  | 
| 50 | 
            -
            # between  | 
| 49 | 
            +
            # The message will be reprocessed if the client loses connection to the server
         | 
| 50 | 
            +
            # between message arrival and when the message was supposed to be ack'ed.
         | 
| 51 51 | 
             
            q.subscribe(prefetch: 20) do |msg|
         | 
| 52 52 | 
             
              process(JSON.parse(msg.body))
         | 
| 53 53 | 
             
              msg.ack
         | 
    
        data/lib/amqp/client/channel.rb
    CHANGED
    
    | @@ -16,16 +16,22 @@ module AMQP | |
| 16 16 | 
             
                  @confirm = nil
         | 
| 17 17 | 
             
                  @unconfirmed = ::Queue.new
         | 
| 18 18 | 
             
                  @unconfirmed_empty = ::Queue.new
         | 
| 19 | 
            +
                  @basic_gets = ::Queue.new
         | 
| 19 20 | 
             
                end
         | 
| 20 21 |  | 
| 21 | 
            -
                 | 
| 22 | 
            +
                def inspect
         | 
| 23 | 
            +
                  "#<#{self.class} @id=#{@id} @open=#{@open} @closed=#{@closed} confirm_selected=#{!@confirm.nil?}"\
         | 
| 24 | 
            +
                    " consumer_count=#{@consumers.size} replies_count=#{@replies.size} unconfirmed_count=#{@unconfirmed.size}>"
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                attr_reader :id
         | 
| 22 28 |  | 
| 23 29 | 
             
                def open
         | 
| 24 30 | 
             
                  return self if @open
         | 
| 25 31 |  | 
| 32 | 
            +
                  @open = true
         | 
| 26 33 | 
             
                  write_bytes FrameBytes.channel_open(@id)
         | 
| 27 34 | 
             
                  expect(:channel_open_ok)
         | 
| 28 | 
            -
                  @open = true
         | 
| 29 35 | 
             
                  self
         | 
| 30 36 | 
             
                end
         | 
| 31 37 |  | 
| @@ -33,21 +39,25 @@ module AMQP | |
| 33 39 | 
             
                  return if @closed
         | 
| 34 40 |  | 
| 35 41 | 
             
                  write_bytes FrameBytes.channel_close(@id, reason, code)
         | 
| 36 | 
            -
                  expect :channel_close_ok
         | 
| 37 42 | 
             
                  @closed = [code, reason]
         | 
| 43 | 
            +
                  expect :channel_close_ok
         | 
| 44 | 
            +
                  @replies.close
         | 
| 45 | 
            +
                  @basic_gets.close
         | 
| 46 | 
            +
                  @unconfirmed_empty.close
         | 
| 47 | 
            +
                  @consumers.each_value(&:close)
         | 
| 38 48 | 
             
                end
         | 
| 39 49 |  | 
| 40 | 
            -
                # Called when closed by server
         | 
| 50 | 
            +
                # Called when channel is closed by server
         | 
| 41 51 | 
             
                def closed!(code, reason, classid, methodid)
         | 
| 42 | 
            -
                  write_bytes FrameBytes.channel_close_ok(@id)
         | 
| 43 52 | 
             
                  @closed = [code, reason, classid, methodid]
         | 
| 44 53 | 
             
                  @replies.close
         | 
| 45 | 
            -
                  @ | 
| 46 | 
            -
                  @ | 
| 54 | 
            +
                  @basic_gets.close
         | 
| 55 | 
            +
                  @unconfirmed_empty.close
         | 
| 56 | 
            +
                  @consumers.each_value(&:close)
         | 
| 47 57 | 
             
                end
         | 
| 48 58 |  | 
| 49 | 
            -
                def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false,  | 
| 50 | 
            -
                  write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal,  | 
| 59 | 
            +
                def exchange_declare(name, type, passive: false, durable: true, auto_delete: false, internal: false, arguments: {})
         | 
| 60 | 
            +
                  write_bytes FrameBytes.exchange_declare(@id, name, type, passive, durable, auto_delete, internal, arguments)
         | 
| 51 61 | 
             
                  expect :exchange_declare_ok
         | 
| 52 62 | 
             
                end
         | 
| 53 63 |  | 
| @@ -56,16 +66,18 @@ module AMQP | |
| 56 66 | 
             
                  expect :exchange_delete_ok
         | 
| 57 67 | 
             
                end
         | 
| 58 68 |  | 
| 59 | 
            -
                def exchange_bind(destination, source, binding_key, arguments  | 
| 69 | 
            +
                def exchange_bind(destination, source, binding_key, arguments: {})
         | 
| 60 70 | 
             
                  write_bytes FrameBytes.exchange_bind(@id, destination, source, binding_key, false, arguments)
         | 
| 61 71 | 
             
                  expect :exchange_bind_ok
         | 
| 62 72 | 
             
                end
         | 
| 63 73 |  | 
| 64 | 
            -
                def exchange_unbind(destination, source, binding_key, arguments  | 
| 74 | 
            +
                def exchange_unbind(destination, source, binding_key, arguments: {})
         | 
| 65 75 | 
             
                  write_bytes FrameBytes.exchange_unbind(@id, destination, source, binding_key, false, arguments)
         | 
| 66 76 | 
             
                  expect :exchange_unbind_ok
         | 
| 67 77 | 
             
                end
         | 
| 68 78 |  | 
| 79 | 
            +
                QueueOk = Struct.new(:queue_name, :message_count, :consumer_count)
         | 
| 80 | 
            +
             | 
| 69 81 | 
             
                def queue_declare(name = "", passive: false, durable: true, exclusive: false, auto_delete: false, arguments: {})
         | 
| 70 82 | 
             
                  durable = false if name.empty?
         | 
| 71 83 | 
             
                  exclusive = true if name.empty?
         | 
| @@ -73,11 +85,8 @@ module AMQP | |
| 73 85 |  | 
| 74 86 | 
             
                  write_bytes FrameBytes.queue_declare(@id, name, passive, durable, exclusive, auto_delete, arguments)
         | 
| 75 87 | 
             
                  name, message_count, consumer_count = expect(:queue_declare_ok)
         | 
| 76 | 
            -
             | 
| 77 | 
            -
             | 
| 78 | 
            -
                    message_count: message_count,
         | 
| 79 | 
            -
                    consumer_count: consumer_count
         | 
| 80 | 
            -
                  }
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                  QueueOk.new(name, message_count, consumer_count)
         | 
| 81 90 | 
             
                end
         | 
| 82 91 |  | 
| 83 92 | 
             
                def queue_delete(name, if_unused: false, if_empty: false, no_wait: false)
         | 
| @@ -86,7 +95,7 @@ module AMQP | |
| 86 95 | 
             
                  message_count
         | 
| 87 96 | 
             
                end
         | 
| 88 97 |  | 
| 89 | 
            -
                def queue_bind(name, exchange, binding_key, arguments  | 
| 98 | 
            +
                def queue_bind(name, exchange, binding_key, arguments: {})
         | 
| 90 99 | 
             
                  write_bytes FrameBytes.queue_bind(@id, name, exchange, binding_key, false, arguments)
         | 
| 91 100 | 
             
                  expect :queue_bind_ok
         | 
| 92 101 | 
             
                end
         | 
| @@ -96,29 +105,17 @@ module AMQP | |
| 96 105 | 
             
                  expect :queue_purge_ok unless no_wait
         | 
| 97 106 | 
             
                end
         | 
| 98 107 |  | 
| 99 | 
            -
                def queue_unbind(name, exchange, binding_key, arguments  | 
| 108 | 
            +
                def queue_unbind(name, exchange, binding_key, arguments: {})
         | 
| 100 109 | 
             
                  write_bytes FrameBytes.queue_unbind(@id, name, exchange, binding_key, arguments)
         | 
| 101 110 | 
             
                  expect :queue_unbind_ok
         | 
| 102 111 | 
             
                end
         | 
| 103 112 |  | 
| 104 113 | 
             
                def basic_get(queue_name, no_ack: true)
         | 
| 105 114 | 
             
                  write_bytes FrameBytes.basic_get(@id, queue_name, no_ack)
         | 
| 106 | 
            -
                   | 
| 107 | 
            -
                   | 
| 108 | 
            -
                  when :basic_get_ok
         | 
| 109 | 
            -
                    delivery_tag, exchange_name, routing_key, _message_count, redelivered = rest
         | 
| 110 | 
            -
                    body_size, properties = expect(:header)
         | 
| 111 | 
            -
                    pos = 0
         | 
| 112 | 
            -
                    body = String.new("", capacity: body_size)
         | 
| 113 | 
            -
                    while pos < body_size
         | 
| 114 | 
            -
                      body_part, = expect(:body)
         | 
| 115 | 
            -
                      body += body_part
         | 
| 116 | 
            -
                      pos += body_part.bytesize
         | 
| 117 | 
            -
                    end
         | 
| 118 | 
            -
                    Message.new(self, delivery_tag, exchange_name, routing_key, properties, body, redelivered)
         | 
| 115 | 
            +
                  case (msg = @basic_gets.pop)
         | 
| 116 | 
            +
                  when Message then msg
         | 
| 119 117 | 
             
                  when :basic_get_empty then nil
         | 
| 120 118 | 
             
                  when nil              then raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
         | 
| 121 | 
            -
                  else raise AMQP::Client::UnexpectedFrame.new(%i[basic_get_ok basic_get_empty], frame)
         | 
| 122 119 | 
             
                  end
         | 
| 123 120 | 
             
                end
         | 
| 124 121 |  | 
| @@ -126,8 +123,12 @@ module AMQP | |
| 126 123 | 
             
                  frame_max = @connection.frame_max - 8
         | 
| 127 124 | 
             
                  id = @id
         | 
| 128 125 | 
             
                  mandatory = properties.delete(:mandatory) || false
         | 
| 126 | 
            +
                  case properties.delete(:persistent)
         | 
| 127 | 
            +
                  when true then properties[:delivery_mode] = 2
         | 
| 128 | 
            +
                  when false then properties[:delivery_mode] = 1
         | 
| 129 | 
            +
                  end
         | 
| 129 130 |  | 
| 130 | 
            -
                  if  | 
| 131 | 
            +
                  if body.bytesize.between?(1, frame_max)
         | 
| 131 132 | 
             
                    write_bytes FrameBytes.basic_publish(id, exchange, routing_key, mandatory),
         | 
| 132 133 | 
             
                                FrameBytes.header(id, body.bytesize, properties),
         | 
| 133 134 | 
             
                                FrameBytes.body(id, body)
         | 
| @@ -156,22 +157,19 @@ module AMQP | |
| 156 157 |  | 
| 157 158 | 
             
                # Consume from a queue
         | 
| 158 159 | 
             
                # worker_threads: 0 => blocking, messages are executed in the thread calling this method
         | 
| 159 | 
            -
                def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {},
         | 
| 160 | 
            -
                                  worker_threads: 1)
         | 
| 160 | 
            +
                def basic_consume(queue, tag: "", no_ack: true, exclusive: false, arguments: {}, worker_threads: 1)
         | 
| 161 161 | 
             
                  write_bytes FrameBytes.basic_consume(@id, queue, tag, no_ack, exclusive, arguments)
         | 
| 162 162 | 
             
                  tag, = expect(:basic_consume_ok)
         | 
| 163 163 | 
             
                  q = @consumers[tag] = ::Queue.new
         | 
| 164 | 
            -
                  msgs = ::Queue.new
         | 
| 165 | 
            -
                  Thread.new { recv_deliveries(tag, q, msgs) }
         | 
| 166 164 | 
             
                  if worker_threads.zero?
         | 
| 167 | 
            -
                     | 
| 168 | 
            -
                      yield  | 
| 165 | 
            +
                    loop do
         | 
| 166 | 
            +
                      yield (q.pop || break)
         | 
| 169 167 | 
             
                    end
         | 
| 170 168 | 
             
                  else
         | 
| 171 169 | 
             
                    threads = Array.new(worker_threads) do
         | 
| 172 170 | 
             
                      Thread.new do
         | 
| 173 | 
            -
                         | 
| 174 | 
            -
                          yield( | 
| 171 | 
            +
                        loop do
         | 
| 172 | 
            +
                          yield (q.pop || break)
         | 
| 175 173 | 
             
                        end
         | 
| 176 174 | 
             
                      end
         | 
| 177 175 | 
             
                    end
         | 
| @@ -222,9 +220,14 @@ module AMQP | |
| 222 220 | 
             
                def wait_for_confirms
         | 
| 223 221 | 
             
                  return true if @unconfirmed.empty?
         | 
| 224 222 |  | 
| 225 | 
            -
                  @unconfirmed_empty.pop
         | 
| 223 | 
            +
                  case @unconfirmed_empty.pop
         | 
| 224 | 
            +
                  when true then true
         | 
| 225 | 
            +
                  when false then false
         | 
| 226 | 
            +
                  else raise AMQP::Client::ChannelClosedError.new(@id, *@closed)
         | 
| 227 | 
            +
                  end
         | 
| 226 228 | 
             
                end
         | 
| 227 229 |  | 
| 230 | 
            +
                # Called by Connection when received ack/nack from server
         | 
| 228 231 | 
             
                def confirm(args)
         | 
| 229 232 | 
             
                  ack_or_nack, delivery_tag, multiple = *args
         | 
| 230 233 | 
             
                  loop do
         | 
| @@ -239,7 +242,7 @@ module AMQP | |
| 239 242 | 
             
                  return unless @unconfirmed.empty?
         | 
| 240 243 |  | 
| 241 244 | 
             
                  @unconfirmed_empty.num_waiting.times do
         | 
| 242 | 
            -
                    @unconfirmed_empty << ack_or_nack == :ack
         | 
| 245 | 
            +
                    @unconfirmed_empty << (ack_or_nack == :ack)
         | 
| 243 246 | 
             
                  end
         | 
| 244 247 | 
             
                end
         | 
| 245 248 |  | 
| @@ -258,47 +261,66 @@ module AMQP | |
| 258 261 | 
             
                  expect :tx_rollback_ok
         | 
| 259 262 | 
             
                end
         | 
| 260 263 |  | 
| 264 | 
            +
                def on_return(&block)
         | 
| 265 | 
            +
                  @on_return = block
         | 
| 266 | 
            +
                end
         | 
| 267 | 
            +
             | 
| 261 268 | 
             
                def reply(args)
         | 
| 262 269 | 
             
                  @replies.push(args)
         | 
| 263 270 | 
             
                end
         | 
| 264 271 |  | 
| 265 272 | 
             
                def message_returned(reply_code, reply_text, exchange, routing_key)
         | 
| 266 | 
            -
                   | 
| 267 | 
            -
             | 
| 268 | 
            -
                    body = String.new("", capacity: body_size)
         | 
| 269 | 
            -
                    while body.bytesize < body_size
         | 
| 270 | 
            -
                      body_part, = expect(:body)
         | 
| 271 | 
            -
                      body += body_part
         | 
| 272 | 
            -
                    end
         | 
| 273 | 
            -
                    msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, properties, body)
         | 
| 273 | 
            +
                  @next_msg = ReturnMessage.new(reply_code, reply_text, exchange, routing_key, nil, "")
         | 
| 274 | 
            +
                end
         | 
| 274 275 |  | 
| 275 | 
            -
             | 
| 276 | 
            -
             | 
| 277 | 
            -
             | 
| 278 | 
            -
             | 
| 279 | 
            -
             | 
| 276 | 
            +
                def message_delivered(consumer_tag, delivery_tag, redelivered, exchange, routing_key)
         | 
| 277 | 
            +
                  @next_msg = Message.new(self, delivery_tag, exchange, routing_key, nil, "", redelivered, consumer_tag)
         | 
| 278 | 
            +
                end
         | 
| 279 | 
            +
             | 
| 280 | 
            +
                def basic_get_empty
         | 
| 281 | 
            +
                  @basic_gets.push :basic_get_empty
         | 
| 282 | 
            +
                end
         | 
| 283 | 
            +
             | 
| 284 | 
            +
                def header_delivered(body_size, properties)
         | 
| 285 | 
            +
                  @next_msg.properties = properties
         | 
| 286 | 
            +
                  if body_size.zero?
         | 
| 287 | 
            +
                    next_message_finished!
         | 
| 288 | 
            +
                  else
         | 
| 289 | 
            +
                    @next_body = StringIO.new(String.new(capacity: body_size))
         | 
| 290 | 
            +
                    @next_body_size = body_size
         | 
| 280 291 | 
             
                  end
         | 
| 281 292 | 
             
                end
         | 
| 282 293 |  | 
| 283 | 
            -
                def  | 
| 284 | 
            -
                  @ | 
| 294 | 
            +
                def body_delivered(body_part)
         | 
| 295 | 
            +
                  @next_body.write(body_part)
         | 
| 296 | 
            +
                  return unless @next_body.pos == @next_body_size
         | 
| 297 | 
            +
             | 
| 298 | 
            +
                  @next_msg.body = @next_body.string
         | 
| 299 | 
            +
                  next_message_finished!
         | 
| 300 | 
            +
                end
         | 
| 301 | 
            +
             | 
| 302 | 
            +
                def close_consumer(tag)
         | 
| 303 | 
            +
                  @consumers.fetch(tag).close
         | 
| 285 304 | 
             
                end
         | 
| 286 305 |  | 
| 287 306 | 
             
                private
         | 
| 288 307 |  | 
| 289 | 
            -
                def  | 
| 290 | 
            -
                   | 
| 291 | 
            -
             | 
| 292 | 
            -
                     | 
| 293 | 
            -
             | 
| 294 | 
            -
                     | 
| 295 | 
            -
                       | 
| 296 | 
            -
                      body += body_part
         | 
| 308 | 
            +
                def next_message_finished!
         | 
| 309 | 
            +
                  next_msg = @next_msg
         | 
| 310 | 
            +
                  if next_msg.is_a? ReturnMessage
         | 
| 311 | 
            +
                    if @on_return
         | 
| 312 | 
            +
                      Thread.new { @on_return.call(next_msg) }
         | 
| 313 | 
            +
                    else
         | 
| 314 | 
            +
                      warn "AMQP-Client message returned: #{msg.inspect}"
         | 
| 297 315 | 
             
                    end
         | 
| 298 | 
            -
             | 
| 316 | 
            +
                  elsif next_msg.consumer_tag.nil?
         | 
| 317 | 
            +
                    @basic_gets.push next_msg
         | 
| 318 | 
            +
                  else
         | 
| 319 | 
            +
                    Thread.pass until (consumer = @consumers[next_msg.consumer_tag])
         | 
| 320 | 
            +
                    consumer.push next_msg
         | 
| 299 321 | 
             
                  end
         | 
| 300 322 | 
             
                ensure
         | 
| 301 | 
            -
                   | 
| 323 | 
            +
                  @next_msg = @next_body = @next_body_size = nil
         | 
| 302 324 | 
             
                end
         | 
| 303 325 |  | 
| 304 326 | 
             
                def write_bytes(*bytes)
         | 
| @@ -308,13 +330,11 @@ module AMQP | |
| 308 330 | 
             
                end
         | 
| 309 331 |  | 
| 310 332 | 
             
                def expect(expected_frame_type)
         | 
| 311 | 
            -
                   | 
| 312 | 
            -
             | 
| 313 | 
            -
             | 
| 314 | 
            -
                    return args if frame_type == expected_frame_type
         | 
| 333 | 
            +
                  frame_type, *args = @replies.pop
         | 
| 334 | 
            +
                  raise AMQP::Client::ChannelClosedError.new(@id, *@closed) if frame_type.nil?
         | 
| 335 | 
            +
                  raise AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type) unless frame_type == expected_frame_type
         | 
| 315 336 |  | 
| 316 | 
            -
             | 
| 317 | 
            -
                  end
         | 
| 337 | 
            +
                  args
         | 
| 318 338 | 
             
                end
         | 
| 319 339 | 
             
              end
         | 
| 320 340 | 
             
            end
         | 
| @@ -10,9 +10,7 @@ require_relative "./errors" | |
| 10 10 | 
             
            module AMQP
         | 
| 11 11 | 
             
              # Represents a single AMQP connection
         | 
| 12 12 | 
             
              class Connection
         | 
| 13 | 
            -
                def self.connect(uri, **options)
         | 
| 14 | 
            -
                  read_loop_thread = options[:read_loop_thread] || true
         | 
| 15 | 
            -
             | 
| 13 | 
            +
                def self.connect(uri, read_loop_thread: true, **options)
         | 
| 16 14 | 
             
                  uri = URI.parse(uri)
         | 
| 17 15 | 
             
                  tls = uri.scheme == "amqps"
         | 
| 18 16 | 
             
                  port = port_from_env || uri.port || (tls ? 5671 : 5672)
         | 
| @@ -34,6 +32,7 @@ module AMQP | |
| 34 32 | 
             
                    socket.sync_close = true # closing the TLS socket also closes the TCP socket
         | 
| 35 33 | 
             
                    socket.hostname = host # SNI host
         | 
| 36 34 | 
             
                    socket.connect
         | 
| 35 | 
            +
                    socket.post_connection_check(host) || raise(AMQP::Client::Error, "TLS certificate hostname doesn't match requested")
         | 
| 37 36 | 
             
                  end
         | 
| 38 37 | 
             
                  channel_max, frame_max, heartbeat = establish(socket, user, password, vhost, **options)
         | 
| 39 38 | 
             
                  Connection.new(socket, channel_max, frame_max, heartbeat, read_loop_thread: read_loop_thread)
         | 
| @@ -41,22 +40,35 @@ module AMQP | |
| 41 40 |  | 
| 42 41 | 
             
                def initialize(socket, channel_max, frame_max, heartbeat, read_loop_thread: true)
         | 
| 43 42 | 
             
                  @socket = socket
         | 
| 44 | 
            -
                  @channel_max = channel_max
         | 
| 43 | 
            +
                  @channel_max = channel_max.zero? ? 65_536 : channel_max
         | 
| 45 44 | 
             
                  @frame_max = frame_max
         | 
| 46 45 | 
             
                  @heartbeat = heartbeat
         | 
| 47 46 | 
             
                  @channels = {}
         | 
| 48 47 | 
             
                  @closed = false
         | 
| 49 48 | 
             
                  @replies = Queue.new
         | 
| 49 | 
            +
                  @write_lock = Mutex.new
         | 
| 50 50 | 
             
                  Thread.new { read_loop } if read_loop_thread
         | 
| 51 51 | 
             
                end
         | 
| 52 52 |  | 
| 53 53 | 
             
                attr_reader :frame_max
         | 
| 54 54 |  | 
| 55 | 
            +
                def inspect
         | 
| 56 | 
            +
                  "#<#{self.class} @closed=#{@closed} channel_count=#{@channels.size}>"
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 55 59 | 
             
                def channel(id = nil)
         | 
| 60 | 
            +
                  raise ArgumentError, "Channel ID cannot be 0" if id&.zero?
         | 
| 61 | 
            +
                  raise ArgumentError, "Channel ID higher than connection's channel max #{@channel_max}" if id && id > @channel_max
         | 
| 62 | 
            +
             | 
| 56 63 | 
             
                  if id
         | 
| 57 64 | 
             
                    ch = @channels[id] ||= Channel.new(self, id)
         | 
| 58 65 | 
             
                  else
         | 
| 59 | 
            -
                    id =  | 
| 66 | 
            +
                    id = nil
         | 
| 67 | 
            +
                    1.upto(@channel_max) do |i|
         | 
| 68 | 
            +
                      break id = i unless @channels.key? i
         | 
| 69 | 
            +
                    end
         | 
| 70 | 
            +
                    raise AMQP::Client::Error, "Max channels reached" if id.nil?
         | 
| 71 | 
            +
             | 
| 60 72 | 
             
                    ch = @channels[id] = Channel.new(self, id)
         | 
| 61 73 | 
             
                  end
         | 
| 62 74 | 
             
                  ch.open
         | 
| @@ -75,9 +87,10 @@ module AMQP | |
| 75 87 | 
             
                def close(reason = "", code = 200)
         | 
| 76 88 | 
             
                  return if @closed
         | 
| 77 89 |  | 
| 90 | 
            +
                  @closed = true
         | 
| 78 91 | 
             
                  write_bytes FrameBytes.connection_close(code, reason)
         | 
| 92 | 
            +
                  @channels.each_value { |ch| ch.closed!(code, reason, 0, 0) }
         | 
| 79 93 | 
             
                  expect(:close_ok)
         | 
| 80 | 
            -
                  @closed = true
         | 
| 81 94 | 
             
                end
         | 
| 82 95 |  | 
| 83 96 | 
             
                def closed?
         | 
| @@ -85,47 +98,51 @@ module AMQP | |
| 85 98 | 
             
                end
         | 
| 86 99 |  | 
| 87 100 | 
             
                def write_bytes(*bytes)
         | 
| 88 | 
            -
                  @socket. | 
| 101 | 
            +
                  if @socket.is_a? OpenSSL::SSL::SSLSocket
         | 
| 102 | 
            +
                    @write_lock.synchronize do
         | 
| 103 | 
            +
                      @socket.write(*bytes)
         | 
| 104 | 
            +
                    end
         | 
| 105 | 
            +
                  else
         | 
| 106 | 
            +
                    @socket.write(*bytes)
         | 
| 107 | 
            +
                  end
         | 
| 89 108 | 
             
                rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
         | 
| 90 | 
            -
                  raise AMQP::Client::Error | 
| 109 | 
            +
                  raise AMQP::Client::Error, "Could not write to socket, #{e.message}"
         | 
| 91 110 | 
             
                end
         | 
| 92 111 |  | 
| 93 112 | 
             
                # Reads from the socket, required for any kind of progress. Blocks until the connection is closed
         | 
| 94 113 | 
             
                def read_loop
         | 
| 114 | 
            +
                  # read more often than write so that channel errors crop up early
         | 
| 115 | 
            +
                  Thread.current.priority += 1
         | 
| 95 116 | 
             
                  socket = @socket
         | 
| 96 117 | 
             
                  frame_max = @frame_max
         | 
| 97 | 
            -
                   | 
| 118 | 
            +
                  frame_start = String.new(capacity: 7)
         | 
| 119 | 
            +
                  frame_buffer = String.new(capacity: frame_max)
         | 
| 98 120 | 
             
                  loop do
         | 
| 99 | 
            -
                     | 
| 100 | 
            -
             | 
| 101 | 
            -
                     | 
| 102 | 
            -
                       | 
| 121 | 
            +
                    socket.read(7, frame_start)
         | 
| 122 | 
            +
                    type, channel_id, frame_size = frame_start.unpack("C S> L>")
         | 
| 123 | 
            +
                    if frame_size > frame_max
         | 
| 124 | 
            +
                      raise AMQP::Client::Error, "Frame size #{frame_size} is larger than negotiated max frame size #{frame_max}"
         | 
| 103 125 | 
             
                    end
         | 
| 104 126 |  | 
| 105 | 
            -
                     | 
| 106 | 
            -
                     | 
| 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
         | 
| 127 | 
            +
                    # read the frame content
         | 
| 128 | 
            +
                    socket.read(frame_size, frame_buffer)
         | 
| 112 129 |  | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
                      raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
         | 
| 130 | 
            +
                    # make sure that the frame end is correct
         | 
| 131 | 
            +
                    frame_end = socket.readchar.ord
         | 
| 132 | 
            +
                    raise AMQP::Client::UnexpectedFrameEnd, frame_end if frame_end != 206
         | 
| 117 133 |  | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 120 | 
            -
                      parse_frame(type, channel_id, frame_size, buf) || return
         | 
| 121 | 
            -
                    end
         | 
| 134 | 
            +
                    # parse the frame, will return false if a close frame was received
         | 
| 135 | 
            +
                    parse_frame(type, channel_id, frame_size, frame_buffer) || return
         | 
| 122 136 | 
             
                  end
         | 
| 137 | 
            +
                rescue IOError, OpenSSL::OpenSSLError, SystemCallError => e
         | 
| 138 | 
            +
                  warn "AMQP-Client read error: #{e.inspect}"
         | 
| 139 | 
            +
                  nil # ignore read errors
         | 
| 123 140 | 
             
                ensure
         | 
| 124 141 | 
             
                  @closed = true
         | 
| 125 142 | 
             
                  @replies.close
         | 
| 126 143 | 
             
                  begin
         | 
| 127 144 | 
             
                    @socket.close
         | 
| 128 | 
            -
                  rescue IOError
         | 
| 145 | 
            +
                  rescue IOError, OpenSSL::OpenSSLError, SystemCallError
         | 
| 129 146 | 
             
                    nil
         | 
| 130 147 | 
             
                  end
         | 
| 131 148 | 
             
                end
         | 
| @@ -135,20 +152,26 @@ module AMQP | |
| 135 152 | 
             
                def parse_frame(type, channel_id, frame_size, buf)
         | 
| 136 153 | 
             
                  case type
         | 
| 137 154 | 
             
                  when 1 # method frame
         | 
| 138 | 
            -
                    class_id, method_id = buf.unpack(" | 
| 155 | 
            +
                    class_id, method_id = buf.unpack("S> S>")
         | 
| 139 156 | 
             
                    case class_id
         | 
| 140 157 | 
             
                    when 10 # connection
         | 
| 141 158 | 
             
                      raise AMQP::Client::Error, "Unexpected channel id #{channel_id} for Connection frame" if channel_id != 0
         | 
| 142 159 |  | 
| 143 160 | 
             
                      case method_id
         | 
| 144 161 | 
             
                      when 50 # connection#close
         | 
| 145 | 
            -
                         | 
| 146 | 
            -
                         | 
| 147 | 
            -
                         | 
| 148 | 
            -
                         | 
| 149 | 
            -
                         | 
| 162 | 
            +
                        @closed = true
         | 
| 163 | 
            +
                        code, text_len = buf.unpack("@4 S> C")
         | 
| 164 | 
            +
                        text = buf.byteslice(7, text_len).force_encoding("utf-8")
         | 
| 165 | 
            +
                        error_class_id, error_method_id = buf.byteslice(7 + text_len, 4).unpack("S> S>")
         | 
| 166 | 
            +
                        @channels.each_value { |ch| ch.closed!(code, text, error_class_id, error_method_id) }
         | 
| 167 | 
            +
                        begin
         | 
| 168 | 
            +
                          write_bytes FrameBytes.connection_close_ok
         | 
| 169 | 
            +
                        rescue AMQP::Client::Error
         | 
| 170 | 
            +
                          nil # rabbitmq closes the socket after sending Connection::Close, so ignore write errors
         | 
| 171 | 
            +
                        end
         | 
| 150 172 | 
             
                        return false
         | 
| 151 173 | 
             
                      when 51 # connection#close-ok
         | 
| 174 | 
            +
                        @closed = true
         | 
| 152 175 | 
             
                        @replies.push [:close_ok]
         | 
| 153 176 | 
             
                        return false
         | 
| 154 177 | 
             
                      else raise AMQP::Client::UnsupportedMethodFrame, class_id, method_id
         | 
| @@ -158,11 +181,12 @@ module AMQP | |
| 158 181 | 
             
                      when 11 # channel#open-ok
         | 
| 159 182 | 
             
                        @channels[channel_id].reply [:channel_open_ok]
         | 
| 160 183 | 
             
                      when 40 # channel#close
         | 
| 161 | 
            -
                        reply_code, reply_text_len = buf.unpack("@ | 
| 162 | 
            -
                        reply_text = buf.byteslice( | 
| 163 | 
            -
                        classid, methodid = buf.byteslice( | 
| 184 | 
            +
                        reply_code, reply_text_len = buf.unpack("@4 S> C")
         | 
| 185 | 
            +
                        reply_text = buf.byteslice(7, reply_text_len).force_encoding("utf-8")
         | 
| 186 | 
            +
                        classid, methodid = buf.byteslice(7 + reply_text_len, 4).unpack("S> S>")
         | 
| 164 187 | 
             
                        channel = @channels.delete(channel_id)
         | 
| 165 188 | 
             
                        channel.closed!(reply_code, reply_text, classid, methodid)
         | 
| 189 | 
            +
                        write_bytes FrameBytes.channel_close_ok(channel_id)
         | 
| 166 190 | 
             
                      when 41 # channel#close-ok
         | 
| 167 191 | 
             
                        channel = @channels.delete(channel_id)
         | 
| 168 192 | 
             
                        channel.reply [:channel_close_ok]
         | 
| @@ -183,16 +207,16 @@ module AMQP | |
| 183 207 | 
             
                    when 50 # queue
         | 
| 184 208 | 
             
                      case method_id
         | 
| 185 209 | 
             
                      when 11 # declare-ok
         | 
| 186 | 
            -
                        queue_name_len = buf.unpack1("@ | 
| 187 | 
            -
                        queue_name = buf.byteslice( | 
| 188 | 
            -
                        message_count, consumer_count = buf.byteslice( | 
| 210 | 
            +
                        queue_name_len = buf.unpack1("@4 C")
         | 
| 211 | 
            +
                        queue_name = buf.byteslice(5, queue_name_len).force_encoding("utf-8")
         | 
| 212 | 
            +
                        message_count, consumer_count = buf.byteslice(5 + queue_name_len, 8).unpack("L> L>")
         | 
| 189 213 | 
             
                        @channels[channel_id].reply [:queue_declare_ok, queue_name, message_count, consumer_count]
         | 
| 190 214 | 
             
                      when 21 # bind-ok
         | 
| 191 215 | 
             
                        @channels[channel_id].reply [:queue_bind_ok]
         | 
| 192 216 | 
             
                      when 31 # purge-ok
         | 
| 193 217 | 
             
                        @channels[channel_id].reply [:queue_purge_ok]
         | 
| 194 218 | 
             
                      when 41 # delete-ok
         | 
| 195 | 
            -
                        message_count = buf.unpack1("@ | 
| 219 | 
            +
                        message_count = buf.unpack1("@4 L>")
         | 
| 196 220 | 
             
                        @channels[channel_id].reply [:queue_delete, message_count]
         | 
| 197 221 | 
             
                      when 51 # unbind-ok
         | 
| 198 222 | 
             
                        @channels[channel_id].reply [:queue_unbind_ok]
         | 
| @@ -203,22 +227,22 @@ module AMQP | |
| 203 227 | 
             
                      when 11 # qos-ok
         | 
| 204 228 | 
             
                        @channels[channel_id].reply [:basic_qos_ok]
         | 
| 205 229 | 
             
                      when 21 # consume-ok
         | 
| 206 | 
            -
                        tag_len = buf.unpack1("@ | 
| 207 | 
            -
                        tag = buf.byteslice( | 
| 230 | 
            +
                        tag_len = buf.unpack1("@4 C")
         | 
| 231 | 
            +
                        tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
         | 
| 208 232 | 
             
                        @channels[channel_id].reply [:basic_consume_ok, tag]
         | 
| 209 233 | 
             
                      when 30 # cancel
         | 
| 210 | 
            -
                        tag_len = buf.unpack1("@ | 
| 211 | 
            -
                        tag = buf.byteslice( | 
| 212 | 
            -
                        no_wait = buf[ | 
| 213 | 
            -
                        @channels[channel_id]. | 
| 234 | 
            +
                        tag_len = buf.unpack1("@4 C")
         | 
| 235 | 
            +
                        tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
         | 
| 236 | 
            +
                        no_wait = buf[5 + tag_len].ord
         | 
| 237 | 
            +
                        @channels[channel_id].close_consumer(tag)
         | 
| 214 238 | 
             
                        write_bytes FrameBytes.basic_cancel_ok(@id, tag) unless no_wait == 1
         | 
| 215 239 | 
             
                      when 31 # cancel-ok
         | 
| 216 | 
            -
                        tag_len = buf.unpack1("@ | 
| 217 | 
            -
                        tag = buf.byteslice( | 
| 240 | 
            +
                        tag_len = buf.unpack1("@4 C")
         | 
| 241 | 
            +
                        tag = buf.byteslice(5, tag_len).force_encoding("utf-8")
         | 
| 218 242 | 
             
                        @channels[channel_id].reply [:basic_cancel_ok, tag]
         | 
| 219 243 | 
             
                      when 50 # return
         | 
| 220 | 
            -
                        reply_code, reply_text_len = buf.unpack("@ | 
| 221 | 
            -
                        pos =  | 
| 244 | 
            +
                        reply_code, reply_text_len = buf.unpack("@4 S> C")
         | 
| 245 | 
            +
                        pos = 7
         | 
| 222 246 | 
             
                        reply_text = buf.byteslice(pos, reply_text_len).force_encoding("utf-8")
         | 
| 223 247 | 
             
                        pos += reply_text_len
         | 
| 224 248 | 
             
                        exchange_len = buf[pos].ord
         | 
| @@ -230,9 +254,9 @@ module AMQP | |
| 230 254 | 
             
                        routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
         | 
| 231 255 | 
             
                        @channels[channel_id].message_returned(reply_code, reply_text, exchange, routing_key)
         | 
| 232 256 | 
             
                      when 60 # deliver
         | 
| 233 | 
            -
                        ctag_len = buf[ | 
| 234 | 
            -
                        consumer_tag = buf.byteslice( | 
| 235 | 
            -
                        pos =  | 
| 257 | 
            +
                        ctag_len = buf[4].ord
         | 
| 258 | 
            +
                        consumer_tag = buf.byteslice(5, ctag_len).force_encoding("utf-8")
         | 
| 259 | 
            +
                        pos = 5 + ctag_len
         | 
| 236 260 | 
             
                        delivery_tag, redelivered, exchange_len = buf.byteslice(pos, 10).unpack("Q> C C")
         | 
| 237 261 | 
             
                        pos += 8 + 1 + 1
         | 
| 238 262 | 
             
                        exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
         | 
| @@ -240,35 +264,27 @@ module AMQP | |
| 240 264 | 
             
                        rk_len = buf[pos].ord
         | 
| 241 265 | 
             
                        pos += 1
         | 
| 242 266 | 
             
                        routing_key = buf.byteslice(pos, rk_len).force_encoding("utf-8")
         | 
| 243 | 
            -
                         | 
| 244 | 
            -
                          if (consumer = @channels[channel_id].consumers[consumer_tag])
         | 
| 245 | 
            -
                            consumer.push [:deliver, delivery_tag, redelivered == 1, exchange, routing_key]
         | 
| 246 | 
            -
                            break
         | 
| 247 | 
            -
                          else
         | 
| 248 | 
            -
                            Thread.pass
         | 
| 249 | 
            -
                          end
         | 
| 250 | 
            -
                        end
         | 
| 267 | 
            +
                        @channels[channel_id].message_delivered(consumer_tag, delivery_tag, redelivered == 1, exchange, routing_key)
         | 
| 251 268 | 
             
                      when 71 # get-ok
         | 
| 252 | 
            -
                        delivery_tag, redelivered, exchange_len = buf.unpack("@ | 
| 253 | 
            -
                        pos =  | 
| 269 | 
            +
                        delivery_tag, redelivered, exchange_len = buf.unpack("@4 Q> C C")
         | 
| 270 | 
            +
                        pos = 14
         | 
| 254 271 | 
             
                        exchange = buf.byteslice(pos, exchange_len).force_encoding("utf-8")
         | 
| 255 272 | 
             
                        pos += exchange_len
         | 
| 256 273 | 
             
                        routing_key_len = buf[pos].ord
         | 
| 257 274 | 
             
                        pos += 1
         | 
| 258 275 | 
             
                        routing_key = buf.byteslice(pos, routing_key_len).force_encoding("utf-8")
         | 
| 259 276 | 
             
                        pos += routing_key_len
         | 
| 260 | 
            -
                         | 
| 261 | 
            -
                         | 
| 262 | 
            -
                        @channels[channel_id].reply [:basic_get_ok, delivery_tag, exchange, routing_key, message_count, redelivered]
         | 
| 277 | 
            +
                        _message_count = buf.byteslice(pos, 4).unpack1("L>")
         | 
| 278 | 
            +
                        @channels[channel_id].message_delivered(nil, delivery_tag, redelivered == 1, exchange, routing_key)
         | 
| 263 279 | 
             
                      when 72 # get-empty
         | 
| 264 | 
            -
                        @channels[channel_id]. | 
| 280 | 
            +
                        @channels[channel_id].basic_get_empty
         | 
| 265 281 | 
             
                      when 80 # ack
         | 
| 266 | 
            -
                        delivery_tag, multiple = buf.unpack("@ | 
| 282 | 
            +
                        delivery_tag, multiple = buf.unpack("@4 Q> C")
         | 
| 267 283 | 
             
                        @channels[channel_id].confirm [:ack, delivery_tag, multiple == 1]
         | 
| 268 284 | 
             
                      when 111 # recover-ok
         | 
| 269 285 | 
             
                        @channels[channel_id].reply [:basic_recover_ok]
         | 
| 270 286 | 
             
                      when 120 # nack
         | 
| 271 | 
            -
                        delivery_tag, multiple, requeue = buf.unpack("@ | 
| 287 | 
            +
                        delivery_tag, multiple, requeue = buf.unpack("@4 Q> C C")
         | 
| 272 288 | 
             
                        @channels[channel_id].confirm [:nack, delivery_tag, multiple == 1, requeue == 1]
         | 
| 273 289 | 
             
                      else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
         | 
| 274 290 | 
             
                      end
         | 
| @@ -291,20 +307,19 @@ module AMQP | |
| 291 307 | 
             
                    else raise AMQP::Client::UnsupportedMethodFrame.new class_id, method_id
         | 
| 292 308 | 
             
                    end
         | 
| 293 309 | 
             
                  when 2 # header
         | 
| 294 | 
            -
                    body_size = buf.unpack1("@ | 
| 295 | 
            -
                    properties = Properties.decode(buf.byteslice( | 
| 296 | 
            -
                    @channels[channel_id]. | 
| 310 | 
            +
                    body_size = buf.unpack1("@4 Q>")
         | 
| 311 | 
            +
                    properties = Properties.decode(buf.byteslice(12, buf.bytesize - 12))
         | 
| 312 | 
            +
                    @channels[channel_id].header_delivered body_size, properties
         | 
| 297 313 | 
             
                  when 3 # body
         | 
| 298 | 
            -
                     | 
| 299 | 
            -
                    @channels[channel_id].reply [:body, body]
         | 
| 314 | 
            +
                    @channels[channel_id].body_delivered buf
         | 
| 300 315 | 
             
                  else raise AMQP::Client::UnsupportedFrameType, type
         | 
| 301 316 | 
             
                  end
         | 
| 302 317 | 
             
                  true
         | 
| 303 318 | 
             
                end
         | 
| 304 319 |  | 
| 305 320 | 
             
                def expect(expected_frame_type)
         | 
| 306 | 
            -
                  frame_type, args = @replies. | 
| 307 | 
            -
                  frame_type == expected_frame_type || raise(UnexpectedFrame.new(expected_frame_type, frame_type))
         | 
| 321 | 
            +
                  frame_type, args = @replies.pop
         | 
| 322 | 
            +
                  frame_type == expected_frame_type || raise(AMQP::Client::UnexpectedFrame.new(expected_frame_type, frame_type))
         | 
| 308 323 | 
             
                  args
         | 
| 309 324 | 
             
                end
         | 
| 310 325 |  | 
| @@ -320,7 +335,7 @@ module AMQP | |
| 320 335 | 
             
                    end
         | 
| 321 336 |  | 
| 322 337 | 
             
                    type, channel_id, frame_size = buf.unpack("C S> L>")
         | 
| 323 | 
            -
                    frame_end = buf | 
| 338 | 
            +
                    frame_end = buf[frame_size + 7].ord
         | 
| 324 339 | 
             
                    raise UnexpectedFrameEndError, frame_end if frame_end != 206
         | 
| 325 340 |  | 
| 326 341 | 
             
                    case type
         | 
| @@ -367,7 +382,7 @@ module AMQP | |
| 367 382 | 
             
                  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
         | 
| 368 383 | 
             
                  socket.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)
         | 
| 369 384 | 
             
                rescue StandardError => e
         | 
| 370 | 
            -
                  warn " | 
| 385 | 
            +
                  warn "AMQP-Client could not enable TCP keepalive on socket. #{e.inspect}"
         | 
| 371 386 | 
             
                end
         | 
| 372 387 |  | 
| 373 388 | 
             
                def self.port_from_env
         | 
    
        data/lib/amqp/client/message.rb
    CHANGED
    
    
| @@ -38,7 +38,8 @@ module AMQP | |
| 38 38 | 
             
                  end
         | 
| 39 39 |  | 
| 40 40 | 
             
                  if delivery_mode
         | 
| 41 | 
            -
                     | 
| 41 | 
            +
                    delivery_mode.is_a?(Integer) || raise(ArgumentError, "delivery_mode must be an int")
         | 
| 42 | 
            +
                    delivery_mode.between?(0, 2) || raise(ArgumentError, "delivery_mode must be be between 0 and 2")
         | 
| 42 43 |  | 
| 43 44 | 
             
                    flags |= (1 << 12)
         | 
| 44 45 | 
             
                    arr << delivery_mode
         | 
| @@ -69,7 +70,7 @@ module AMQP | |
| 69 70 | 
             
                  end
         | 
| 70 71 |  | 
| 71 72 | 
             
                  if expiration
         | 
| 72 | 
            -
                    expiration = expiration.to_s if expiration.is_a?(Integer)
         | 
| 73 | 
            +
                    self.expiration = expiration.to_s if expiration.is_a?(Integer)
         | 
| 73 74 | 
             
                    expiration.is_a?(String) || raise(ArgumentError, "expiration must be a string or integer")
         | 
| 74 75 |  | 
| 75 76 | 
             
                    flags |= (1 << 8)
         | 
| @@ -86,7 +87,7 @@ module AMQP | |
| 86 87 | 
             
                  end
         | 
| 87 88 |  | 
| 88 89 | 
             
                  if timestamp
         | 
| 89 | 
            -
                    timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be a  | 
| 90 | 
            +
                    timestamp.is_a?(Integer) || timestamp.is_a?(Time) || raise(ArgumentError, "timestamp must be an Integer or a Time")
         | 
| 90 91 |  | 
| 91 92 | 
             
                    flags |= (1 << 6)
         | 
| 92 93 | 
             
                    arr << timestamp.to_i
         | 
    
        data/lib/amqp/client/version.rb
    CHANGED
    
    
    
        data/lib/amqp/client.rb
    CHANGED
    
    | @@ -12,31 +12,41 @@ module AMQP | |
| 12 12 | 
             
                  @options = options
         | 
| 13 13 |  | 
| 14 14 | 
             
                  @queues = {}
         | 
| 15 | 
            +
                  @exchanges = {}
         | 
| 15 16 | 
             
                  @subscriptions = Set.new
         | 
| 16 17 | 
             
                  @connq = SizedQueue.new(1)
         | 
| 17 18 | 
             
                end
         | 
| 18 19 |  | 
| 20 | 
            +
                # Opens an AMQP connection, does not try to reconnect
         | 
| 19 21 | 
             
                def connect(read_loop_thread: true)
         | 
| 20 | 
            -
                  Connection.connect(@uri,  | 
| 22 | 
            +
                  Connection.connect(@uri, read_loop_thread: read_loop_thread, **@options)
         | 
| 21 23 | 
             
                end
         | 
| 22 24 |  | 
| 25 | 
            +
                # Opens an AMQP connection using the high level API, will try to reconnect
         | 
| 23 26 | 
             
                def start
         | 
| 24 27 | 
             
                  @stopped = false
         | 
| 25 | 
            -
                  Thread.new do
         | 
| 28 | 
            +
                  Thread.new(connect(read_loop_thread: false)) do |conn|
         | 
| 29 | 
            +
                    Thread.abort_on_exception = true # Raising an unhandled exception is a bug
         | 
| 26 30 | 
             
                    loop do
         | 
| 27 31 | 
             
                      break if @stopped
         | 
| 28 32 |  | 
| 29 | 
            -
                      conn  | 
| 33 | 
            +
                      conn ||= connect(read_loop_thread: false)
         | 
| 30 34 | 
             
                      Thread.new do
         | 
| 31 35 | 
             
                        # restore connection in another thread, read_loop have to run
         | 
| 32 36 | 
             
                        conn.channel(1) # reserve channel 1 for publishes
         | 
| 33 | 
            -
                        @subscriptions.each  | 
| 37 | 
            +
                        @subscriptions.each do |queue_name, no_ack, prefetch, wt, args, blk|
         | 
| 38 | 
            +
                          ch = conn.channel
         | 
| 39 | 
            +
                          ch.basic_qos(prefetch)
         | 
| 40 | 
            +
                          ch.basic_consume(queue_name, no_ack: no_ack, worker_threads: wt, arguments: args, &blk)
         | 
| 41 | 
            +
                        end
         | 
| 34 42 | 
             
                        @connq << conn
         | 
| 35 43 | 
             
                      end
         | 
| 36 44 | 
             
                      conn.read_loop # blocks until connection is closed, then reconnect
         | 
| 37 | 
            -
                    rescue => e
         | 
| 45 | 
            +
                    rescue AMQP::Client::Error => e
         | 
| 38 46 | 
             
                      warn "AMQP-Client reconnect error: #{e.inspect}"
         | 
| 39 47 | 
             
                      sleep @options[:reconnect_interval] || 1
         | 
| 48 | 
            +
                    ensure
         | 
| 49 | 
            +
                      conn = nil
         | 
| 40 50 | 
             
                    end
         | 
| 41 51 | 
             
                  end
         | 
| 42 52 | 
             
                  self
         | 
| @@ -49,21 +59,58 @@ module AMQP | |
| 49 59 | 
             
                  nil
         | 
| 50 60 | 
             
                end
         | 
| 51 61 |  | 
| 52 | 
            -
                def queue(name, arguments: {})
         | 
| 62 | 
            +
                def queue(name, durable: true, exclusive: false, auto_delete: false, arguments: {})
         | 
| 53 63 | 
             
                  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
         | 
| 54 64 |  | 
| 55 65 | 
             
                  @queues.fetch(name) do
         | 
| 56 66 | 
             
                    with_connection do |conn|
         | 
| 57 67 | 
             
                      conn.with_channel do |ch| # use a temp channel in case the declaration fails
         | 
| 58 | 
            -
                        ch.queue_declare(name, arguments: arguments)
         | 
| 68 | 
            +
                        ch.queue_declare(name, durable: durable, exclusive: exclusive, auto_delete: auto_delete, arguments: arguments)
         | 
| 59 69 | 
             
                      end
         | 
| 60 70 | 
             
                    end
         | 
| 61 71 | 
             
                    @queues[name] = Queue.new(self, name)
         | 
| 62 72 | 
             
                  end
         | 
| 63 73 | 
             
                end
         | 
| 64 74 |  | 
| 75 | 
            +
                def exchange(name, type, durable: true, auto_delete: false, internal: false, arguments: {})
         | 
| 76 | 
            +
                  @exchanges.fetch(name) do
         | 
| 77 | 
            +
                    with_connection do |conn|
         | 
| 78 | 
            +
                      conn.with_channel do |ch|
         | 
| 79 | 
            +
                        ch.exchange_declare(name, type, durable: durable, auto_delete: auto_delete, internal: internal, arguments: arguments)
         | 
| 80 | 
            +
                      end
         | 
| 81 | 
            +
                    end
         | 
| 82 | 
            +
                    @exchanges[name] = Exchange.new(self, name)
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                end
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                # High level representation of an exchange
         | 
| 87 | 
            +
                class Exchange
         | 
| 88 | 
            +
                  def initialize(client, name)
         | 
| 89 | 
            +
                    @client = client
         | 
| 90 | 
            +
                    @name = name
         | 
| 91 | 
            +
                  end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
                  def publish(body, routing_key, arguments: {})
         | 
| 94 | 
            +
                    @client.publish(body, @name, routing_key, arguments: arguments)
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                  # Bind to another exchange
         | 
| 98 | 
            +
                  def bind(exchange, routing_key, arguments: {})
         | 
| 99 | 
            +
                    @client.exchange_bind(@name, exchange, routing_key, arguments: arguments)
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                  # Unbind from another exchange
         | 
| 103 | 
            +
                  def unbind(exchange, routing_key, arguments: {})
         | 
| 104 | 
            +
                    @client.exchange_unbind(@name, exchange, routing_key, arguments: arguments)
         | 
| 105 | 
            +
                  end
         | 
| 106 | 
            +
             | 
| 107 | 
            +
                  def delete
         | 
| 108 | 
            +
                    @client.delete_exchange(@name)
         | 
| 109 | 
            +
                  end
         | 
| 110 | 
            +
                end
         | 
| 111 | 
            +
             | 
| 65 112 | 
             
                def subscribe(queue_name, no_ack: false, prefetch: 1, worker_threads: 1, arguments: {}, &blk)
         | 
| 66 | 
            -
                  @subscriptions.add? [queue_name, no_ack, prefetch, arguments, blk]
         | 
| 113 | 
            +
                  @subscriptions.add? [queue_name, no_ack, prefetch, worker_threads, arguments, blk]
         | 
| 67 114 |  | 
| 68 115 | 
             
                  with_connection do |conn|
         | 
| 69 116 | 
             
                    ch = conn.channel
         | 
| @@ -74,28 +121,49 @@ module AMQP | |
| 74 121 | 
             
                  end
         | 
| 75 122 | 
             
                end
         | 
| 76 123 |  | 
| 124 | 
            +
                # Publish a (persistent) message and wait for confirmation
         | 
| 77 125 | 
             
                def publish(body, exchange, routing_key, **properties)
         | 
| 78 126 | 
             
                  with_connection do |conn|
         | 
| 79 | 
            -
                     | 
| 127 | 
            +
                    properties = { delivery_mode: 2 }.merge!(properties)
         | 
| 80 128 | 
             
                    conn.channel(1).basic_publish_confirm(body, exchange, routing_key, **properties)
         | 
| 81 | 
            -
                  rescue
         | 
| 82 | 
            -
                    conn.channel(1) # reopen channel 1 if it raised
         | 
| 83 | 
            -
                    raise
         | 
| 84 129 | 
             
                  end
         | 
| 85 | 
            -
                rescue => e
         | 
| 86 | 
            -
                  warn "AMQP-Client error publishing, retrying (#{e.inspect})"
         | 
| 87 | 
            -
                  retry
         | 
| 88 130 | 
             
                end
         | 
| 89 131 |  | 
| 90 | 
            -
                 | 
| 132 | 
            +
                # Publish a (persistent) message but don't wait for a confirmation
         | 
| 133 | 
            +
                def publish_and_forget(body, exchange, routing_key, **properties)
         | 
| 91 134 | 
             
                  with_connection do |conn|
         | 
| 92 | 
            -
                     | 
| 135 | 
            +
                    properties = { delivery_mode: 2 }.merge!(properties)
         | 
| 136 | 
            +
                    conn.channel(1).basic_publish(body, exchange, routing_key, **properties)
         | 
| 93 137 | 
             
                  end
         | 
| 94 138 | 
             
                end
         | 
| 95 139 |  | 
| 96 | 
            -
                def  | 
| 140 | 
            +
                def wait_for_confirms
         | 
| 97 141 | 
             
                  with_connection do |conn|
         | 
| 98 | 
            -
                    conn.channel(1). | 
| 142 | 
            +
                    conn.channel(1).wait_for_confirms
         | 
| 143 | 
            +
                  end
         | 
| 144 | 
            +
                end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
                def bind(queue, exchange, routing_key, arguments: {})
         | 
| 147 | 
            +
                  with_connection do |conn|
         | 
| 148 | 
            +
                    conn.channel(1).queue_bind(queue, exchange, routing_key, arguments: arguments)
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
                end
         | 
| 151 | 
            +
             | 
| 152 | 
            +
                def unbind(queue, exchange, routing_key, arguments: {})
         | 
| 153 | 
            +
                  with_connection do |conn|
         | 
| 154 | 
            +
                    conn.channel(1).queue_unbind(queue, exchange, routing_key, arguments: arguments)
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                end
         | 
| 157 | 
            +
             | 
| 158 | 
            +
                def exchange_bind(destination, source, routing_key, arguments: {})
         | 
| 159 | 
            +
                  with_connection do |conn|
         | 
| 160 | 
            +
                    conn.channel(1).exchange_bind(destination, source, routing_key, arguments: arguments)
         | 
| 161 | 
            +
                  end
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
             | 
| 164 | 
            +
                def exchange_unbind(destination, source, routing_key, arguments: {})
         | 
| 165 | 
            +
                  with_connection do |conn|
         | 
| 166 | 
            +
                    conn.channel(1).exchange_unbind(destination, source, routing_key, arguments: arguments)
         | 
| 99 167 | 
             
                  end
         | 
| 100 168 | 
             
                end
         | 
| 101 169 |  | 
| @@ -105,9 +173,17 @@ module AMQP | |
| 105 173 | 
             
                  end
         | 
| 106 174 | 
             
                end
         | 
| 107 175 |  | 
| 108 | 
            -
                def delete_queue( | 
| 176 | 
            +
                def delete_queue(name)
         | 
| 177 | 
            +
                  with_connection do |conn|
         | 
| 178 | 
            +
                    conn.channel(1).queue_delete(name)
         | 
| 179 | 
            +
                    @queues.delete(name)
         | 
| 180 | 
            +
                  end
         | 
| 181 | 
            +
                end
         | 
| 182 | 
            +
             | 
| 183 | 
            +
                def delete_exchange(name)
         | 
| 109 184 | 
             
                  with_connection do |conn|
         | 
| 110 | 
            -
                    conn.channel(1). | 
| 185 | 
            +
                    conn.channel(1).exchange_delete(name)
         | 
| 186 | 
            +
                    @exchanges.delete(name)
         | 
| 111 187 | 
             
                  end
         | 
| 112 188 | 
             
                end
         | 
| 113 189 |  | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: amqp-client
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0. | 
| 4 | 
            +
              version: 1.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Carl Hörberg
         | 
| 8 8 | 
             
            autorequire:
         | 
| 9 9 | 
             
            bindir: exe
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021-08- | 
| 11 | 
            +
            date: 2021-08-27 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies: []
         | 
| 13 13 | 
             
            description: Work in progress
         | 
| 14 14 | 
             
            email:
         | 
| @@ -61,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 61 61 | 
             
                - !ruby/object:Gem::Version
         | 
| 62 62 | 
             
                  version: '0'
         | 
| 63 63 | 
             
            requirements: []
         | 
| 64 | 
            -
            rubygems_version: 3.2. | 
| 64 | 
            +
            rubygems_version: 3.2.22
         | 
| 65 65 | 
             
            signing_key:
         | 
| 66 66 | 
             
            specification_version: 4
         | 
| 67 67 | 
             
            summary: AMQP 0-9-1 client
         |