amqp-client 1.2.1 → 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 +24 -26
- 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 +48 -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 +343 -79
- metadata +8 -18
- data/.github/workflows/codeql-analysis.yml +0 -41
- data/.github/workflows/docs.yml +0 -28
- data/.github/workflows/main.yml +0 -147
- data/.github/workflows/release.yml +0 -54
- data/.gitignore +0 -9
- data/.rubocop.yml +0 -30
- data/.rubocop_todo.yml +0 -65
- data/.yardopts +0 -1
- data/CHANGELOG.md +0 -115
- data/CODEOWNERS +0 -1
- data/Gemfile +0 -18
- data/README.md +0 -193
- data/Rakefile +0 -197
- 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,38 +67,53 @@ 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
         | 
| 86 119 | 
             
                  return unless @connq.size.positive?
         | 
| @@ -90,6 +123,12 @@ module AMQP | |
| 90 123 | 
             
                  nil
         | 
| 91 124 | 
             
                end
         | 
| 92 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 | 
            +
             | 
| 93 132 | 
             
                # @!endgroup
         | 
| 94 133 | 
             
                # @!group High level objects
         | 
| 95 134 |  | 
| @@ -99,18 +138,20 @@ module AMQP | |
| 99 138 | 
             
                #   messages in the queue will only survive if they are published as persistent
         | 
| 100 139 | 
             
                # @param auto_delete [Boolean] If true the queue will be deleted when the last consumer stops consuming
         | 
| 101 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
         | 
| 102 143 | 
             
                # @param arguments [Hash] Custom arguments, such as queue-ttl etc.
         | 
| 103 144 | 
             
                # @return [Queue]
         | 
| 104 145 | 
             
                # @example
         | 
| 105 146 | 
             
                #   amqp = AMQP::Client.new.start
         | 
| 106 147 | 
             
                #   q = amqp.queue("foobar")
         | 
| 107 148 | 
             
                #   q.publish("body")
         | 
| 108 | 
            -
                def queue(name, durable: true, auto_delete: false, arguments: {})
         | 
| 149 | 
            +
                def queue(name, durable: true, auto_delete: false, exclusive: false, passive: false, arguments: {})
         | 
| 109 150 | 
             
                  raise ArgumentError, "Currently only supports named, durable queues" if name.empty?
         | 
| 110 151 |  | 
| 111 152 | 
             
                  @queues.fetch(name) do
         | 
| 112 153 | 
             
                    with_connection do |conn|
         | 
| 113 | 
            -
                      conn.channel(1).queue_declare(name, durable | 
| 154 | 
            +
                      conn.channel(1).queue_declare(name, durable:, auto_delete:, exclusive:, passive:, arguments:)
         | 
| 114 155 | 
             
                    end
         | 
| 115 156 | 
             
                    @queues[name] = Queue.new(self, name)
         | 
| 116 157 | 
             
                  end
         | 
| @@ -126,66 +167,104 @@ module AMQP | |
| 126 167 | 
             
                # @return [Exchange]
         | 
| 127 168 | 
             
                # @example
         | 
| 128 169 | 
             
                #   amqp = AMQP::Client.new.start
         | 
| 129 | 
            -
                #   x = amqp.exchange("my.hash.exchange", "x-consistent-hash")
         | 
| 130 | 
            -
                #   x.publish("body", "routing-key")
         | 
| 131 | 
            -
                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: {})
         | 
| 132 173 | 
             
                  @exchanges.fetch(name) do
         | 
| 133 174 | 
             
                    with_connection do |conn|
         | 
| 134 | 
            -
                      conn.channel(1).exchange_declare(name, type | 
| 135 | 
            -
                                                                   internal: internal, arguments: arguments)
         | 
| 175 | 
            +
                      conn.channel(1).exchange_declare(name, type:, durable:, auto_delete:, internal:, arguments:)
         | 
| 136 176 | 
             
                    end
         | 
| 137 177 | 
             
                    @exchanges[name] = Exchange.new(self, name)
         | 
| 138 178 | 
             
                  end
         | 
| 139 179 | 
             
                end
         | 
| 140 180 |  | 
| 141 | 
            -
                # Declare a fanout exchange and return a high level Exchange object
         | 
