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
Attributes included from ServerMiddleware
Instance Method Summary collapse
-
#call(_worker, job, _queue) ⇒ Object
The block is passed on rather than taken as
&block: reifying it would allocate a Proc for every job in the process, bounded or not.
Methods included from ServerMiddleware
Instance Method Details
#call(_worker, job, _queue) ⇒ Object
The block is passed on rather than taken as &block: reifying it would
allocate a Proc for every job in the process, bounded or not.
61 62 63 64 65 66 67 68 69 70 |
# File 'lib/wurk/middleware/timeout.rb', line 61 def call(_worker, job, _queue) 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) { yield } # rubocop:disable Style/ExplicitBlockArgument end end |