Class: AceMQ::AMQP::Interceptors
- Inherits:
-
Object
- Object
- AceMQ::AMQP::Interceptors
- Defined in:
- lib/acemq/amqp/interceptors.rb
Overview
The interceptors registered on a connection, and the rules for running them.
The extension point for the things every message in an organisation needs and no library can guess: a tenant identifier, a trace context, a log scope, a size limit, a metric. Without a seam they end up copied into every call site, where one of them is always missing.
mq.intercept_publish { |context| context.set_header("tenant", Current.tenant) }
mq.intercept_consume do |context|
raise AceMQ::AMQP::FatalError, "not our tenant" unless ours?(context)
end
An interceptor is anything answering the hooks it cares about, so the common case is a block and the full case is an object:
class Timing
def order = -100
def before_handle(context) = @started[context.envelope.id] = now
def after_handle(context, ack) = record(context, ack, now - @started.delete(...))
end
mq.intercept_consume(Timing.new)
Nothing here needs anything private. An interceptor is handed a context whose every field is public and is registered through a public method, so everything in this library that could have been an interceptor could have been written by somebody outside it — which is the same rule the patterns follow, and the only way to know a seam is wide enough.
What a failure means
Raising from before_publish stops the publish, and the caller sees the
exception. That is the point of intercepting rather than observing: a
message that must not go out is stopped once, here, rather than in every
publisher.
Raising from before_handle means the handler never runs and the delivery
is treated exactly as a failed handler would be — retried, then
dead-lettered. An interceptor that refuses a message has to be willing for
that message to end up in the dead-letter queue, which is the honest
outcome; the alternative is acknowledging something nothing processed.
FatalError still means what it means, so an interceptor that refuses for
a reason retrying cannot fix can say so.
Raising from after_confirm, after_handle or on_error is reported on
stderr and otherwise ignored. By then the message has been sent or the
delivery settled, and an exception cannot un-send or un-settle it: letting
it out would report a successful publish as a failed one, or skip the
remaining teardowns.
Order
Lower order runs first, and interceptors with the same order run in the
order they were registered. On the way out of a handler the order is
reversed, so a pair that opens something on the way in and closes it on
the way out nests properly: the first to open is the last to close.
An interceptor is called from whatever thread is publishing or handling, so one that keeps state has to be safe to call from several at once.
Defined Under Namespace
Classes: Registered
Instance Method Summary collapse
-
#add_consume(interceptor = nil, order: nil) {|context| ... } ⇒ Interceptors
Adds an interceptor to every consumer on this connection.
-
#add_publish(interceptor = nil, order: nil) {|context| ... } ⇒ Interceptors
Adds an interceptor to every publish on this connection.
-
#after_confirm(context) ⇒ Object
Runs every
after_confirm, once the broker has the message. -
#after_handle(context, ack) ⇒ Object
Runs every
after_handle, in reverse order, after the handler has returned or raised and before the delivery is settled. -
#before_handle(context) ⇒ Object
Runs every
before_handle, in order. -
#before_publish(context) ⇒ PublishContext
Runs every
before_publish, in order. - #consuming ⇒ Object
-
#empty? ⇒ Boolean
Whether anything at all is registered, which is what a caller checks before doing work only an interceptor would want done.
-
#initialize ⇒ Interceptors
constructor
A new instance of Interceptors.
-
#on_consume_error(context, failure) ⇒ Object
Runs every
on_errorafter a handler raised. -
#on_publish_error(context, failure) ⇒ Object
Runs every
on_errorafter a publish failed, including when an interceptor refused it. -
#publishing ⇒ Object
How many are registered on each side.
Constructor Details
#initialize ⇒ Interceptors
Returns a new instance of Interceptors.
260 261 262 263 264 265 |
# File 'lib/acemq/amqp/interceptors.rb', line 260 def initialize @lock = Mutex.new @publishing = [].freeze @consuming = [].freeze @registered = 0 end |
Instance Method Details
#add_consume(interceptor = nil, order: nil) {|context| ... } ⇒ Interceptors
Adds an interceptor to every consumer on this connection.
284 285 286 287 |
# File 'lib/acemq/amqp/interceptors.rb', line 284 def add_consume(interceptor = nil, order: nil, &block) add(:@consuming, interceptor, block, order, before: :before_handle, after: :after_handle) end |
#add_publish(interceptor = nil, order: nil) {|context| ... } ⇒ Interceptors
Adds an interceptor to every publish on this connection.
273 274 275 276 |
# File 'lib/acemq/amqp/interceptors.rb', line 273 def add_publish(interceptor = nil, order: nil, &block) add(:@publishing, interceptor, block, order, before: :before_publish, after: :after_confirm) end |
#after_confirm(context) ⇒ Object
Runs every after_confirm, once the broker has the message.
310 311 312 313 314 315 |
# File 'lib/acemq/amqp/interceptors.rb', line 310 def after_confirm(context) @publishing.each do |entry| safely(entry.after, "after a publish") { entry.after.call(context) } end context end |
#after_handle(context, ack) ⇒ Object
Runs every after_handle, in reverse order, after the handler has
returned or raised and before the delivery is settled.
The context carries the Settlement by then, so an interceptor is told what is about to happen rather than only what was asked for.
338 339 340 341 342 343 |
# File 'lib/acemq/amqp/interceptors.rb', line 338 def after_handle(context, ack) @consuming.reverse_each do |entry| safely(entry.after, "after a handler") { entry.after.call(context, ack) } end context end |
#before_handle(context) ⇒ Object
Runs every before_handle, in order. An exception is let out and the
consumer treats it as a handler failure, which is the honest outcome for
a refused message.
328 329 330 331 |
# File 'lib/acemq/amqp/interceptors.rb', line 328 def before_handle(context) @consuming.each { |entry| entry.before&.call(context) } context end |
#before_publish(context) ⇒ PublishContext
Runs every before_publish, in order. An exception is let straight out:
refusing a publish is the whole reason a policy interceptor exists, and
swallowing it here would turn "this message is not allowed" into "this
message was sent".
304 305 306 307 |
# File 'lib/acemq/amqp/interceptors.rb', line 304 def before_publish(context) @publishing.each { |entry| entry.before&.call(context) } context end |
#consuming ⇒ Object
295 |
# File 'lib/acemq/amqp/interceptors.rb', line 295 def consuming = @consuming.size |
#empty? ⇒ Boolean
Whether anything at all is registered, which is what a caller checks before doing work only an interceptor would want done.
291 |
# File 'lib/acemq/amqp/interceptors.rb', line 291 def empty? = @publishing.empty? && @consuming.empty? |
#on_consume_error(context, failure) ⇒ Object
Runs every on_error after a handler raised.
346 347 348 |
# File 'lib/acemq/amqp/interceptors.rb', line 346 def on_consume_error(context, failure) report(@consuming, context, failure, "a failed handler") end |
#on_publish_error(context, failure) ⇒ Object
Runs every on_error after a publish failed, including when an
interceptor refused it. Never allowed to replace the original failure:
the caller needs to see what went wrong, not what an observer did about
it.
321 322 323 |
# File 'lib/acemq/amqp/interceptors.rb', line 321 def on_publish_error(context, failure) report(@publishing, context, failure, "a failed publish") end |
#publishing ⇒ Object
How many are registered on each side.
294 |
# File 'lib/acemq/amqp/interceptors.rb', line 294 def publishing = @publishing.size |