| 142 | 
            -
                # @param name [String] Name of the exchange (defaults to "amq.fanout")
         | 
| 143 | 
            -
                # @see {#exchange} for other parameters
         | 
| 144 | 
            -
                # @return [Exchange]
         | 
| 145 | 
            -
                def fanout(name = "amq.fanout", **kwargs)
         | 
| 146 | 
            -
                  exchange(name, "fanout", **kwargs)
         | 
| 147 | 
            -
                end
         | 
| 148 | 
            -
             | 
| 149 181 | 
             
                # Declare a direct exchange and return a high level Exchange object
         | 
| 150 | 
            -
                # @param name [String] Name of the exchange (defaults to "" | 
| 151 | 
            -
                # @see  | 
| 182 | 
            +
                # @param name [String] Name of the exchange (defaults to "amq.direct")
         | 
| 183 | 
            +
                # @see #exchange for other parameters
         | 
| 152 184 | 
             
                # @return [Exchange]
         | 
| 153 | 
            -
                def  | 
| 154 | 
            -
                  return exchange(name, "direct", ** | 
| 185 | 
            +
                def direct_exchange(name = "amq.direct", **)
         | 
| 186 | 
            +
                  return exchange(name, type: "direct", **) unless name.empty?
         | 
| 155 187 |  | 
| 188 | 
            +
                  # Return the default exchange
         | 
| 156 189 | 
             
                  @exchanges.fetch(name) do
         | 
| 157 190 | 
             
                    @exchanges[name] = Exchange.new(self, name)
         | 
| 158 191 | 
             
                  end
         | 
| 159 192 | 
             
                end
         | 
| 160 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 | 
            +
             | 
| 161 221 | 
             
                # Declare a topic exchange and return a high level Exchange object
         | 
| 162 222 | 
             
                # @param name [String] Name of the exchange (defaults to "amq.topic")
         | 
| 163 | 
            -
                # @see  | 
| 223 | 
            +
                # @see #exchange for other parameters
         | 
| 164 224 | 
             
                # @return [Exchange]
         | 
| 165 | 
            -
                def  | 
| 166 | 
            -
                  exchange(name, "topic", ** | 
| 225 | 
            +
                def topic_exchange(name = "amq.topic", **)
         | 
| 226 | 
            +
                  exchange(name, type: "topic", **)
         | 
| 167 227 | 
             
                end
         | 
| 168 228 |  | 
| 229 | 
            +
                # @deprecated
         | 
| 230 | 
            +
                # @see #topic_exchange
         | 
| 231 | 
            +
                alias topic topic_exchange
         | 
| 232 | 
            +
             | 
| 169 233 | 
             
                # Declare a headers exchange and return a high level Exchange object
         | 
| 170 234 | 
             
                # @param name [String] Name of the exchange (defaults to "amq.headers")
         | 
| 171 | 
            -
                # @see  | 
| 235 | 
            +
                # @see #exchange for other parameters
         | 
| 172 236 | 
             
                # @return [Exchange]
         | 
| 173 | 
            -
                def  | 
| 174 | 
            -
                  exchange(name, "headers", ** | 
| 237 | 
            +
                def headers_exchange(name = "amq.headers", **)
         | 
| 238 | 
            +
                  exchange(name, type: "headers", **)
         | 
| 175 239 | 
             
                end
         | 
| 176 240 |  | 
| 241 | 
            +
                # @deprecated
         | 
| 242 | 
            +
                # @see #headers_exchange
         | 
| 243 | 
            +
                alias headers headers_exchange
         | 
| 244 | 
            +
             | 
| 177 245 | 
             
                # @!endgroup
         | 
| 178 246 | 
             
                # @!group Publish
         | 
| 179 247 |  | 
| 180 248 | 
             
                # Publish a (persistent) message and wait for confirmation
         | 
| 181 | 
            -
                # @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
         | 
| 182 253 | 
             
                # @option (see Connection::Channel#basic_publish_confirm)
         | 
| 183 | 
            -
                # @return  | 
| 254 | 
            +
                # @return [nil]
         | 
| 184 255 | 
             
                # @raise (see Connection::Channel#basic_publish_confirm)
         | 
| 185 | 
            -
                 | 
| 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)
         | 
| 186 260 | 
             
                  with_connection do |conn|
         | 
| 187 | 
            -
                    properties  | 
| 188 | 
            -
                     | 
| 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
         | 
| 189 268 | 
             
                  end
         | 
| 190 269 | 
             
                end
         | 
| 191 270 |  | 
| @@ -194,10 +273,14 @@ module AMQP | |
| 194 273 | 
             
                # @option (see Connection::Channel#basic_publish)
         | 
| 195 274 | 
             
                # @return (see Connection::Channel#basic_publish)
         | 
| 196 275 | 
             
                # @raise (see Connection::Channel#basic_publish)
         | 
| 197 | 
            -
                 | 
| 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)
         | 
| 198 279 | 
             
                  with_connection do |conn|
         | 
| 199 | 
            -
                    properties  | 
| 200 | 
            -
                     | 
| 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)
         | 
