Class: AceMQ::AMQP::Consumer
- Inherits:
-
Object
- Object
- AceMQ::AMQP::Consumer
- Defined in:
- lib/acemq/amqp/connection.rb
Overview
A running subscription. Cancel it to stop.
Where the retry policy is actually enforced: the attempt is advanced here,
the decision about where the delay is waited is made here, and so is the
decision to give up and dead-letter — that last one because the broker
cannot write x-acemq-error onto a message explaining why it gave up.
A short delay is waited here, holding one prefetch slot. A long one is waited by the broker, in a rung queue; see RetryLadder for why the two are not the same choice.
Constant Summary collapse
- ALSO_COUNTED =
The counters a delivery raises besides
acemq.consume.total, keyed by the word the Settlement carries.A retry and a message set aside are counted twice on purpose: once in
acemq.consume.totaltagged with the outcome, which is the series saying exactly one thing happened to this message, and once under a name of their own, because a retry rate and a dead-letter rate are the two most often wanted without a tag filter. All five languages keep the same pair. Nothing else has a second name; the tag is how they are read.parkedsharesdead.lettered.totalwithdead_letteredrather than having a counter of its own, tagged apart by the outcome. That is Java's rule, and its reasoning is worth repeating: both are a message set aside, and an operator asking "how much is this queue giving up on" wants one number that can then be split. { Settlement::RETRIED => Telemetry::RETRIED_TOTAL, Settlement::DEAD_LETTERED => Telemetry::DEAD_LETTERED_TOTAL, Settlement::PARKED => Telemetry::DEAD_LETTERED_TOTAL }.freeze
Instance Attribute Summary collapse
-
#codec ⇒ Object
readonly
Returns the value of attribute codec.
-
#ladder ⇒ Object
readonly
Returns the value of attribute ladder.
-
#queue ⇒ Object
readonly
Returns the value of attribute queue.
-
#retry_policy ⇒ Object
readonly
Returns the value of attribute retry_policy.
Instance Method Summary collapse
-
#cancel(timeout: Connection::DRAIN_TIMEOUT) ⇒ nil
Stops delivery and waits for handlers already running.
-
#handle(delivery) ⇒ Object
Runs one delivery through the codec, the handler and the retry policy.
-
#in_flight ⇒ Object
How many messages this consumer is working on right now.
-
#initialize(transport:, queue:, handler:, codec:, retry_policy:, retry_threshold: RetryLadder::DEFAULT_THRESHOLD, interceptors: Interceptors.new, telemetry: nil) ⇒ Consumer
constructor
A new instance of Consumer.
-
#release ⇒ Integer
private
The last step of #cancel: closes the channel, which hands every unsettled delivery back to the broker, and stops any handler still running from settling.
-
#running? ⇒ Boolean
Whether the broker is still sending this consumer messages.
- #start(prefetch:, concurrency:, tag:, arguments:, dead_letters: true) ⇒ Object private
-
#stop ⇒ Object
private
The first step of #cancel: no more deliveries, and none of those bunny has already received is started — they go back with the channel.
Constructor Details
#initialize(transport:, queue:, handler:, codec:, retry_policy:, retry_threshold: RetryLadder::DEFAULT_THRESHOLD, interceptors: Interceptors.new, telemetry: nil) ⇒ Consumer
Returns a new instance of Consumer.
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 |
# File 'lib/acemq/amqp/connection.rb', line 779 def initialize(transport:, queue:, handler:, codec:, retry_policy:, retry_threshold: RetryLadder::DEFAULT_THRESHOLD, interceptors: Interceptors.new, telemetry: nil) @transport = transport @queue = queue @handler = handler @codec = codec @interceptors = interceptors @telemetry = Telemetry::Reporter.for(telemetry) @retry_policy = retry_policy @ladder = RetryLadder.for(queue, retry_policy, threshold: retry_threshold) @dead_letter_queue = Naming.dead_letter_queue(queue) @parked_queue = Naming.parked_queue(queue) @lock = Mutex.new @in_flight = 0 @rungs_seen = {} end |
Instance Attribute Details
#codec ⇒ Object (readonly)
Returns the value of attribute codec.
777 778 779 |
# File 'lib/acemq/amqp/connection.rb', line 777 def codec @codec end |
#ladder ⇒ Object (readonly)
Returns the value of attribute ladder.
777 778 779 |
# File 'lib/acemq/amqp/connection.rb', line 777 def ladder @ladder end |
#queue ⇒ Object (readonly)
Returns the value of attribute queue.
777 778 779 |
# File 'lib/acemq/amqp/connection.rb', line 777 def queue @queue end |
#retry_policy ⇒ Object (readonly)
Returns the value of attribute retry_policy.
777 778 779 |
# File 'lib/acemq/amqp/connection.rb', line 777 def retry_policy @retry_policy end |
Instance Method Details
#cancel(timeout: Connection::DRAIN_TIMEOUT) ⇒ nil
Stops delivery and waits for handlers already running.
The subscription is stopped and closed whether or not the wait ran out, because a consumer that has been cancelled and is still subscribed is the worst of both: it is not draining and it is still being sent work. Whatever a handler was holding when time ran out is unacknowledged, so the broker redelivers it. #in_flight afterwards is how many that was, and it is what AceMQ::AMQP::Connection#close reports in its DrainTimeout.
A handler still running at the deadline is left to finish, and its
outcome is thrown away. Ruby cannot stop a thread safely — Thread#kill
in the middle of a database transaction is worse than anything it would
save — so the thread runs on, as Java's drain leaves it ("the consumer
is still stopped, but something is still running"). Whatever it returns
is not settled: no ack, no retry, no dead letter. The channel it came
down is already closed, so the broker has requeued the message and will
redeliver it; settling it as well would be the same message twice.
Deliveries bunny had already received but no handler had started are not started at all once this is called. They go back with the channel.
907 908 909 910 |
# File 'lib/acemq/amqp/connection.rb', line 907 def cancel(timeout: Connection::DRAIN_TIMEOUT) failure, = Drain.of([self], timeout) raise failure if failure end |
#handle(delivery) ⇒ Object
Runs one delivery through the codec, the handler and the retry policy.
Public because it is the whole of this class worth testing, and a test that has to open a broker to reach it is a test nobody runs.
834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 |
# File 'lib/acemq/amqp/connection.rb', line 834 def handle(delivery) enter envelope = envelope_for(delivery) # Which go this delivery is, recorded after the envelope and before the # codec: a body that will not decode is still a delivery that arrived, # and that ordering is what makes this distribution's sample count # trustworthy as "deliveries in". @telemetry.observe(Telemetry::CONSUME_ATTEMPTS, envelope.attempt, queue: @queue) context = context_for(delivery, envelope, decode(delivery)) started = Process.clock_gettime(Process::CLOCK_MONOTONIC) ack = invoke(context, delivery) took = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started # Decided before the interceptors are told rather than after, because # the decision is the thing worth telling them: an ack asking for a # retry on the last attempt is a dead letter, and a retry that will # really happen has a delay attached that does not exist until the # policy has been asked. An interceptor left to infer it from the ack # reports the message as retried and the delay as nothing. context.settlement = decide(context.envelope, ack) observe(context.settlement, took) # After the handler and before the delivery is settled, so an # interceptor can still see how it went and still undo whatever it set # up on the way in. The envelope read back off the context is the one an # interceptor may have changed, and it is the one a dead letter is # written with — otherwise a header stamped on the way in would be there # for the handler and gone from the queue somebody has to look at. @interceptors.after_handle(context, ack) # Not settled when the drain gave up on this handler: the channel is # gone, the broker has the message back, and an ack would fail while a # retry or a dead letter would publish a second copy. See {#cancel}. settle(delivery, context.envelope, context.settlement) unless @released rescue DecodeError => e # A body that will not decode decodes no better next time, so it is # parked rather than retried. Parked and not dead-lettered: a message # that failed five times and a message nothing could read are two # different problems, and whoever drains the dead letters should not # have to sort them by hand. # # Counted here rather than inside {#park}, because this delivery never # reached a handler and so never reached {#observe}: a handler that asks # for {Ack.park} is counted there with every other outcome, and counting # in both places would count it twice. @telemetry.count(Telemetry::CONSUME_TOTAL, 1, queue: @queue, outcome: Telemetry::Outcome::PARKED) park(delivery, envelope, "could not be decoded: #{e.}") ensure leave end |
#in_flight ⇒ Object
How many messages this consumer is working on right now.
913 |
# File 'lib/acemq/amqp/connection.rb', line 913 def in_flight = @lock.synchronize { @in_flight } |
#release ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The last step of #cancel: closes the channel, which hands every unsettled delivery back to the broker, and stops any handler still running from settling.
932 933 934 935 |
# File 'lib/acemq/amqp/connection.rb', line 932 def release @released = true in_flight.tap { @subscription&.close } end |
#running? ⇒ Boolean
Whether the broker is still sending this consumer messages.
Asked of the subscription rather than remembered here, because the two can disagree: a channel closed by the broker, or taken down by an error on it, stops delivery without anything in this process being told. A flag set in #cancel would say "running" for a consumer that had been deaf for an hour.
804 |
# File 'lib/acemq/amqp/connection.rb', line 804 def running? = !!@subscription&.open? |
#start(prefetch:, concurrency:, tag:, arguments:, dead_letters: true) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 |
# File 'lib/acemq/amqp/connection.rb', line 807 def start(prefetch:, concurrency:, tag:, arguments:, dead_letters: true) # Declared before anything is subscribed, and not on the failure path. # A rung, a dead-letter queue or a parking queue that does not exist # loses the message rather than reporting anything — the default # exchange drops what it cannot route — so the moment to find out is the # one where nothing has failed yet. # # Both halves, and the dead-letter half even for a consumer with no # retry policy: this consumer republishes to +{queue}.dlq+ and # +{queue}.parked+ by name whatever its policy says, and a topology # nobody remembered to apply must not be the difference between a dead # letter somebody can read and one that never existed. # # The one exception is a consumer started with +dead_letters: false+, # which is the request-reply reply consumer: it accepts everything, so # durable queues hung off an exclusive reply queue would only outlive it. @ladder.declare(@transport) if dead_letters @subscription = @transport.subscribe( @queue, prefetch: prefetch, concurrency: concurrency, tag: tag, arguments: arguments ) { |delivery| handle(delivery) unless @stopped } self end |
#stop ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The first step of #cancel: no more deliveries, and none of those bunny has already received is started — they go back with the channel. Returns at once; bunny is not allowed to wait for a busy handler here, see Transport#subscribe.
921 922 923 924 |
# File 'lib/acemq/amqp/connection.rb', line 921 def stop @stopped = true @subscription&.stop end |