Cancellation & timeouts
The Ruby worker uses cooperative cancellation. It never force-kills a job
thread — interrupting a thread mid-call (an in-flight HTTP request, a database
query) can leave a connection in a corrupt state. Instead, every job is handed a
cancellation handle on its ctx, and a well-behaved handler watches it.
Cancellation can fire for three reasons:
- Server cancel — the job was cancelled (e.g. from the dashboard or the cancel API). The worker learns this from a heartbeat response and trips the handle.
- Timeout — the job ran past its configured timeout.
- Host shutdown — the process received
SIGTERM/SIGINTand is draining.
In all three cases the job is ultimately acked failed once it returns.
Honoring cancellation
Check the handle at safe points and return (or raise) early:
def perform(payload, ctx)
payload["ids"].each do |id|
return if ctx.cancelled? # stop between units of work
process(id)
end
end
For a loop where you'd rather abort with an exception:
def perform(payload, ctx)
loop do
ctx.check_cancellation! # raises if cancellation was requested
do_one_chunk
end
end
When you need to wait, wait on the handle instead of sleep so the wait wakes
the instant cancellation is requested:
def perform(payload, ctx)
# returns true immediately if cancelled, else after the delay
woke_to_cancel = ctx.cancellation.wait(5.0)
return if woke_to_cancel
# ...
end
The limit of cooperative cancellation
A handler that never checks the handle — a tight CPU loop, or a single blocking call with no timeout — cannot be stopped by the SDK. It runs until it finishes on its own. The server still protects you: if a worker stops heartbeating a job (because it's wedged, or the process died), the server eventually reclaims the job and re-delivers it. But that's a coarse backstop measured in the job's timeout window, not an instant abort.
Two practical rules:
- Set socket/read timeouts on your own I/O at or below the job's timeout, so a stuck network call returns instead of hanging forever.
- Check
ctx.cancelled?at the boundaries of long loops.
Liveness: heartbeat immediately, then on a cadence
The worker sends one heartbeat the moment it claims a job, then continues on a
fixed cadence for as long as the job runs. The immediate first beat matters: it
tells the server the job is genuinely in progress, which puts a long-running job
in the more forgiving reclaim window. If heartbeats stop arriving — a wedged or
dead worker — the server reclaims the job and re-delivers it elsewhere. This is
exactly why handlers must be idempotent (dedupe on ctx.job_id).
Graceful drain
On SIGTERM/SIGINT, worker.run stops claiming new jobs, trips cancellation
on any freshly-claimed job, waits up to the
shutdown grace for in-flight jobs to
finish, and then exits. Keep the grace under your platform's termination window
so jobs drain before the process is hard-killed; anything still running at the
hard kill is re-delivered by the server.