When workers fail#
What happens when your process function (or the node it runs on) is broken — and what daisy tells you about it.
A minimal failing task#
import daisy
def process(block):
raise RuntimeError("broken import on this node")
task = daisy.Task(
"my-step",
total_roi=daisy.Roi((0,), (80,)),
read_roi=daisy.Roi((0,), (10,)),
write_roi=daisy.Roi((0,), (10,)),
process_function=process,
max_workers=4,
max_worker_restarts=3,
)
daisy.run_blockwise([task])
What daisy 1.x did#
Daisy 1.x had no limit on worker restarts: a worker whose process died was
reaped and immediately respawned, forever
(reap_dead_workers + inc_num_workers in daisy 1.x’s
task_worker_pools.py). For a worker that crashes on startup this meant an
infinite respawn loop — tens of new worker processes per second, zero
blocks processed, and a run_blockwise call that never returns. On a
cluster, that’s requested (billed) nodes spinning until someone notices and
kills the job by hand.
What daisy does now#
The runner replaces a failed worker at most max_worker_restarts times
(default 10). If the cap is exhausted while blocks remain, the task is
abandoned: remaining blocks are accounted as orphaned, downstream tasks
that depended on them are abandoned transitively, and — most importantly —
the run terminates and tells you why:
RuntimeError: task 'my-step' was abandoned (worker restart cap exhausted):
workers failed 4 times, 3 restarts performed; 8 of 8 blocks were orphaned.
Last worker error: RuntimeError: broken import on this node
Fix the worker error or increase Task(max_worker_restarts=...); use
Server().run_blockwise(...) to inspect task states without raising.
The original worker exception is captured and carried into the error — you don’t have to dig through per-worker log files to learn what broke.
Inspecting instead of raising#
run_blockwise raising on abandonment is the right default for pipelines: a
run that produced no output should not look like a clean exit. If you want
to handle abandonment programmatically, use the server API, which returns
per-task states and never raises for abandonment:
server = daisy.Server()
states = server.run_blockwise([task])
state = states["my-step"]
state.abandoned # True
state.abandon_reason # "worker restart cap exhausted"
state.last_worker_error # "RuntimeError: broken import on this node"
state.orphaned_count # 8
See Task abandonment and the typestate lifecycle for the mechanics (counter invariants, the typestate lifecycle, and how in-flight messages from dying workers are handled during the transition).