amqp-client 1.2.0 → 2.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/lib/amqp/client/channel.rb +115 -46
- data/lib/amqp/client/configuration.rb +66 -0
- data/lib/amqp/client/connection.rb +35 -18
- data/lib/amqp/client/consumer.rb +47 -0
- data/lib/amqp/client/errors.rb +84 -9
- data/lib/amqp/client/exchange.rb +30 -28
- data/lib/amqp/client/frame_bytes.rb +3 -4
- data/lib/amqp/client/message.rb +66 -1
- data/lib/amqp/client/message_codec_registry.rb +106 -0
- data/lib/amqp/client/message_codecs.rb +43 -0
- data/lib/amqp/client/properties.rb +16 -15
- data/lib/amqp/client/queue.rb +52 -14
- data/lib/amqp/client/rpc_client.rb +56 -0
- data/lib/amqp/client/table.rb +2 -2
- data/lib/amqp/client/version.rb +1 -1
- data/lib/amqp/client.rb +369 -61
- metadata +8 -18
- data/.github/workflows/codeql-analysis.yml +0 -41
- data/.github/workflows/docs.yml +0 -28
- data/.github/workflows/main.yml +0 -140
- data/.github/workflows/release.yml +0 -26
- data/.gitignore +0 -9
- data/.rubocop.yml +0 -30
- data/.rubocop_todo.yml +0 -65
- data/.yardopts +0 -1
- data/CHANGELOG.md +0 -109
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -18
- data/README.md +0 -193
- data/Rakefile +0 -180
- data/amqp-client.gemspec +0 -29
- data/bin/console +0 -15
- data/bin/setup +0 -8
    
        data/lib/amqp/client.rb
    CHANGED
    
    | @@ -4,6 +4,11 @@ require_relative "client/version" | |
| 4 4 | 
             
            require_relative "client/connection"
         | 
| 5 5 | 
             
            require_relative "client/exchange"
         | 
| 6 6 | 
             
            require_relative "client/queue"
         | 
| 7 | 
            +
            require_relative "client/consumer"
         | 
| 8 | 
            +
            require_relative "client/rpc_client"
         | 
| 9 | 
            +
            require_relative "client/message_codecs"
         | 
| 10 | 
            +
            require_relative "client/message_codec_registry"
         | 
| 11 | 
            +
            require_relative "client/configuration"
         | 
| 7 12 |  | 
| 8 13 | 
             
            # AMQP 0-9-1 Protocol, this library only implements the Client
         | 
| 9 14 | 
             
            # @see Client
         | 
| @@ -11,6 +16,11 @@ module AMQP | |
| 11 16 | 
             
              # AMQP 0-9-1 Client
         | 
| 12 17 | 
             
              # @see Connection
         | 
| 13 18 | 
             
              class Client
         | 
| 19 | 
            +
                # Class-level codec registry
         | 
| 20 | 
            +
                @codec_registry = MessageCodecRegistry.new
         | 
| 21 | 
            +
                # Class-level configuration
         | 
| 22 | 
            +
                @config = Configuration.new(@codec_registry)
         | 
| 23 | 
            +
             | 
| 14 24 | 
             
                # Create a new Client object, this won't establish a connection yet, use {#connect} or {#start} for that
         | 
| 15 25 | 
             
                # @param uri [String] URL on the format amqp://username:password@hostname/vhost,
         | 
| 16 26 | 
             
                #   use amqps:// for encrypted connection
         | 
| @@ -20,15 +30,23 @@ module AMQP | |
| 20 30 | 
             
                # @option options [Integer] heartbeat (0) Heartbeat timeout, defaults to 0 and relies on TCP keepalive instead
         | 
| 21 31 | 
             
                # @option options [Integer] frame_max (131_072) Maximum frame size,
         | 
| 22 32 | 
             
                #    the smallest of the client's and the broker's values will be used
         | 
| 23 | 
            -
                # @option options [Integer] channel_max (2048)  | 
| 24 | 
            -
                #    | 
| 33 | 
            +
                # @option options [Integer] channel_max (2048) Maximum number of channels the client will be allowed to have open.
         | 
| 34 | 
            +
                #   Maximum allowed is 65_536.  The smallest of the client's and the broker's value will be used.
         | 
| 25 35 | 
             
                def initialize(uri = "", **options)
         | 
| 26 36 | 
             
                  @uri = uri
         | 
| 27 37 | 
             
                  @options = options
         | 
| 28 38 | 
             
                  @queues = {}
         | 
| 29 39 | 
             
                  @exchanges = {}
         | 
| 30 | 
            -
                  @ | 
| 40 | 
            +
                  @consumers = {}
         | 
| 41 | 
            +
                  @next_consumer_id = 0
         | 
| 31 42 | 
             
                  @connq = SizedQueue.new(1)
         | 
| 43 | 
            +
                  @codec_registry = self.class.codec_registry.dup
         | 
| 44 | 
            +
                  @strict_coding = self.class.config.strict_coding
         | 
| 45 | 
            +
                  @default_content_encoding = self.class.config.default_content_encoding
         | 
| 46 | 
            +
                  @default_content_type = self.class.config.default_content_type
         | 
| 47 | 
            +
                  @start_lock = Mutex.new
         | 
| 48 | 
            +
                  @supervisor_started = false
         | 
| 49 | 
            +
                  @stopped = false
         | 
| 32 50 | 
             
                end
         | 
| 33 51 |  | 
| 34 52 | 
             
                # @!group Connect and disconnect
         | 
| @@ -39,7 +57,7 @@ module AMQP | |
| 39 57 | 
             
                # @example
         | 
| 40 58 | 
             
                #   connection = AMQP::Client.new("amqps://server.rmq.cloudamqp.com", connection_name: "My connection").connect
         | 
| 41 59 | 
             
                def connect(read_loop_thread: true)
         | 