| 201 284 | 
             
                  end
         | 
| 202 285 | 
             
                end
         | 
| 203 286 |  | 
| @@ -209,7 +292,6 @@ module AMQP | |
| 209 292 | 
             
                  end
         | 
| 210 293 | 
             
                end
         | 
| 211 294 |  | 
| 212 | 
            -
                # @!endgroup
         | 
| 213 295 | 
             
                # @!group Queue actions
         | 
| 214 296 |  | 
| 215 297 | 
             
                # Consume messages from a queue
         | 
| @@ -217,19 +299,40 @@ module AMQP | |
| 217 299 | 
             
                # @param no_ack [Boolean] When false messages have to be manually acknowledged (or rejected) (default: false)
         | 
| 218 300 | 
             
                # @param prefetch [Integer] Specify how many messages to prefetch for consumers with no_ack is false (default: 1)
         | 
| 219 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
         | 
| 220 304 | 
             
                # @param arguments [Hash] Custom arguments to the consumer
         | 
| 221 305 | 
             
                # @yield [Message] Delivered message from the queue
         | 
| 222 | 
            -
                # @return [ | 
| 223 | 
            -
                 | 
| 224 | 
            -
             | 
| 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)
         | 
| 225 309 | 
             
                  raise ArgumentError, "worker_threads have to be > 0" if worker_threads <= 0
         | 
| 226 310 |  | 
| 227 | 
            -
                  @subscriptions.add? [queue, no_ack, prefetch, worker_threads, arguments, blk]
         | 
| 228 | 
            -
             | 
| 229 311 | 
             
                  with_connection do |conn|
         | 
| 230 312 | 
             
                    ch = conn.channel
         | 
| 231 313 | 
             
                    ch.basic_qos(prefetch)
         | 
| 232 | 
            -
                     | 
| 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:)
         | 
| 233 336 | 
             
                  end
         | 
| 234 337 | 
             
                end
         | 
| 235 338 |  | 
| @@ -239,9 +342,9 @@ module AMQP | |
| 239 342 | 
             
                # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
         | 
| 240 343 | 
             
                # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 241 344 | 
             
                # @return [nil]
         | 
