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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +268 -0
- data/LICENSE +21 -0
- data/README.md +594 -0
- data/exe/async-rabbitmq +12 -0
- data/lib/async_rabbitmq/channel.rb +1422 -0
- data/lib/async_rabbitmq/channel_id_allocator.rb +57 -0
- data/lib/async_rabbitmq/cli.rb +300 -0
- data/lib/async_rabbitmq/cluster.rb +433 -0
- data/lib/async_rabbitmq/errors.rb +113 -0
- data/lib/async_rabbitmq/exchange.rb +70 -0
- data/lib/async_rabbitmq/frame_io.rb +309 -0
- data/lib/async_rabbitmq/log.rb +52 -0
- data/lib/async_rabbitmq/notifier.rb +122 -0
- data/lib/async_rabbitmq/pool.rb +43 -0
- data/lib/async_rabbitmq/queue.rb +88 -0
- data/lib/async_rabbitmq/sasl.rb +123 -0
- data/lib/async_rabbitmq/session.rb +1154 -0
- data/lib/async_rabbitmq/telemetry/open_telemetry.rb +211 -0
- data/lib/async_rabbitmq/tls.rb +82 -0
- data/lib/async_rabbitmq/topology_registry.rb +210 -0
- data/lib/async_rabbitmq/version.rb +3 -0
- data/lib/async_rabbitmq/versioned_delivery_tag.rb +62 -0
- data/lib/async_rabbitmq.rb +18 -0
- metadata +180 -0
|
@@ -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,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
|