| 42 | 
            -
                  Connection.new(@uri, read_loop_thread:  | 
| 60 | 
            +
                  Connection.new(@uri, read_loop_thread:, codec_registry: @codec_registry, strict_coding: @strict_coding, **@options)
         | 
| 43 61 | 
             
                end
         | 
| 44 62 |  | 
| 45 63 | 
             
                # Opens an AMQP connection using the high level API, will try to reconnect if successfully connected at first
         | 
| @@ -49,45 +67,68 @@ module AMQP | |
| 49 67 | 
             
                #   amqp.start
         | 
| 50 68 | 
             
                #   amqp.queue("foobar")
         | 
| 51 69 | 
             
                def start
         | 
| 52 | 
            -
                   | 
| 53 | 
            -
             | 
| 54 | 
            -
             | 
| 55 | 
            -
                     | 
| 56 | 
            -
             | 
| 57 | 
            -
             | 
| 58 | 
            -
             | 
| 59 | 
            -
             | 
| 60 | 
            -
             | 
| 61 | 
            -
             | 
| 62 | 
            -
                         | 
| 63 | 
            -
             | 
| 64 | 
            -
             | 
| 65 | 
            -
             | 
| 70 | 
            +
                  return self if started?
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                  @start_lock.synchronize do # rubocop:disable Metrics/BlockLength
         | 
| 73 | 
            +
                    return self if started?
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                    @supervisor_started = true
         | 
| 76 | 
            +
                    @stopped = false
         | 
| 77 | 
            +
                    Thread.new(connect(read_loop_thread: false)) do |conn|
         | 
| 78 | 
            +
                      Thread.current.abort_on_exception = true # Raising an unhandled exception is a bug
         | 
| 79 | 
            +
                      loop do
         | 
| 80 | 
            +
                        break if @stopped
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                        conn ||= connect(read_loop_thread: false)
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                        Thread.new do
         | 
| 85 | 
            +
                          # restore connection in another thread, read_loop have to run
         | 
| 86 | 
            +
                          conn.channel(1) # reserve channel 1 for publishes
         | 
| 87 | 
            +
                          @consumers.each_value do |consumer|
         | 
| 88 | 
            +
                            ch = conn.channel
         | 
| 89 | 
            +
                            ch.basic_qos(consumer.prefetch)
         | 
| 90 | 
            +
                            consume_ok = ch.basic_consume(consumer.queue,
         | 
| 91 | 
            +
                                                          **consumer.basic_consume_args,
         | 
| 92 | 
            +
                                                          &consumer.block)
         | 
| 93 | 
            +
                            # Update the consumer with new channel and consume_ok metadata
         | 
| 94 | 
            +
                            consumer.update_consume_ok(consume_ok)
         | 
| 95 | 
            +
                          end
         | 
| 96 | 
            +
                          @connq << conn
         | 
| 97 | 
            +
                          # Remove consumers whose internal queues were already closed (e.g. cancelled during reconnect window)
         | 
| 98 | 
            +
                          @consumers.delete_if { |_, c| c.closed? }
         | 
| 66 99 | 
             
                        end
         | 
| 67 | 
            -
                         | 
| 100 | 
            +
                        conn.read_loop # blocks until connection is closed, then reconnect
         | 
| 101 | 
            +
                      rescue Error => e
         | 
| 102 | 
            +
                        warn "AMQP-Client reconnect error: #{e.inspect}"
         | 
| 103 | 
            +
                        sleep @options[:reconnect_interval] || 1
         | 
| 104 | 
            +
                      ensure
         | 
| 105 | 
            +
                        @connq.clear
         | 
| 106 | 
            +
                        conn = nil
         | 
| 68 107 | 
             
                      end
         | 
| 69 | 
            -
                      conn.read_loop # blocks until connection is closed, then reconnect
         | 
| 70 | 
            -
                    rescue Error => e
         | 
| 71 | 
            -
                      warn "AMQP-Client reconnect error: #{e.inspect}"
         | 
| 72 | 
            -
                      sleep @options[:reconnect_interval] || 1
         | 
| 73 | 
            -
                    ensure
         | 
| 74 | 
            -
                      conn = nil
         | 
| 75 108 | 
             
                    end
         | 
| 76 109 | 
             
                  end
         | 
| 77 110 | 
             
                  self
         | 
| 78 111 | 
             
                end
         | 
| 79 112 |  | 
| 80 | 
            -
                # Close the currently open connection
         | 
| 113 | 
            +
                # Close the currently open connection and stop the supervision / reconnection logic.
         | 
| 81 114 | 
             
                # @return [nil]
         | 
| 82 115 | 
             
                def stop
         | 
| 83 | 
            -
                  return if @stopped
         | 
| 116 | 
            +
                  return if @stopped && !@supervisor_started
         | 
| 84 117 |  | 
| 85 118 | 
             
                  @stopped = true
         | 
| 119 | 
            +
                  return unless @connq.size.positive?
         | 
| 120 | 
            +
             | 
| 86 121 | 
             
                  conn = @connq.pop
         | 
| 87 122 | 
             
                  conn.close
         | 
| 88 123 | 
             
                  nil
         | 
| 89 124 | 
             
                end
         | 
| 90 125 |  | 
| 126 | 
            +
                # Check if the client is connected
         | 
| 127 | 
            +
                # @return [Boolean] true if connected or currently trying to connect, false otherwise
         | 
| 128 | 
            +
                def started?
         | 
| 129 | 
            +
                  @supervisor_started && !@stopped
         | 
| 130 | 
            +
                end
         | 
| 131 | 
            +
             | 
| 91 132 | 
             
                # @!endgroup
         | 
| 92 133 | 
             
                # @!group High level objects
         | 
| 93 134 |  | 
| @@ -97,51 +138,133 @@ module AMQP | |
| 97 138 | 
             
                #   messages in the queue will only survive if they are published as persistent
         | 
| 98 139 | 
             
                # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
         | 
| 99 140 | 
             
                #   (it won't be deleted until at least one consumer has consumed from it)
         | 
| 141 | 
            +
                # @param exclusive [Boolean] If true the queue will be deleted when the connection is closed
         | 
| 142 | 
            +
                # @param passive [Boolean] If true an exception will be raised if the queue doesn't already exists
         | 
| 100 143 | 
             
                # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
         | 
| 101 144 | 
             
                # @return [Queue]
         | 
| 102 145 | 
             
                # @example
         | 
| 103 146 | 
             
                #   amqp = AMQP::Client.new.start
         | 
| 104 147 | 
             
                #   q = amqp.queue("foobar")
         | 
| 105 148 | 
             
                #   q.publish("body")
         | 
| 106 | 
            -
                def queue(name, durable: true, auto_delete: false, arguments: {})
         | 
| 149 | 
            +
                def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
         | 
| 107 150 | 
             
                  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
         | 
| 108 151 |  | 
| 109 152 | 
             
                  @queues.fetch(name) do
         | 
| 110 153 | 
             
                    with_connection do |conn|
         | 
| 111 | 
            -
                      conn.channel(1).queue_declare(name, durable | 
| 154 | 
            +
                      conn.channel(1).queue_declare(name, durable:, auto_delete:, exclusive:, passive:, arguments:)
         | 
| 112 155 | 
             
                    end
         | 
| 113 156 | 
             
                    @queues[name] = Queue.new(self, name)
         | 
| 114 157 | 
             
                  end
         | 
| 115 158 | 
             
                end
         | 
| 116 159 |  | 
| 117 160 | 
             
                # Declare an exchange and return a high level Exchange object
         | 
| 161 | 
            +
                # @param name [String] Name of the exchange
         | 
| 162 | 
            +
                # @param type [String] Type of the exchange, one of "direct", "fanout", "topic", "headers" or custom exchange type
         | 
| 163 | 
            +
                # @param durable [Boolean] If true the exchange will survive broker restarts
         | 
| 164 | 
            +
                # @param auto_delete [Boolean] If true the exchange will be deleted when the last queue is unbound
         | 
| 165 | 
            +
                # @param internal [Boolean] If true the exchange will not accept directly published messages
         | 
| 166 | 
            +
                # @param arguments [Hash] Custom arguments such as alternate-exchange etc.
         | 
| 118 167 | 
             
                # @return [Exchange]
         | 
| 119 168 | 
             
                # @example
         | 
| 120 169 | 
             
                #   amqp = AMQP::Client.new.start
         | 
| 121 | 
            -
                #   x = amqp.exchange("my.hash.exchange", "x-consistent-hash")
         | 
| 122 | 
            -
                #   x.publish("body", "routing-key")
         | 
| 123 | 
            -
                def exchange(name, type | 
| 170 | 
            +
                #   x = amqp.exchange("my.hash.exchange", type: "x-consistent-hash")
         | 
| 171 | 
            +
                #   x.publish("body", routing_key: "routing-key")
         | 
| 172 | 
            +
                def exchange(name, type:, durable: true, auto_delete: false, internal: false, arguments: {})
         | 
| 124 173 | 
             
                  @exchanges.fetch(name) do
         | 
| 125 174 | 
             
                    with_connection do |conn|
         | 
| 126 | 
            -
                      conn.channel(1).exchange_declare(name, type | 
| 127 | 
            -
                                                                   internal: internal, arguments: arguments)
         | 
| 175 | 
            +
                      conn.channel(1).exchange_declare(name, type:, durable:, auto_delete:, internal:, arguments:)
         | 
| 128 176 | 
             
                    end
         | 
| 129 177 | 
             
                    @exchanges[name] = Exchange.new(self, name)
         | 
| 130 178 | 
             
                  end
         | 
| 131 179 | 
             
                end
         | 
| 132 180 |  | 
| 181 | 
            +
                # Declare a direct exchange and return a high level Exchange object
         | 
| 182 | 
            +
                # @param name [String] Name of the exchange (defaults to "amq.direct")
         | 
| 183 | 
            +
                # @see #exchange for other parameters
         | 
| 184 | 
            +
                # @return [Exchange]
         | 
| 185 | 
            +
                def direct_exchange(name = "amq.direct", **)
         | 
| 186 | 
            +
                  return exchange(name, type: "direct", **) unless name.empty?
         | 
| 187 | 
            +
             | 
| 188 | 
            +
                  # Return the default exchange
         | 
| 189 | 
            +
                  @exchanges.fetch(name) do
         | 
| 190 | 
            +
                    @exchanges[name] = Exchange.new(self, name)
         | 
| 191 | 
            +
                  end
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
             | 
| 194 | 
            +
                # @deprecated
         | 
| 195 | 
            +
                # @see #direct_exchange
         | 
| 196 | 
            +
                alias direct direct_exchange
         | 
| 197 | 
            +
             | 
| 198 | 
            +
                # Return a high level Exchange object for the default direct exchange
         | 
| 199 | 
            +
                # @see #direct for parameters
         | 
| 200 | 
            +
                # @return [Exchange]
         | 
| 201 | 
            +
                def default_exchange(**)
         | 
| 202 | 
            +
                  direct("", **)
         | 
| 203 | 
            +
                end
         | 
| 204 | 
            +
             | 
| 205 | 
            +
                # @deprecated
         | 
| 206 | 
            +
                # @see #default_exchange
         | 
| 207 | 
            +
                alias default default_exchange
         | 
| 208 | 
            +
             | 
| 209 | 
            +
                # Declare a fanout exchange and return a high level Exchange object
         | 
| 210 | 
            +
                # @param name [String] Name of the exchange (defaults to "amq.fanout")
         | 
| 211 | 
            +
                # @see #exchange for other parameters
         | 
| 212 | 
            +
                # @return [Exchange]
         | 
| 213 | 
            +
                def fanout_exchange(name = "amq.fanout", **)
         | 
| 214 | 
            +
                  exchange(name, type: "fanout", **)
         | 
| 215 | 
            +
                end
         | 
| 216 | 
            +
             | 
| 217 | 
            +
                # @deprecated
         | 
| 218 | 
            +
                # @see #fanout_exchange
         | 
| 219 | 
            +
                alias fanout fanout_exchange
         | 
| 220 | 
            +
             | 
| 221 | 
            +
                # Declare a topic exchange and return a high level Exchange object
         | 
| 222 | 
            +
                # @param name [String] Name of the exchange (defaults to "amq.topic")
         | 
| 223 | 
            +
                # @see #exchange for other parameters
         | 
| 224 | 
            +
                # @return [Exchange]
         | 
| 225 | 
            +
                def topic_exchange(name = "amq.topic", **)
         | 
| 226 | 
            +
                  exchange(name, type: "topic", **)
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                # @deprecated
         | 
| 230 | 
            +
                # @see #topic_exchange
         | 
| 231 | 
            +
                alias topic topic_exchange
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                # Declare a headers exchange and return a high level Exchange object
         | 
| 234 | 
            +
                # @param name [String] Name of the exchange (defaults to "amq.headers")
         | 
| 235 | 
            +
                # @see #exchange for other parameters
         | 
| 236 | 
            +
                # @return [Exchange]
         | 
| 237 | 
            +
                def headers_exchange(name = "amq.headers", **)
         | 
| 238 | 
            +
                  exchange(name, type: "headers", **)
         | 
| 239 | 
            +
                end
         | 
| 240 | 
            +
             | 
| 241 | 
            +
                # @deprecated
         | 
| 242 | 
            +
                # @see #headers_exchange
         | 
| 243 | 
            +
                alias headers headers_exchange
         | 
| 244 | 
            +
             | 
| 133 245 | 
             
                # @!endgroup
         | 
| 134 246 | 
             
                # @!group Publish
         | 
| 135 247 |  | 
| 136 248 | 
             
                # Publish a (persistent) message and wait for confirmation
         | 
| 137 | 
            -
                # @param  | 
| 249 | 
            +
                # @param body [Object] The message body
         | 
| 250 | 
            +
                #   will be encoded if any matching codec is found in the client's codec registry
         | 
| 251 | 
            +
                # @param exchange [String] Name of the exchange to publish to
         | 
| 252 | 
            +
                # @param routing_key [String] Routing key for the message
         | 
| 138 253 | 
             
                # @option (see Connection::Channel#basic_publish_confirm)
         | 
| 139 | 
            -
                # @return  | 
| 254 | 
            +
                # @return [nil]
         | 
| 140 255 | 
             
                # @raise (see Connection::Channel#basic_publish_confirm)
         | 
| 141 | 
            -
                 | 
| 256 | 
            +
                # @raise [Error::PublishNotConfirmed] If the message was not confirmed by the broker
         | 
| 257 | 
            +
                # @raise [Error::UnsupportedContentType] If content type is unsupported
         | 
| 258 | 
            +
                # @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
         | 
| 259 | 
            +
                def publish(body, exchange:, routing_key: "", **properties)
         | 
| 142 260 | 
             
                  with_connection do |conn|
         | 
| 143 | 
            -
                    properties  | 
| 144 | 
            -
                     | 
| 261 | 
            +
                    properties[:delivery_mode] ||= 2
         | 
| 262 | 
            +
                    properties = default_content_properties.merge(properties)
         | 
| 263 | 
            +
                    body = serialize_and_encode_body(body, properties)
         | 
| 264 | 
            +
                    result = conn.channel(1).basic_publish_confirm(body, exchange:, routing_key:, **properties)
         | 
| 265 | 
            +
                    raise Error::PublishNotConfirmed unless result
         | 
| 266 | 
            +
             | 
| 267 | 
            +
                    nil
         | 
| 145 268 | 
             
                  end
         | 
| 146 269 | 
             
                end
         | 
| 147 270 |  | 
| @@ -150,10 +273,14 @@ module AMQP | |
| 150 273 | 
             
                # @option (see Connection::Channel#basic_publish)
         | 
| 151 274 | 
             
                # @return (see Connection::Channel#basic_publish)
         | 
| 152 275 | 
             
                # @raise (see Connection::Channel#basic_publish)
         | 
| 153 | 
            -
                 | 
| 276 | 
            +
                # @raise [Error::UnsupportedContentType] If content type is unsupported
         | 
| 277 | 
            +
                # @raise [Error::UnsupportedContentEncoding] If content encoding is unsupported
         | 
| 278 | 
            +
                def publish_and_forget(body, exchange:, routing_key: "", **properties)
         | 
| 154 279 | 
             
                  with_connection do |conn|
         | 
| 155 | 
            -
                    properties  | 
| 156 | 
            -
                     | 
| 280 | 
            +
                    properties[:delivery_mode] ||= 2
         | 
| 281 | 
            +
                    properties = default_content_properties.merge(properties)
         | 
| 282 | 
            +
                    body = serialize_and_encode_body(body, properties)
         | 
| 283 | 
            +
                    conn.channel(1).basic_publish(body, exchange:, routing_key:, **properties)
         | 
| 157 284 | 
             
                  end
         | 
| 158 285 | 
             
                end
         | 
| 159 286 |  | 
| @@ -165,7 +292,6 @@ module AMQP | |
| 165 292 | 
             
                  end
         | 
| 166 293 | 
             
                end
         | 
| 167 294 |  | 
| 168 | 
            -
                # @!endgroup
         | 
| 169 295 | 
             
                # @!group Queue actions
         | 
| 170 296 |  | 
| 171 297 | 
             
                # Consume messages from a queue
         | 
| @@ -173,19 +299,40 @@ module AMQP | |
| 173 299 | 
             
                # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected) (default: false)
         | 
| 174 300 | 
             
                # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false (default: 1)
         | 
| 175 301 | 
             
                # @param worker_threads [Integer] Number of threads processing messages (default: 1)
         | 
| 302 | 
            +
                # @param on_cancel [Proc] Optional proc that will be called if the consumer is cancelled by the broker
         | 
| 303 | 
            +
                #   The proc will be called with the consumer tag as the only argument
         | 
| 176 304 | 
             
                # @param arguments [Hash] Custom arguments to the consumer
         | 
| 177 305 | 
             
                # @yield [Message] Delivered message from the queue
         | 
| 178 | 
            -
                # @return [ | 
| 179 | 
            -
                 | 
| 180 | 
            -
             | 
| 306 | 
            +
                # @return [Consumer] The consumer object, which can be used to cancel the consumer
         | 
| 307 | 
            +
                def subscribe(queue, exclusive: false, no_ack: false, prefetch: 1, worker_threads: 1,
         | 
| 308 | 
            +
                              on_cancel: nil, arguments: {}, &blk)
         | 
| 181 309 | 
             
                  raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
         | 
| 182 310 |  | 
| 183 | 
            -
                  @subscriptions.add? [queue, no_ack, prefetch, worker_threads, arguments, blk]
         | 
| 184 | 
            -
             | 
| 185 311 | 
             
                  with_connection do |conn|
         | 
| 186 312 | 
             
                    ch = conn.channel
         | 
| 187 313 | 
             
                    ch.basic_qos(prefetch)
         | 
| 188 | 
            -
                     | 
| 314 | 
            +
                    consumer_id = @next_consumer_id += 1
         | 
| 315 | 
            +
                    on_cancel_proc = proc do |tag|
         | 
| 316 | 
            +
                      @consumers.delete(consumer_id)
         | 
| 317 | 
            +
                      on_cancel&.call(tag)
         | 
| 318 | 
            +
                    end
         | 
| 319 | 
            +
                    basic_consume_args = { exclusive:, no_ack:, worker_threads:, on_cancel: on_cancel_proc, arguments: }
         | 
| 320 | 
            +
                    consume_ok = ch.basic_consume(queue, **basic_consume_args, &blk)
         | 
| 321 | 
            +
                    consumer = Consumer.new(client: self, channel_id: ch.id, id: consumer_id, block: blk,
         | 
| 322 | 
            +
                                            queue:, consume_ok:, prefetch:, basic_consume_args:)
         | 
| 323 | 
            +
                    @consumers[consumer_id] = consumer
         | 
| 324 | 
            +
                    consumer
         | 
| 325 | 
            +
                  end
         | 
| 326 | 
            +
                end
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                # Get a message from a queue
         | 
| 329 | 
            +
                # @param queue [String] Name of the queue to get the message from
         | 
| 330 | 
            +
                # @param no_ack [Boolean] When false the message has to be manually acknowledged (or rejected) (default: false)
         | 
| 331 | 
            +
                # @return [Message, nil] The message from the queue or nil if the queue is empty
         | 
| 332 | 
            +
                def get(queue, no_ack: false)
         | 
| 333 | 
            +
                  with_connection do |conn|
         | 
| 334 | 
            +
                    ch = conn.channel
         | 
| 335 | 
            +
                    ch.basic_get(queue, no_ack:)
         | 
| 189 336 | 
             
                  end
         | 
| 190 337 | 
             
                end
         | 
| 191 338 |  | 
| @@ -195,9 +342,9 @@ module AMQP | |
| 195 342 | 
             
                # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
         | 
| 196 343 | 
             
                # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 197 344 | 
             
                # @return [nil]
         | 
| 198 | 
            -
                def bind(queue | 
| 345 | 
            +
                def bind(queue:, exchange:, binding_key: "", arguments: {})
         | 
| 199 346 | 
             
                  with_connection do |conn|
         | 
| 200 | 
            -
                    conn.channel(1).queue_bind(queue, exchange | 
| 347 | 
            +
                    conn.channel(1).queue_bind(queue, exchange:, binding_key:, arguments:)
         | 
| 201 348 | 
             
                  end
         | 
| 202 349 | 
             
                end
         | 
| 203 350 |  | 
| @@ -207,9 +354,9 @@ module AMQP | |
| 207 354 | 
             
                # @param binding_key [String] Binding key which the queue is bound to the exchange with
         | 
| 208 355 | 
             
                # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 209 356 | 
             
                # @return [nil]
         | 
| 210 | 
            -
                def unbind(queue | 
| 357 | 
            +
                def unbind(queue:, exchange:, binding_key: "", arguments: {})
         | 
| 211 358 | 
             
                  with_connection do |conn|
         | 
| 212 | 
            -
                    conn.channel(1).queue_unbind(queue, exchange | 
| 359 | 
            +
                    conn.channel(1).queue_unbind(queue, exchange:, binding_key:, arguments:)
         | 
| 213 360 | 
             
                  end
         | 
| 214 361 | 
             
                end
         | 
| 215 362 |  | 
| @@ -229,7 +376,7 @@ module AMQP | |
| 229 376 | 
             
                # @return [Integer] Number of messages in the queue when deleted
         | 
| 230 377 | 
             
                def delete_queue(name, if_unused: false, if_empty: false)
         | 
| 231 378 | 
             
                  with_connection do |conn|
         | 
| 232 | 
            -
                    msgs = conn.channel(1).queue_delete(name, if_unused | 
| 379 | 
            +
                    msgs = conn.channel(1).queue_delete(name, if_unused:, if_empty:)
         | 
| 233 380 | 
             
                    @queues.delete(name)
         | 
| 234 381 | 
             
                    msgs
         | 
| 235 382 | 
             
                  end
         | 
| @@ -239,26 +386,26 @@ module AMQP | |
| 239 386 | 
             
                # @!group Exchange actions
         | 
| 240 387 |  | 
| 241 388 | 
             
                # Bind an exchange to an exchange
         | 
| 242 | 
            -
                # @param destination [String] Name of the exchange to bind
         | 
| 243 389 | 
             
                # @param source [String] Name of the exchange to bind to
         | 
| 390 | 
            +
                # @param destination [String] Name of the exchange to bind
         | 
| 244 391 | 
             
                # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
         | 
| 245 392 | 
             
                # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 246 393 | 
             
                # @return [nil]
         | 
| 247 | 
            -
                def exchange_bind(destination | 
| 394 | 
            +
                def exchange_bind(source:, destination:, binding_key: "", arguments: {})
         | 
| 248 395 | 
             
                  with_connection do |conn|
         | 
| 249 | 
            -
                    conn.channel(1).exchange_bind(destination | 
| 396 | 
            +
                    conn.channel(1).exchange_bind(destination:, source:, binding_key:, arguments:)
         | 
| 250 397 | 
             
                  end
         | 
| 251 398 | 
             
                end
         | 
| 252 399 |  | 
| 253 400 | 
             
                # Unbind an exchange from an exchange
         | 
| 254 | 
            -
                # @param destination [String] Name of the exchange to unbind
         | 
| 255 401 | 
             
                # @param source [String] Name of the exchange to unbind from
         | 
| 402 | 
            +
                # @param destination [String] Name of the exchange to unbind
         | 
| 256 403 | 
             
                # @param binding_key [String] Binding key which the exchange is bound to the exchange with
         | 
| 257 404 | 
             
                # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 258 405 | 
             
                # @return [nil]
         | 
| 259 | 
            -
                def exchange_unbind(destination | 
| 406 | 
            +
                def exchange_unbind(source:, destination:, binding_key: "", arguments: {})
         | 
| 260 407 | 
             
                  with_connection do |conn|
         | 
| 261 | 
            -
                    conn.channel(1).exchange_unbind(destination | 
| 408 | 
            +
                    conn.channel(1).exchange_unbind(destination:, source:, binding_key:, arguments:)
         | 
| 262 409 | 
             
                  end
         | 
| 263 410 | 
             
                end
         | 
| 264 411 |  | 
| @@ -274,9 +421,122 @@ module AMQP | |
| 274 421 | 
             
                end
         | 
| 275 422 |  | 
| 276 423 | 
             
                # @!endgroup
         | 
| 424 | 
            +
                # @!group RPC
         | 
| 277 425 |  | 
| 278 | 
            -
                 | 
| 426 | 
            +
                # Create a RPC server for a single method/function/procedure
         | 
| 427 | 
            +
                # @param method [String, Symbol] name of the RPC method to host (i.e. queue name on the server side)
         | 
| 428 | 
            +
                # @param worker_threads [Integer] number of threads that process requests
         | 
| 429 | 
            +
                # @param durable [Boolean] If true the queue will survive broker restarts
         | 
| 430 | 
            +
                # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
         | 
| 431 | 
            +
                #   (it won't be deleted until at least one consumer has consumed from it)
         | 
| 432 | 
            +
                # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
         | 
| 433 | 
            +
                # @yield Block that processes the RPC request messages
         | 
| 434 | 
            +
                # @yieldparam [String] The body of the request message
         | 
| 435 | 
            +
                # @yieldreturn [String] The response message body
         | 
| 436 | 
            +
                # @return (see #subscribe)
         | 
| 437 | 
            +
                def rpc_server(method, worker_threads: 1, durable: true, auto_delete: false, arguments: {}, &_)
         | 
| 438 | 
            +
                  queue(method.to_s, durable:, auto_delete:, arguments:)
         | 
| 439 | 
            +
                    .subscribe(prefetch: worker_threads, worker_threads:) do |msg|
         | 
| 440 | 
            +
                      result = yield msg.parse
         | 
| 441 | 
            +
                      properties = { content_type: msg.properties.content_type,
         | 
| 442 | 
            +
                                     content_encoding: msg.properties.content_encoding }
         | 
| 443 | 
            +
                      result_body = serialize_and_encode_body(result, properties)
         | 
| 444 | 
            +
             | 
| 445 | 
            +
                      msg.channel.basic_publish(result_body, exchange: "", routing_key: msg.properties.reply_to,
         | 
| 446 | 
            +
                                                             correlation_id: msg.properties.correlation_id, **properties)
         | 
| 447 | 
            +
                      msg.ack
         | 
| 448 | 
            +
                    rescue StandardError
         | 
| 449 | 
            +
                      msg.reject(requeue: false)
         | 
| 450 | 
            +
                      raise
         | 
| 451 | 
            +
                    end
         | 
| 452 | 
            +
                end
         | 
| 453 | 
            +
             | 
| 454 | 
            +
                # Do a RPC call, sends a messages, waits for a response
         | 
| 455 | 
            +
                # @param method [String, Symbol] name of the RPC method to call (i.e. queue name on the server side)
         | 
| 456 | 
            +
                # @param arguments [String] arguments/body to the call
         | 
| 457 | 
            +
                # @param timeout [Numeric, nil] Number of seconds to wait for a response
         | 
| 458 | 
            +
                # @option (see Client#publish)
         | 
| 459 | 
            +
                # @return [String] Returns the result from the call
         | 
| 460 | 
            +
                # @raise [Timeout::Error] if no response is received within the timeout period
         | 
| 461 | 
            +
                def rpc_call(method, arguments, timeout: nil, **properties)
         | 
| 462 | 
            +
                  ch = with_connection(&:channel)
         | 
| 463 | 
            +
                  begin
         | 
| 464 | 
            +
                    msg = ch.basic_consume_once("amq.rabbitmq.reply-to", timeout:) do
         | 
| 465 | 
            +
                      properties = default_content_properties.merge(properties)
         | 
| 466 | 
            +
                      body = serialize_and_encode_body(arguments, properties)
         | 
| 467 | 
            +
                      ch.basic_publish(body, exchange: "", routing_key: method.to_s,
         | 
| 468 | 
            +
                                             reply_to: "amq.rabbitmq.reply-to", **properties)
         | 
| 469 | 
            +
                    end
         | 
| 470 | 
            +
                    msg.parse
         | 
| 471 | 
            +
                  ensure
         | 
| 472 | 
            +
                    ch.close
         | 
| 473 | 
            +
                  end
         | 
| 474 | 
            +
                end
         | 
| 475 | 
            +
             | 
| 476 | 
            +
                # Create a reusable RPC client
         | 
| 477 | 
            +
                # @return [RPCClient]
         | 
| 478 | 
            +
                def rpc_client
         | 
| 479 | 
            +
                  ch = with_connection(&:channel)
         | 
| 480 | 
            +
                  RPCClient.new(ch).start
         | 
| 481 | 
            +
                end
         | 
| 482 | 
            +
             | 
| 483 | 
            +
                # @!endgroup
         | 
| 484 | 
            +
                # @!group Message coding
         | 
| 485 | 
            +
             | 
| 486 | 
            +
                class << self
         | 
| 487 | 
            +
                  # Configure the AMQP::Client class-level settings
         | 
| 488 | 
            +
                  # @yield [Configuration] Yields the configuration object for modification
         | 
| 489 | 
            +
                  # @return [Configuration] The configuration object
         | 
| 490 | 
            +
                  # @example
         | 
| 491 | 
            +
                  #   AMQP::Client.configure do |config|
         | 
| 492 | 
            +
                  #     config.default_content_type = "application/json"
         | 
| 493 | 
            +
                  #     config.strict_coding = true
         | 
| 494 | 
            +
                  #   end
         | 
| 495 | 
            +
                  def configure
         | 
| 496 | 
            +
                    yield @config if block_given?
         | 
| 497 | 
            +
                    @config
         | 
| 498 | 
            +
                  end
         | 
| 499 | 
            +
             | 
| 500 | 
            +
                  # Get the class-level configuration
         | 
| 501 | 
            +
                  # @return [Configuration]
         | 
| 502 | 
            +
                  attr_reader :config
         | 
| 503 | 
            +
             | 
| 504 | 
            +
                  # Get the class-level codec registry
         | 
| 505 | 
            +
                  # @return [MessageCodecRegistry]
         | 
| 506 | 
            +
                  attr_reader :codec_registry
         | 
| 507 | 
            +
             | 
| 508 | 
            +
                  # We need to set the subclass's configuration and codec registry
         | 
| 509 | 
            +
                  # because these are class instance variables, hence not inherited.
         | 
| 510 | 
            +
                  # @api private
         | 
| 511 | 
            +
                  def inherited(subclass)
         | 
| 512 | 
            +
                    super
         | 
| 513 | 
            +
                    subclass_codec_registry = @codec_registry.dup
         | 
| 514 | 
            +
                    subclass.instance_variable_set(:@codec_registry, subclass_codec_registry)
         | 
| 515 | 
            +
                    subclass.instance_variable_set(:@config, Configuration.new(subclass_codec_registry))
         | 
| 516 | 
            +
                    # Copy configuration settings from parent
         | 
| 517 | 
            +
                    subclass.config.strict_coding = @config.strict_coding
         | 
| 518 | 
            +
                    subclass.config.default_content_type = @config.default_content_type
         | 
| 519 | 
            +
                    subclass.config.default_content_encoding = @config.default_content_encoding
         | 
| 520 | 
            +
                  end
         | 
| 521 | 
            +
                end
         | 
| 522 | 
            +
             | 
| 523 | 
            +
                # Get the codec registry for this instance
         | 
| 524 | 
            +
                # @return [MessageCodecRegistry]
         | 
| 525 | 
            +
                attr_reader :codec_registry
         | 
| 526 | 
            +
             | 
| 527 | 
            +
                # Get/set if condig should be strict, i.e. if the client should raise on unknown codecs
         | 
| 528 | 
            +
                attr_accessor :strict_coding
         | 
| 529 | 
            +
             | 
| 530 | 
            +
                # Get/set the default content_type to use when publishing messages
         | 
| 531 | 
            +
                # @return [String, nil]
         | 
| 532 | 
            +
                attr_accessor :default_content_type
         | 
| 279 533 |  | 
| 534 | 
            +
                # Get/set the default content_encoding to use when publishing messages
         | 
| 535 | 
            +
                # @return [String, nil]
         | 
| 536 | 
            +
                attr_accessor :default_content_encoding
         | 
| 537 | 
            +
             | 
| 538 | 
            +
                # @!endgroup
         | 
| 539 | 
            +
                #
         | 
| 280 540 | 
             
                def with_connection
         | 
| 281 541 | 
             
                  conn = nil
         | 
| 282 542 | 
             
                  loop do
         | 
| @@ -291,5 +551,53 @@ module AMQP | |
| 291 551 | 
             
                    @connq << conn unless conn.closed?
         | 
| 292 552 | 
             
                  end
         | 
| 293 553 | 
             
                end
         | 
| 554 | 
            +
             | 
| 555 | 
            +
                # @api private
         | 
| 556 | 
            +
                def cancel_consumer(consumer)
         | 
| 557 | 
            +
                  @consumers.delete(consumer.id)
         | 
| 558 | 
            +
                  with_connection do |conn|
         | 
| 559 | 
            +
                    conn.channel(consumer.channel_id).basic_cancel(consumer.tag)
         | 
| 560 | 
            +
                  end
         | 
| 561 | 
            +
                end
         | 
| 562 | 
            +
             | 
| 563 | 
            +
                private
         | 
| 564 | 
            +
             | 
| 565 | 
            +
                def default_content_properties
         | 
| 566 | 
            +
                  {
         | 
| 567 | 
            +
                    content_type: @default_content_type,
         | 
| 568 | 
            +
                    content_encoding: @default_content_encoding
         | 
| 569 | 
            +
                  }.compact
         | 
| 570 | 
            +
                end
         | 
| 571 | 
            +
             | 
| 572 | 
            +
                def serialize_and_encode_body(body, properties)
         | 
| 573 | 
            +
                  body = serialize_body(body, properties)
         | 
| 574 | 
            +
                  encode_body(body, properties)
         | 
| 575 | 
            +
                end
         | 
| 576 | 
            +
             | 
| 577 | 
            +
                def encode_body(body, properties)
         | 
| 578 | 
            +
                  ce = properties[:content_encoding]
         | 
| 579 | 
            +
                  coder = @codec_registry.find_coder(ce)
         | 
| 580 | 
            +
             | 
| 581 | 
            +
                  return coder.encode(body, properties) if coder
         | 
| 582 | 
            +
             | 
| 583 | 
            +
                  is_unsupported = ce && ce != ""
         | 
| 584 | 
            +
                  raise Error::UnsupportedContentEncoding, ce if is_unsupported && @strict_coding
         | 
| 585 | 
            +
             | 
| 586 | 
            +
                  body
         | 
| 587 | 
            +
                end
         | 
| 588 | 
            +
             | 
| 589 | 
            +
                def serialize_body(body, properties)
         | 
| 590 | 
            +
                  return body if body.is_a?(String)
         | 
| 591 | 
            +
             | 
| 592 | 
            +
                  ct = properties[:content_type]
         | 
| 593 | 
            +
                  parser = @codec_registry.find_parser(ct)
         | 
| 594 | 
            +
             | 
| 595 | 
            +
                  return parser.serialize(body, properties) if parser
         | 
| 596 | 
            +
             | 
| 597 | 
            +
                  is_unsupported = ct && ct != "" && ct != "text/plain"
         | 
| 598 | 
            +
                  raise Error::UnsupportedContentType, ct if is_unsupported && @strict_coding
         | 
| 599 | 
            +
             | 
| 600 | 
            +
                  body.to_s
         | 
| 601 | 
            +
                end
         | 
| 294 602 | 
             
              end
         | 
| 295 603 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,11 +1,11 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: amqp-client
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version:  | 
| 4 | 
            +
              version: 2.0.0
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - CloudAMQP
         | 
| 8 | 
            -
            bindir:  | 
| 8 | 
            +
            bindir: bin
         | 
| 9 9 | 
             
            cert_chain: []
         | 
| 10 10 | 
             
            date: 1980-01-02 00:00:00.000000000 Z
         | 
| 11 11 | 
             
            dependencies: []
         | 
| @@ -16,33 +16,22 @@ executables: [] | |
| 16 16 | 
             
            extensions: []
         | 
| 17 17 | 
             
            extra_rdoc_files: []
         | 
| 18 18 | 
             
            files:
         | 
| 19 | 
            -
            - ".github/workflows/codeql-analysis.yml"
         | 
| 20 | 
            -
            - ".github/workflows/docs.yml"
         | 
| 21 | 
            -
            - ".github/workflows/main.yml"
         | 
| 22 | 
            -
            - ".github/workflows/release.yml"
         | 
| 23 | 
            -
            - ".gitignore"
         | 
| 24 | 
            -
            - ".rubocop.yml"
         | 
| 25 | 
            -
            - ".rubocop_todo.yml"
         | 
| 26 | 
            -
            - ".yardopts"
         | 
| 27 | 
            -
            - CHANGELOG.md
         | 
| 28 | 
            -
            - CODEOWNERS
         | 
| 29 | 
            -
            - Gemfile
         | 
| 30 19 | 
             
            - LICENSE.txt
         | 
| 31 | 
            -
            - README.md
         | 
| 32 | 
            -
            - Rakefile
         | 
| 33 | 
            -
            - amqp-client.gemspec
         | 
| 34 | 
            -
            - bin/console
         | 
| 35 | 
            -
            - bin/setup
         | 
| 36 20 | 
             
            - lib/amqp-client.rb
         | 
| 37 21 | 
             
            - lib/amqp/client.rb
         | 
| 38 22 | 
             
            - lib/amqp/client/channel.rb
         | 
| 23 | 
            +
            - lib/amqp/client/configuration.rb
         | 
| 39 24 | 
             
            - lib/amqp/client/connection.rb
         | 
| 25 | 
            +
            - lib/amqp/client/consumer.rb
         | 
| 40 26 | 
             
            - lib/amqp/client/errors.rb
         | 
| 41 27 | 
             
            - lib/amqp/client/exchange.rb
         | 
| 42 28 | 
             
            - lib/amqp/client/frame_bytes.rb
         | 
| 43 29 | 
             
            - lib/amqp/client/message.rb
         | 
| 30 | 
            +
            - lib/amqp/client/message_codec_registry.rb
         | 
| 31 | 
            +
            - lib/amqp/client/message_codecs.rb
         | 
| 44 32 | 
             
            - lib/amqp/client/properties.rb
         | 
| 45 33 | 
             
            - lib/amqp/client/queue.rb
         | 
| 34 | 
            +
            - lib/amqp/client/rpc_client.rb
         | 
| 46 35 | 
             
            - lib/amqp/client/table.rb
         | 
| 47 36 | 
             
            - lib/amqp/client/version.rb
         | 
| 48 37 | 
             
            homepage: https://github.com/cloudamqp/amqp-client.rb
         | 
| @@ -52,6 +41,7 @@ metadata: | |
| 52 41 | 
             
              homepage_uri: https://github.com/cloudamqp/amqp-client.rb
         | 
| 53 42 | 
             
              source_code_uri: https://github.com/cloudamqp/amqp-client.rb.git
         | 
| 54 43 | 
             
              changelog_uri: https://github.com/cloudamqp/amqp-client.rb/blob/main/CHANGELOG.md
         | 
| 44 | 
            +
              rubygems_mfa_required: 'true'
         | 
| 55 45 | 
             
            rdoc_options: []
         | 
| 56 46 | 
             
            require_paths:
         | 
| 57 47 | 
             
            - lib
         |