Class: Wurk::Middleware::Timeout
- Inherits:
-
Object
- Object
- Wurk::Middleware::Timeout
- Includes:
- ServerMiddleware
- Defined in:
- lib/wurk/middleware/timeout.rb
Overview
Arms the capsule's Watchdog with whichever wall-clock bounds a job
declared. A class that declares neither — the default — costs two Hash
lookups and a yield.
sidekiq_options timeout: <seconds> bounds one attempt. Past it the
watchdog raises Job::TimedOut into the thread running perform,
and the job takes the ordinary failure path from there: booked as a
failure, retried on the class's own policy, dead set once the retries run
out. Per attempt, not per job — every attempt re-arms its own bound, so a
retry of a job that timed out gets the full budget again.
sidekiq_options deadline: <seconds> bounds the job from enqueue onward.
The client resolves it to an absolute deadline_at once, at push
(JobUtil#stamp_deadline), and what this middleware arms is whatever is
left of it — so retries and IterableJob resumes eat into one budget rather
than each getting a fresh one. Past it the watchdog raises
Job::DeadlineExceeded, which is not a failure to retry:
Expiry catches it one frame out and books the job expired, the same
terminal state as one whose deadline had already passed before it started.
That check, immediately before perform, is the other half of the bound —
a job whose window closed while it queued never starts at all.
Soft only. Thread#kill would cut the job between any two instructions,
stranding whatever it was holding — a checked-out connection, a half
written file, a lock nobody is left to release. That is the class of bug
docs/plans/2026/07/31/101-leak-logic-perf-fixes/ closed, and the reason a
hard kill belongs to the process supervisor rather than to a thread. What
soft costs is that a perform which swallows StandardError swallows this
too: the same bargain Celery's soft time limit and stdlib Timeout make.
Chain position (lib/wurk.rb), both halves load-bearing:
* OUTSIDE Statsd / History / Status, so the raise unwinds *through* all
three and the timeout is measured and booked as the failure it is. A
`DeadlineExceeded` crosses the same three, which is why the two metric
emitters name it: it is booked `expired`, and booking it failed as
well would count one abandoned job twice.
* INSIDE Batch, Expiry and Limiter. Limiter is the sharp one: its rescue
re-enqueues an over-limit job and only then raises `Rescheduled`, so a
bound reaching around it could fire between those two steps and leave
the job both rescheduled and retried. Ending the bound before that
frame puts the bookkeeping out of the watchdog's reach. Expiry has to
be outside for a second reason: it is what catches the deadline raise.
Against shutdown_timeout: both bounds run and the shorter one wins. A
timeout under the drain budget fires first and the job retries; over it,
shutdown unwinds the job with Wurk::Shutdown instead and the payload is
reclaimed from the private list on the next boot.
Instance Attribute Summary collapse
-
#config ⇒ Object
included
from ServerMiddleware
Returns the value of attribute config.
Class Method Summary collapse
-
.shared_instance? ⇒ Boolean
One instance per chain entry, shared by every job (Middleware::Chain::Entry): #call and its helpers keep their state in locals and write no ivar.
Instance Method Summary collapse
-
#call(_worker, job, _queue, &block) ⇒ Object
A named block, not
&: Ruby 3.3.0 rejects an anonymous block forwarded from inside another block (fixed in 3.3.1, but within the gem's floor). - #logger ⇒ Object included from ServerMiddleware
- #redis(idempotent: false) ⇒ Object included from ServerMiddleware
- #redis_pool ⇒ Object included from ServerMiddleware
Instance Attribute Details
#config ⇒ Object Originally defined in module ServerMiddleware
Returns the value of attribute config.
Class Method Details
.shared_instance? ⇒ Boolean
One instance per chain entry, shared by every job (Middleware::Chain::Entry): #call and its helpers keep their state in locals and write no ivar.
61 |
# File 'lib/wurk/middleware/timeout.rb', line 61 def self.shared_instance? = true |
Instance Method Details
#call(_worker, job, _queue, &block) ⇒ Object
A named block, not &: Ruby 3.3.0 rejects an anonymous block forwarded
from inside another block (fixed in 3.3.1, but within the gem's floor).
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/wurk/middleware/timeout.rb', line 65 def call(_worker, job, _queue, &block) attempt = bound_for(job) remaining = deadline_for(job) timer = (attempt || remaining) && watchdog return yield unless timer within_deadline(timer, job, remaining) do within_attempt(timer, job, attempt, &block) end end |