async-rabbitmq 0.4.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.
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Optional OpenTelemetry tracing. Not loaded by default and not a dependency of
4
+ # the gem: require it yourself, with opentelemetry-api in your bundle.
5
+ #
6
+ # require "async_rabbitmq/telemetry/open_telemetry"
7
+ # AsyncRabbitMQ::Telemetry::OpenTelemetry.install
8
+ #
9
+ # The spans and attributes match opentelemetry-instrumentation-bunny, so a
10
+ # service moving over from Bunny keeps the traces and dashboards it had:
11
+ #
12
+ # * publishing produces a PRODUCER span "<exchange>.<routing key> publish" and
13
+ # injects the W3C trace context into the message headers,
14
+ # * basic_get produces a CONSUMER span named for the queue, then renamed to
15
+ # "<exchange>.<routing key> receive" once the delivery arrives,
16
+ # * a consumer handler runs inside a CONSUMER span
17
+ # "<exchange>.<routing key> process", parented by the context extracted from
18
+ # the message headers, so a trace crosses the broker.
19
+ #
20
+ # One difference from Bunny, forced by the architecture: Bunny creates a receive
21
+ # span for a pushed delivery and parents the process span to it, linking to the
22
+ # producer. Here a pushed delivery goes straight to the handler, so the process
23
+ # span is parented to the producer context directly. Traces join up the same way,
24
+ # with one span per delivery instead of two.
25
+ #
26
+ # Publishing from several fibers on one channel is safe here, and each publish
27
+ # gets its own span: the context is captured when basic_publish is called, not
28
+ # when the frames reach the wire.
29
+
30
+ require "opentelemetry"
31
+
32
+ require_relative "../../async_rabbitmq"
33
+
34
+ module AsyncRabbitMQ
35
+ module Telemetry
36
+ module OpenTelemetry
37
+ TRACER_NAME = "async-rabbitmq"
38
+ PROTOCOL = "AMQP"
39
+ PROTOCOL_VERSION = "0.9.1"
40
+ SYSTEM = "rabbitmq"
41
+
42
+ class << self
43
+ attr_reader :tracer
44
+
45
+ # Start tracing. Safe to call more than once: the patch is applied once
46
+ # and does nothing until a tracer is set.
47
+ def install(tracer_provider: ::OpenTelemetry.tracer_provider,
48
+ tracer_name: TRACER_NAME,
49
+ tracer_version: AsyncRabbitMQ::VERSION)
50
+ @tracer = tracer_provider.tracer(tracer_name, tracer_version)
51
+ AsyncRabbitMQ::Channel.prepend(ChannelSpans) unless AsyncRabbitMQ::Channel.include?(ChannelSpans)
52
+ true
53
+ end
54
+
55
+ # Stop tracing. The prepended module stays in the ancestor chain and
56
+ # falls straight through to the unpatched methods.
57
+ def uninstall
58
+ @tracer = nil
59
+ true
60
+ end
61
+
62
+ def installed?
63
+ !@tracer.nil?
64
+ end
65
+ end
66
+
67
+ # Attribute and name construction. Pure functions: everything they need is
68
+ # passed in, so the publish, get and consume paths cannot drift apart.
69
+ module Helpers
70
+ module_function
71
+
72
+ # Bunny joins the two with a dot and drops whichever is missing.
73
+ def destination(exchange, routing_key)
74
+ [exchange, routing_key].reject { |part| part.nil? || part.to_s.empty? }.join(".")
75
+ end
76
+
77
+ # The default exchange is direct, and a direct exchange addresses one
78
+ # queue; everything else is a topic in OpenTelemetry's model. The type is
79
+ # only known for exchanges this client declared.
80
+ def destination_kind(exchange, exchange_type)
81
+ return "queue" if exchange.nil? || exchange.to_s.empty?
82
+ return "queue" if exchange_type.to_s == "direct"
83
+
84
+ "topic"
85
+ end
86
+
87
+ def basic_attributes(exchange:, routing_key:, peer_host:, peer_port:, exchange_type: nil)
88
+ attributes = {
89
+ "messaging.system" => SYSTEM,
90
+ "messaging.destination" => exchange.to_s,
91
+ "messaging.destination_kind" => destination_kind(exchange, exchange_type),
92
+ "messaging.protocol" => PROTOCOL,
93
+ "messaging.protocol_version" => PROTOCOL_VERSION,
94
+ "net.peer.name" => peer_host,
95
+ "net.peer.port" => peer_port
96
+ }
97
+ attributes["messaging.rabbitmq.routing_key"] = routing_key if routing_key && !routing_key.to_s.empty?
98
+ attributes
99
+ end
100
+
101
+ def headers_of(header)
102
+ return {} unless header.respond_to?(:properties) && header.properties
103
+
104
+ header.properties[:headers] || {}
105
+ end
106
+
107
+ def message_id_of(header)
108
+ return nil unless header.respond_to?(:properties) && header.properties
109
+
110
+ header.properties[:message_id]
111
+ end
112
+ end
113
+
114
+ # Prepended to AsyncRabbitMQ::Channel by .install.
115
+ module ChannelSpans
116
+ def basic_publish(payload, exchange: "", routing_key: "", **opts)
117
+ tracer = OpenTelemetry.tracer
118
+ return super unless tracer
119
+
120
+ tracer.in_span("#{Helpers.destination(exchange, routing_key)} publish",
121
+ attributes: otel_attributes(exchange, routing_key), kind: :producer) do
122
+ # Copy: inject writes traceparent into this hash, and opts[:headers]
123
+ # is the caller's own object — a frozen constant would raise, and a
124
+ # hash shared between fibers would race.
125
+ headers = (opts[:headers] || {}).dup
126
+ ::OpenTelemetry.propagation.inject(headers)
127
+ super(payload, exchange: exchange, routing_key: routing_key, **opts.merge(headers: headers))
128
+ end
129
+ end
130
+
131
+ def basic_publish_batch(payloads, exchange: "", routing_key: "", **opts)
132
+ tracer = OpenTelemetry.tracer
133
+ return super unless tracer
134
+
135
+ attributes = otel_attributes(exchange, routing_key)
136
+ attributes["messaging.batch.message_count"] = payloads.is_a?(Array) ? payloads.size : 0
137
+ tracer.in_span("#{Helpers.destination(exchange, routing_key)} publish",
138
+ attributes: attributes, kind: :producer) do
139
+ headers = (opts[:headers] || {}).dup
140
+ ::OpenTelemetry.propagation.inject(headers)
141
+ super(payloads, exchange: exchange, routing_key: routing_key, **opts.merge(headers: headers))
142
+ end
143
+ end
144
+
145
+ def basic_get(queue_name, manual_ack: true)
146
+ tracer = OpenTelemetry.tracer
147
+ return super unless tracer
148
+
149
+ attributes = otel_attributes("", nil)
150
+ attributes["messaging.operation"] = "receive"
151
+ tracer.in_span("#{queue_name} receive", attributes: attributes, kind: :consumer) do |span|
152
+ result = super
153
+ delivery, = result
154
+ if delivery
155
+ span["messaging.destination"] = delivery.exchange.to_s
156
+ span["messaging.destination_kind"] = Helpers.destination_kind(delivery.exchange,
157
+ otel_exchange_type(delivery.exchange))
158
+ span["messaging.rabbitmq.routing_key"] = delivery.routing_key if delivery.routing_key
159
+ span.name = "#{Helpers.destination(delivery.exchange, delivery.routing_key)} receive"
160
+ end
161
+ result
162
+ end
163
+ end
164
+
165
+ def basic_consume(queue_name, **opts, &block)
166
+ tracer = OpenTelemetry.tracer
167
+ return super unless tracer && block
168
+ # Recovery re-registers consumers with the block it stored, which is
169
+ # already wrapped; wrapping again would nest a span per reconnect.
170
+ return super if block.respond_to?(:async_rabbitmq_traced?)
171
+
172
+ traced = proc do |delivery, header, body|
173
+ otel_process_span(tracer, delivery, header) { block.call(delivery, header, body) }
174
+ end
175
+ traced.define_singleton_method(:async_rabbitmq_traced?) { true }
176
+ super(queue_name, **opts, &traced)
177
+ end
178
+
179
+ private
180
+
181
+ def otel_attributes(exchange, routing_key)
182
+ Helpers.basic_attributes(exchange: exchange, routing_key: routing_key,
183
+ peer_host: @session.host, peer_port: @session.port,
184
+ exchange_type: otel_exchange_type(exchange))
185
+ end
186
+
187
+ # Only exchanges this client declared are in the registry; a
188
+ # pre-existing exchange has no recorded type and is treated as a topic.
189
+ def otel_exchange_type(exchange)
190
+ return nil if exchange.nil? || exchange.to_s.empty?
191
+
192
+ topology&.exchanges&.find { |recorded| recorded.name == exchange }&.type
193
+ end
194
+
195
+ def otel_process_span(tracer, delivery, header, &block)
196
+ attributes = otel_attributes(delivery.exchange, delivery.routing_key)
197
+ attributes["messaging.operation"] = "process"
198
+ if (message_id = Helpers.message_id_of(header))
199
+ attributes["messaging.message_id"] = message_id
200
+ end
201
+
202
+ parent = ::OpenTelemetry.propagation.extract(Helpers.headers_of(header))
203
+ ::OpenTelemetry::Context.with_current(parent) do
204
+ tracer.in_span("#{Helpers.destination(delivery.exchange, delivery.routing_key)} process",
205
+ attributes: attributes, kind: :consumer, &block)
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+
5
+ module AsyncRabbitMQ
6
+ # Builds an OpenSSL::SSL::SSLContext from the things people actually have: a
7
+ # client certificate, its key, and the CA that signed the broker's. Nobody
8
+ # should need to know about X509::Store to connect over TLS; anyone who needs
9
+ # more than this passes a ready-made +tls_context:+ to Session instead.
10
+ #
11
+ # AsyncRabbitMQ::Session.new(tls_cert: "client.pem", tls_key: "client.key",
12
+ # tls_ca_certificates: ["ca.pem"])
13
+ #
14
+ # Every certificate and key option takes either a file path or PEM text, so
15
+ # material from a secrets manager works as well as a file. A certificate
16
+ # value may hold the leaf followed by its chain; a CA value may hold several
17
+ # certificates; +tls_ca_certificates+ takes one value or a list. With no CAs
18
+ # given the system trust store is used.
19
+ module TLS
20
+ VERSIONS = {
21
+ "TLS1_2" => OpenSSL::SSL::TLS1_2_VERSION,
22
+ "TLS1_3" => OpenSSL::SSL::TLS1_3_VERSION
23
+ }.freeze
24
+
25
+ module_function
26
+
27
+ def context(cert: nil, key: nil, ca_certificates: nil, verify_peer: true, min_version: :TLS1_2,
28
+ certificate_store: nil)
29
+ ctx = OpenSSL::SSL::SSLContext.new
30
+ # set_params first: it supplies the system trust store, hostname
31
+ # verification and OpenSSL's recommended options, which we then narrow.
32
+ ctx.set_params(verify_mode: verify_peer ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
33
+ ctx.verify_hostname = verify_peer
34
+ ctx.min_version = version(min_version)
35
+
36
+ if cert
37
+ leaf, *chain = OpenSSL::X509::Certificate.load(material(cert, "tls_cert"))
38
+ ctx.cert = leaf
39
+ ctx.extra_chain_cert = chain unless chain.empty?
40
+ end
41
+ ctx.key = OpenSSL::PKey.read(material(key, "tls_key")) if key
42
+ raise ArgumentError, "tls_key given without tls_cert" if key && !cert
43
+
44
+ if certificate_store
45
+ ctx.cert_store = certificate_store
46
+ elsif ca_certificates
47
+ ctx.cert_store = store(Array(ca_certificates))
48
+ end
49
+ ctx
50
+ end
51
+
52
+ # A trust store holding exactly the given CAs (paths or PEM text, each of
53
+ # which may contain several certificates).
54
+ def store(ca_certificates)
55
+ store = OpenSSL::X509::Store.new
56
+ ca_certificates.each do |value|
57
+ OpenSSL::X509::Certificate.load(material(value, "tls_ca_certificates")).each { |c| store.add_cert(c) }
58
+ end
59
+ store
60
+ end
61
+
62
+ # PEM text is returned as is; anything else is read as a file path.
63
+ def material(value, option)
64
+ text = value.to_s
65
+ return text if text.include?("-----BEGIN")
66
+ return File.read(text) if File.file?(text)
67
+
68
+ raise ArgumentError, "#{option}: #{value.inspect} is neither PEM text nor a readable file"
69
+ end
70
+
71
+ # :TLS1_2, "TLS1_3", "TLSv1.2", 1.2 or an OpenSSL::SSL::*_VERSION constant.
72
+ def version(value)
73
+ return value if value.is_a?(Integer)
74
+
75
+ key = value.to_s.sub(/\ATLSv?/i, "TLS").tr(".", "_").upcase
76
+ key = "TLS#{key}" unless key.start_with?("TLS")
77
+ VERSIONS.fetch(key) do
78
+ raise ArgumentError, "tls_min_version #{value.inspect}: expected one of #{VERSIONS.keys.join(', ')}"
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,210 @@
1
+ module AsyncRabbitMQ
2
+ # Records the exchanges, queues and bindings declared through a Session so
3
+ # they can be re-declared after a reconnect ("topology recovery", as in the
4
+ # RabbitMQ Java client and Bunny 3.0). Passive declares are not recorded,
5
+ # deletes and unbinds remove their records, and a server-named queue that
6
+ # comes back under a new name has its bindings and Queue objects renamed.
7
+ #
8
+ # The registry mirrors what the broker will do, not which channel declared
9
+ # what: a durable queue declared on a channel that was then closed still
10
+ # exists and is still recovered, and so is an exclusive queue, which lives as
11
+ # long as the connection. What a channel closing does remove is the
12
+ # consumers on it, and the broker deletes an auto-delete queue when its last
13
+ # consumer goes, so consumers are recorded too and an auto-delete queue is
14
+ # forgotten with its last one, by cancel, broker cancel or channel close.
15
+ # The bindings go with it, and an auto-delete exchange that just lost its
16
+ # last binding goes as well, again as the broker does. Consumers on other
17
+ # connections are not visible here; if one keeps such a queue alive, all
18
+ # that happens is that recovery does not re-declare a queue that exists.
19
+ #
20
+ # An optional +filter+ object may implement any of filter_exchanges(list),
21
+ # filter_queues(list), filter_queue_bindings(list) and
22
+ # filter_exchange_bindings(list); each receives the recorded entries and
23
+ # returns the subset to recover.
24
+ class TopologyRegistry
25
+ RecordedExchange = Struct.new(:channel_id, :name, :type, :durable, :auto_delete, :internal, :arguments,
26
+ keyword_init: true)
27
+ RecordedQueue = Struct.new(:channel_id, :name, :durable, :exclusive, :auto_delete, :arguments, :server_named,
28
+ :objects, keyword_init: true)
29
+ RecordedQueueBinding = Struct.new(:channel_id, :queue, :exchange, :routing_key, :arguments, keyword_init: true)
30
+ RecordedExchangeBinding = Struct.new(:channel_id, :source, :destination, :routing_key, :arguments, keyword_init: true)
31
+
32
+ attr_reader :filter
33
+
34
+ def initialize(filter: nil)
35
+ @filter = filter
36
+ @exchanges = {} # name => RecordedExchange
37
+ @queues = {} # name => RecordedQueue
38
+ @queue_bindings = []
39
+ @exchange_bindings = []
40
+ @consumers = {} # consumer tag => queue name
41
+ end
42
+
43
+ # --- recording -----------------------------------------------------------
44
+
45
+ def record_exchange(channel_id, name, type, durable:, auto_delete:, internal:, arguments:)
46
+ return if name.empty? || AsyncRabbitMQ::Exchange::PREDEFINED_EXCHANGES.include?(name)
47
+ @exchanges[name] = RecordedExchange.new(channel_id: channel_id, name: name, type: type.to_s, durable: durable,
48
+ auto_delete: auto_delete, internal: internal, arguments: arguments.dup)
49
+ end
50
+
51
+ def delete_exchange(name)
52
+ @exchanges.delete(name)
53
+ @queue_bindings.reject! { |b| b.exchange == name }
54
+ gone, @exchange_bindings = @exchange_bindings.partition { |b| b.source == name || b.destination == name }
55
+ # An exchange bound to the deleted one may itself have been auto-delete
56
+ # and kept alive only by that binding.
57
+ gone.each { |b| prune_auto_delete_exchange(b.source) if b.destination == name }
58
+ end
59
+
60
+ def record_queue(channel_id, name, durable:, exclusive:, auto_delete:, arguments:, server_named:, object: nil)
61
+ existing = @queues[name]
62
+ rec = RecordedQueue.new(channel_id: channel_id, name: name, durable: durable, exclusive: exclusive,
63
+ auto_delete: auto_delete, arguments: arguments.dup, server_named: server_named,
64
+ objects: existing ? existing.objects : [])
65
+ rec.objects << object if object && !rec.objects.include?(object)
66
+ @queues[name] = rec
67
+ end
68
+
69
+ def delete_queue(name)
70
+ @queues.delete(name)
71
+ gone, @queue_bindings = @queue_bindings.partition { |b| b.queue == name }
72
+ gone.each { |b| prune_auto_delete_exchange(b.exchange) }
73
+ end
74
+
75
+ # A server-named queue was re-declared under a different name.
76
+ def rename_queue(old_name, new_name)
77
+ rec = @queues.delete(old_name) or return
78
+ rec.name = new_name
79
+ @queues[new_name] = rec
80
+ @queue_bindings.each { |b| b.queue = new_name if b.queue == old_name }
81
+ @consumers.transform_values! { |queue| queue == old_name ? new_name : queue }
82
+ rec.objects.each { |q| q.update_name_to(new_name) }
83
+ end
84
+
85
+ def record_queue_binding(channel_id, queue:, exchange:, routing_key:, arguments:)
86
+ delete_queue_binding(queue: queue, exchange: exchange, routing_key: routing_key, arguments: arguments, prune: false)
87
+ @queue_bindings << RecordedQueueBinding.new(channel_id: channel_id, queue: queue, exchange: exchange,
88
+ routing_key: routing_key, arguments: arguments.dup)
89
+ end
90
+
91
+ def delete_queue_binding(queue:, exchange:, routing_key:, arguments:, prune: true)
92
+ @queue_bindings.reject! do |b|
93
+ b.queue == queue && b.exchange == exchange && b.routing_key == routing_key && b.arguments == arguments
94
+ end
95
+ prune_auto_delete_exchange(exchange) if prune
96
+ end
97
+
98
+ def record_exchange_binding(channel_id, source:, destination:, routing_key:, arguments:)
99
+ delete_exchange_binding(source: source, destination: destination, routing_key: routing_key,
100
+ arguments: arguments, prune: false)
101
+ @exchange_bindings << RecordedExchangeBinding.new(channel_id: channel_id, source: source, destination: destination,
102
+ routing_key: routing_key, arguments: arguments.dup)
103
+ end
104
+
105
+ def delete_exchange_binding(source:, destination:, routing_key:, arguments:, prune: true)
106
+ @exchange_bindings.reject! do |b|
107
+ b.source == source && b.destination == destination && b.routing_key == routing_key && b.arguments == arguments
108
+ end
109
+ prune_auto_delete_exchange(source) if prune
110
+ end
111
+
112
+ # --- consumers -----------------------------------------------------------
113
+
114
+ def record_consumer(consumer_tag, queue_name)
115
+ @consumers[consumer_tag] = queue_name
116
+ end
117
+
118
+ # The consumer is gone: cancelled by the client or the broker, or its
119
+ # channel closed. If it was the last one on an auto-delete queue, the
120
+ # broker deletes the queue, so forget the queue and what hung off it.
121
+ def delete_consumer(consumer_tag)
122
+ queue_name = @consumers.delete(consumer_tag) or return
123
+ prune_auto_delete_queue(queue_name)
124
+ end
125
+
126
+ # consumer tag => queue name, for inspection.
127
+ def consumers
128
+ @consumers.dup
129
+ end
130
+
131
+ # --- what to recover (filtered) -----------------------------------------
132
+
133
+ def exchanges
134
+ apply(:filter_exchanges, @exchanges.values)
135
+ end
136
+
137
+ def queues
138
+ apply(:filter_queues, @queues.values)
139
+ end
140
+
141
+ def queue_bindings
142
+ apply(:filter_queue_bindings, @queue_bindings.dup)
143
+ end
144
+
145
+ def exchange_bindings
146
+ apply(:filter_exchange_bindings, @exchange_bindings.dup)
147
+ end
148
+
149
+ def empty?
150
+ @exchanges.empty? && @queues.empty? && @queue_bindings.empty? && @exchange_bindings.empty?
151
+ end
152
+
153
+ # Forget everything. For a connection whose channels have all been
154
+ # dropped: nothing client-side holds what it declared, so nothing is
155
+ # re-declared when it reconnects.
156
+ def clear
157
+ @exchanges.clear
158
+ @queues.clear
159
+ @queue_bindings.clear
160
+ @exchange_bindings.clear
161
+ @consumers.clear
162
+ end
163
+
164
+ # Forget only what was tied to the connection that just went: exclusive
165
+ # and auto-delete queues, server-named queues (whose names are gone with
166
+ # the connection), auto-delete exchanges, and any binding that referred to
167
+ # them. Durable topology is kept, so a broker that also lost it — a node
168
+ # restarted from an empty data directory — still has it re-declared.
169
+ def clear_transient
170
+ dropped_queues = @queues.each_value
171
+ .select { |q| q.exclusive || q.auto_delete || q.server_named }
172
+ .map(&:name)
173
+ dropped_exchanges = @exchanges.each_value.select(&:auto_delete).map(&:name)
174
+
175
+ dropped_queues.each { |name| @queues.delete(name) }
176
+ dropped_exchanges.each { |name| @exchanges.delete(name) }
177
+
178
+ @queue_bindings.reject! do |b|
179
+ dropped_queues.include?(b.queue) || dropped_exchanges.include?(b.exchange)
180
+ end
181
+ @exchange_bindings.reject! do |b|
182
+ dropped_exchanges.include?(b.source) || dropped_exchanges.include?(b.destination)
183
+ end
184
+ # Consumers belong to the channels that were dropped with the connection.
185
+ @consumers.clear
186
+ end
187
+
188
+ private
189
+
190
+ def prune_auto_delete_queue(name)
191
+ rec = @queues[name]
192
+ return unless rec&.auto_delete
193
+ return if @consumers.value?(name)
194
+
195
+ delete_queue(name)
196
+ end
197
+
198
+ def prune_auto_delete_exchange(name)
199
+ rec = @exchanges[name]
200
+ return unless rec&.auto_delete
201
+ return if @queue_bindings.any? { |b| b.exchange == name } || @exchange_bindings.any? { |b| b.source == name }
202
+
203
+ delete_exchange(name)
204
+ end
205
+
206
+ def apply(filter_method, list)
207
+ @filter.respond_to?(filter_method) ? @filter.public_send(filter_method, list) : list
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,3 @@
1
+ module AsyncRabbitMQ
2
+ VERSION = "0.4.0"
3
+ end
@@ -0,0 +1,62 @@
1
+ module AsyncRabbitMQ
2
+ # A broker delivery tag plus the connection generation it was issued on.
3
+ #
4
+ # Delivery tags are scoped to a channel on a connection: after a reconnect
5
+ # the broker starts numbering from 1 again. A handler that was still running
6
+ # when the connection dropped would otherwise ack a tag from the old
7
+ # connection against the new channel, where that number now belongs to a
8
+ # different message (silent loss, or a whole range of them with
9
+ # multiple: true) or to no message at all (a 406 that closes the channel).
10
+ #
11
+ # Channel#basic_ack, #basic_nack and #basic_reject compare the generation
12
+ # carried here against the channel's current one and drop stale tags.
13
+ #
14
+ # It stands in for the Integer it wraps: it converts implicitly (#to_int),
15
+ # compares and sorts against Integers, and hashes equal to them, so code
16
+ # that treats a delivery tag as a number keeps working.
17
+ class VersionedDeliveryTag
18
+ include Comparable
19
+
20
+ attr_reader :tag, :generation
21
+
22
+ def initialize(tag, generation)
23
+ @tag = tag
24
+ @generation = generation
25
+ end
26
+
27
+ # True when this tag was issued on an earlier connection than +current+.
28
+ def stale?(current)
29
+ @generation < current
30
+ end
31
+
32
+ def to_i
33
+ @tag
34
+ end
35
+ alias to_int to_i
36
+
37
+ def coerce(other)
38
+ [other, @tag]
39
+ end
40
+
41
+ def <=>(other)
42
+ @tag <=> (other.respond_to?(:to_i) ? other.to_i : other)
43
+ end
44
+
45
+ def ==(other)
46
+ (@tag <=> (other.respond_to?(:to_i) ? other.to_i : other)) == 0
47
+ end
48
+ alias eql? ==
49
+
50
+ def hash
51
+ @tag.hash
52
+ end
53
+
54
+ def to_s
55
+ @tag.to_s
56
+ end
57
+
58
+ def inspect
59
+ "#<#{self.class.name} #{@tag} gen=#{@generation}>"
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,18 @@
1
+ require_relative "async_rabbitmq/version"
2
+ require_relative "async_rabbitmq/log"
3
+ require_relative "async_rabbitmq/errors"
4
+ require_relative "async_rabbitmq/tls"
5
+ require_relative "async_rabbitmq/channel_id_allocator"
6
+ require_relative "async_rabbitmq/notifier"
7
+ require_relative "async_rabbitmq/versioned_delivery_tag"
8
+ require_relative "async_rabbitmq/topology_registry"
9
+ require_relative "async_rabbitmq/frame_io"
10
+ require_relative "async_rabbitmq/queue"
11
+ require_relative "async_rabbitmq/exchange"
12
+ require_relative "async_rabbitmq/channel"
13
+ require_relative "async_rabbitmq/session"
14
+ require_relative "async_rabbitmq/cluster"
15
+
16
+ module AsyncRabbitMQ
17
+ # Optional: require "async_rabbitmq/pool" for connection pooling.
18
+ end