Class: AceMQ::Rails::Runner
- Inherits:
-
Object
- Object
- AceMQ::Rails::Runner
- Defined in:
- lib/acemq/rails/runner.rb
Overview
The consumer process: what rake acemq:consume and acemq-consumer run.
This is deliberately not a thing the web server does. See docs/consumers.md
for the argument at length; the short form is that Puma owns its workers'
lifetimes and a message handler is not a request. A consumer started inside
Puma is forked with the worker if preload_app! is on (and a bunny socket
does not survive a fork), killed by worker_timeout if a handler takes
longer than a slow request should, scaled by however many web processes the
traffic wants rather than by how deep the queue is, and drained on Puma's
schedule rather than on the broker's. None of those is a bug that can be
fixed from in here.
What it does, in order: eager-load the application so consumer classes exist, apply the topology, subscribe every runnable consumer, and then sleep until a signal arrives — at which point it drains.
Defined Under Namespace
Modules: Passthrough
Constant Summary collapse
- SIGNALS =
Signals that mean "stop": what Kubernetes, systemd, Docker and a terminal all send.
%w[INT TERM].freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#consumers ⇒ Object
readonly
Returns the value of attribute consumers.
Instance Method Summary collapse
-
#connection ⇒ Object
The connection.
-
#drain(timeout: config.shutdown_timeout) ⇒ Boolean
Stops every consumer, waits for the handlers they are already running, and closes the connection.
-
#initialize(config: AceMQ::Rails.config, connection: nil, logger: nil) ⇒ Runner
constructor
A new instance of Runner.
-
#run(classes = nil) ⇒ Boolean
Boots, runs, and drains on a signal.
-
#start(classes = nil) ⇒ Array<AceMQ::AMQP::Consumer>
Applies the topology and subscribes every consumer, without waiting.
-
#stop ⇒ Object
Asks #run to come back.
Constructor Details
#initialize(config: AceMQ::Rails.config, connection: nil, logger: nil) ⇒ Runner
Returns a new instance of Runner.
44 45 46 47 48 49 50 51 |
# File 'lib/acemq/rails/runner.rb', line 44 def initialize(config: AceMQ::Rails.config, connection: nil, logger: nil) @config = config @connection = connection @logger = logger || AceMQ::Rails.logger @consumers = [] @stopping = Thread::Queue.new @lock = Mutex.new end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
53 54 55 |
# File 'lib/acemq/rails/runner.rb', line 53 def config @config end |
#consumers ⇒ Object (readonly)
Returns the value of attribute consumers.
53 54 55 |
# File 'lib/acemq/rails/runner.rb', line 53 def consumers @consumers end |
Instance Method Details
#connection ⇒ Object
The connection. Opened on first use, so a runner can be built in a test without one.
57 58 59 |
# File 'lib/acemq/rails/runner.rb', line 57 def connection @lock.synchronize { @connection ||= AceMQ::Rails.connection } end |
#drain(timeout: config.shutdown_timeout) ⇒ Boolean
Stops every consumer, waits for the handlers they are already running, and closes the connection.
What this finishes and what it abandons is set out with measurements in docs/lifecycle.md. In brief:
- A handler in flight finishes, and its message is acknowledged. The drain blocks on it.
- A delivery the broker had sent but no handler had picked up goes back to the broker unacknowledged and is redelivered.
- A retry waiting out a short backoff is waited out in full, because
the library's backoff is a
sleepon the handler's own thread and is therefore in flight. A long one is waiting on a rung queue in the broker and costs this nothing. Which is which isconsumer.broker_wait_threshold, thirty seconds by default, and it is the single setting that most affects how long a deploy takes. - A publish waiting for a confirm is waited for: the library's
publishis synchronous, so a publish in progress is a thread this has to join or abandon, and the transport close below happens after.
The drain itself is the library's Connection#close, which since
acemq-amqp 0.7.0 spends one deadline across every consumer rather
than a fresh one on each. Until then it did not, which is why this class
had a drain of its own: eight consumers each given thirty seconds is
four minutes, and four minutes into a shutdown Kubernetes has long since
sent SIGKILL — killing every handler mid-flight and leaving everything
they held unsettled, the exact outcome draining exists to avoid. One
deadline is the library's answer now, so this passes shutdown_timeout
to it and keeps none of the arithmetic.
What is left here is the shape a process wants rather than the shape a
library wants: a boolean and a log line, where the library raises
AMQP::DrainTimeout. acemq-consumer turns that boolean into
exit 0 or 75, and an exception out of a signal handler's drain would
be a backtrace where an exit status belongs.
129 130 131 132 133 134 135 136 137 138 |
# File 'lib/acemq/rails/runner.rb', line 129 def drain(timeout: config.shutdown_timeout) started = monotonic log_info "acemq: draining #{@consumers.size} consumer(s), #{timeout}s at most" finished = close_connection(timeout) took = (monotonic - started).round(2) log_info "acemq: drained in #{took}s" if finished finished end |
#run(classes = nil) ⇒ Boolean
Boots, runs, and drains on a signal. Returns when the drain is over.
66 67 68 69 70 71 72 |
# File 'lib/acemq/rails/runner.rb', line 66 def run(classes = nil) trap_signals start(classes) log_info "acemq: #{@consumers.size} consumer(s) running; waiting for a signal" @stopping.pop drain end |
#start(classes = nil) ⇒ Array<AceMQ::AMQP::Consumer>
Applies the topology and subscribes every consumer, without waiting.
Separated from #run so a test can start, publish, assert and stop without a signal anywhere near it.
80 81 82 83 84 85 86 87 |
# File 'lib/acemq/rails/runner.rb', line 80 def start(classes = nil) classes = Array(classes || Registry.runnable) refuse_an_empty_run(classes) complain_about_reloading apply_topology @consumers = classes.map { |klass| subscribe(klass) } end |
#stop ⇒ Object
Asks #run to come back. For a test, and for anything embedding this.
90 |
# File 'lib/acemq/rails/runner.rb', line 90 def stop = @stopping << :stop |