| 242 | 
            -
                def bind(queue | 
| 345 | 
            +
                def bind(queue:, exchange:, binding_key: "", arguments: {})
         | 
| 243 346 | 
             
                  with_connection do |conn|
         | 
| 244 | 
            -
                    conn.channel(1).queue_bind(queue, exchange | 
| 347 | 
            +
                    conn.channel(1).queue_bind(queue, exchange:, binding_key:, arguments:)
         | 
| 245 348 | 
             
                  end
         | 
| 246 349 | 
             
                end
         | 
| 247 350 |  | 
| @@ -251,9 +354,9 @@ module AMQP | |
| 251 354 | 
             
                # @param binding_key [String] Binding key which the queue is bound to the exchange with
         | 
| 252 355 | 
             
                # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 253 356 | 
             
                # @return [nil]
         | 
| 254 | 
            -
                def unbind(queue | 
| 357 | 
            +
                def unbind(queue:, exchange:, binding_key: "", arguments: {})
         | 
| 255 358 | 
             
                  with_connection do |conn|
         | 
| 256 | 
            -
                    conn.channel(1).queue_unbind(queue, exchange | 
| 359 | 
            +
                    conn.channel(1).queue_unbind(queue, exchange:, binding_key:, arguments:)
         | 
| 257 360 | 
             
                  end
         | 
| 258 361 | 
             
                end
         | 
| 259 362 |  | 
| @@ -273,7 +376,7 @@ module AMQP | |
| 273 376 | 
             
                # @return [Integer] Number of messages in the queue when deleted
         | 
| 274 377 | 
             
                def delete_queue(name, if_unused: false, if_empty: false)
         | 
| 275 378 | 
             
                  with_connection do |conn|
         | 
| 276 | 
            -
                    msgs = conn.channel(1).queue_delete(name, if_unused | 
| 379 | 
            +
                    msgs = conn.channel(1).queue_delete(name, if_unused:, if_empty:)
         | 
| 277 380 | 
             
                    @queues.delete(name)
         | 
| 278 381 | 
             
                    msgs
         | 
| 279 382 | 
             
                  end
         | 
| @@ -283,26 +386,26 @@ module AMQP | |
| 283 386 | 
             
                # @!group Exchange actions
         | 
| 284 387 |  | 
| 285 388 | 
             
                # Bind an exchange to an exchange
         | 
| 286 | 
            -
                # @param destination [String] Name of the exchange to bind
         | 
| 287 389 | 
             
                # @param source [String] Name of the exchange to bind to
         | 
| 390 | 
            +
                # @param destination [String] Name of the exchange to bind
         | 
| 288 391 | 
             
                # @param binding_key [String] Binding key on which messages that match might be routed (depending on exchange type)
         | 
| 289 392 | 
             
                # @param arguments [Hash] Message headers to match on (only relevant for header exchanges)
         | 
| 290 393 | 
             
                # @return [nil]
         | 
| 291 | 
            -
                def exchange_bind(destination | 
| 394 | 
            +
                def exchange_bind(source:, destination:, binding_key: "", arguments: {})
         | 
| 292 395 | 
             
                  with_connection do |conn|
         | 
| 293 | 
            -
                    conn.channel(1).exchange_bind(destination | 
| 396 | 
            +
                    conn.channel(1).exchange_bind(destination:, source:, binding_key:, arguments:)
         | 
| 294 397 | 
             
                  end
         | 
| 295 398 | 
             
                end
         | 
| 296 399 |  | 
| 297 400 | 
             
                # Unbind an exchange from an exchange
         | 
| 298 | 
            -
                # @param destination [String] Name of the exchange to unbind
         | 
| 299 401 | 
             
                # @param source [String] Name of the exchange to unbind from
         | 
| 402 | 
            +
                # @param destination [String] Name of the exchange to unbind
         | 
| 300 403 | 
             
                # @param binding_key [String] Binding key which the exchange is bound to the exchange with
         | 
| 301 404 | 
             
                # @param arguments [Hash] Arguments matching the binding that's being removed
         | 
| 302 405 | 
             
                # @return [nil]
         | 
| 303 | 
            -
                def exchange_unbind(destination | 
| 406 | 
            +
                def exchange_unbind(source:, destination:, binding_key: "", arguments: {})
         | 
| 304 407 | 
             
                  with_connection do |conn|
         | 
| 305 | 
            -
                    conn.channel(1).exchange_unbind(destination | 
| 408 | 
            +
                    conn.channel(1).exchange_unbind(destination:, source:, binding_key:, arguments:)
         | 
| 306 409 | 
             
                  end
         | 
| 307 410 | 
             
                end
         | 
| 308 411 |  | 
| @@ -318,9 +421,122 @@ module AMQP | |
| 318 421 | 
             
                end
         | 
| 319 422 |  | 
| 320 423 | 
             
                # @!endgroup
         | 
| 424 | 
            +
                # @!group RPC
         | 
| 321 425 |  | 
| 322 | 
            -
                 | 
| 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
         | 
| 323 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
         | 
| 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 | 
            +
                #
         | 
| 324 540 | 
             
                def with_connection
         | 
| 325 541 | 
             
                  conn = nil
         | 
| 326 542 | 
             
                  loop do
         | 
| @@ -335,5 +551,53 @@ module AMQP | |
| 335 551 | 
             
                    @connq << conn unless conn.closed?
         | 
| 336 552 | 
             
                  end
         | 
| 337 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
         | 
| 338 602 | 
             
              end
         | 
| 339 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
         |