Mutex watershed pipeline (block- and worker-function modes)#

A connectomics-flavoured pipeline:

  1. Generate short- and long-range nearest-neighbour affinities from a ground-truth segmentation with lsd_lite.get_affs (in a real pipeline these would come from a UNet — we derive them from GT so the example is self-contained).

  2. Run mwatershed.agglom blockwise, driven by daisy.

The example walks through four runs that demonstrate daisy’s two dispatch modes and its persistent done-marker behaviour:

  • Run 1 — clean process function, every block runs, output matches GT structure.

  • Run 2 — same task again. Daisy consults the persistent done-marker and skips every block.

  • Run 3 — task.reset() clears the marker; a buggy process function then fails on one tile. After max_retries=2 retries the block is permanently failed; the other tiles complete normally.

  • Run 4 — fixed process function, no reset. Only the previously failing tile re-executes; the 15 already-succeeded tiles are skipped via the marker. This is the production resume-from-failure workflow.

Configuration#

Two flags select the dispatch mode and the runner. Override from the CLI with --mode {block_function|worker_function} and --multiprocessing / --no-multiprocessing. The defaults work unchanged in a notebook (parse_known_args ignores Jupyter’s own argv noise).

import argparse

_parser = argparse.ArgumentParser(add_help=False)
_parser.add_argument(
    "--mode",
    choices=["block_function", "worker_function"],
    default=None,
)
_parser.add_argument(
    "--multiprocessing",
    action=argparse.BooleanOptionalAction,
    default=None,
)
_args, _ = _parser.parse_known_args()

MODE = _args.mode or "block_function"
MULTIPROCESSING = True if _args.multiprocessing is None else _args.multiprocessing

if MODE == "worker_function" and not MULTIPROCESSING:
    raise SystemExit(
        "MODE='worker_function' requires MULTIPROCESSING=True — the serial "
        "runner only understands per-block callbacks."
    )
print(f"MODE={MODE}  MULTIPROCESSING={MULTIPROCESSING}")
MODE=block_function  MULTIPROCESSING=True
import tempfile
import time
from pathlib import Path

import daisy.logging as gl
import daisy.v2 as daisy
import lsd_lite
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import mwatershed
import numpy as np
import zarr

_TMP = Path(tempfile.mkdtemp(prefix="daisy_mws_"))
gl.set_log_basedir(_TMP / "logs")
# Tee worker stdout/stderr to both file and terminal so the run-3
# traceback is visible inline. Default `"file"` is the production-grade
# choice; `"both"` is the iteration-friendly one.
gl.set_log_mode("both")
print(f"output paths under: {_TMP}")
output paths under: /tmp/daisy_mws_ad_cltt3

Build the input#

A 512×512 GT with 40 random discs, then short- and long-range nearest-neighbour affinities derived from it. mutex watershed wants affinities centred at zero (positive merges, negative splits), so we subtract 0.5.

H, W = 512, 512
N_DISCS = 40
BLOCK = 128
NUM_WORKERS = 4
TASK_ID = "mws"
MARKER_PATH = _TMP / "markers.zarr" / TASK_ID
FAILING_TILE = (1, 2)  # block-row, block-col in the 4×4 grid

NEIGHBORHOOD = [
    [0, 1],  # right (short-range, attractive)
    [1, 0],  # down  (short-range, attractive)
    [0, 3],  # right-3 (long-range, repulsive)
    [3, 0],  # down-3  (long-range, repulsive)
]


def label_to_rgb(seg, bg=(0, 0, 0)):
    seg = np.asarray(seg).astype(np.int64)
    ids, inverse = np.unique(seg, return_inverse=True)
    rng = np.random.default_rng(42)
    colours = 0.2 + 0.8 * rng.random((ids.size, 3))
    for i, label in enumerate(ids):
        if label == 0:
            colours[i] = bg
    return (colours[inverse].reshape(*seg.shape, 3) * 255).astype(np.uint8)


def random_disc_segmentation(height, width, n_discs, seed=0):
    rng = np.random.default_rng(seed)
    gt = np.zeros((height, width), dtype=np.uint32)
    y, x = np.ogrid[:height, :width]
    for label in range(1, n_discs + 1):
        cy = rng.integers(20, height - 20)
        cx = rng.integers(20, width - 20)
        r = rng.integers(15, 45)
        gt[(y - cy) ** 2 + (x - cx) ** 2 <= r * r] = label
    return gt


GT = random_disc_segmentation(H, W, N_DISCS)
AFFINITIES = (
    lsd_lite.get_affs(GT, NEIGHBORHOOD, dist="equality").astype(np.float32) - 0.5
)
OUTPUT = np.zeros((H, W), dtype=np.uint32)

fig, axes = plt.subplots(1, 3, figsize=(13, 4))
axes[0].imshow(label_to_rgb(GT), interpolation="nearest")
axes[0].set_title(f"GT — {N_DISCS} discs")
axes[0].axis("off")
axes[1].imshow(AFFINITIES[:3].transpose([1, 2, 0]) + 0.5, vmin=0, vmax=1)
axes[1].set_title("affinities (channels 0–2 as RGB)")
axes[1].axis("off")
axes[2].imshow(label_to_rgb(GT), interpolation="nearest")
for r in range(BLOCK, H, BLOCK):
    axes[2].axhline(r - 0.5, color="yellow", linewidth=1.0)
for c in range(BLOCK, W, BLOCK):
    axes[2].axvline(c - 0.5, color="yellow", linewidth=1.0)
fr, fc = FAILING_TILE
axes[2].add_patch(
    mpatches.Rectangle(
        (fc * BLOCK - 0.5, fr * BLOCK - 0.5),
        BLOCK,
        BLOCK,
        fill=False,
        edgecolor="red",
        linewidth=2.5,
    )
)
axes[2].set_title(f"{H // BLOCK}×{W // BLOCK} block grid (failing tile in red)")
axes[2].axis("off")
fig.tight_layout()
../_images/7052445021cbc2e53122078ae2721e7c3c67fb43b3d826a4c511833629592fc9.png

Define process_block and the worker loop#

process_block(block) does the real work for one tile. The MODE="worker_function" path wraps it in a zero-arg worker() that loops on client.acquire_block() — that’s how daisy distinguishes the two shapes (inspect.getfullargspec on the callable’s arity).

Per-block label ids only have local meaning, so we shift each block’s labels into a disjoint global range keyed on block_id — fully deterministic regardless of which worker picks which block up.

def _tile_idx(block):
    off = list(block.write_roi.offset)
    return (off[0] // BLOCK, off[1] // BLOCK)


def process_block(block):
    off = list(block.write_roi.offset)
    shape = list(block.write_roi.shape)
    r0, c0 = off[0], off[1]
    rs, cs = shape[0], shape[1]
    tile = AFFINITIES[:, r0 : r0 + rs, c0 : c0 + cs].astype(np.float64)
    local = mwatershed.agglom(tile, offsets=NEIGHBORHOOD).astype(np.uint32)
    block_offset = block.block_id[1] * BLOCK * BLOCK
    OUTPUT[r0 : r0 + rs, c0 : c0 + cs] = np.where(local > 0, local + block_offset, 0)


def process_block_buggy(block):
    if _tile_idx(block) == FAILING_TILE:
        raise RuntimeError(
            f"simulated failure on tile {FAILING_TILE} (block_id={block.block_id})"
        )
    process_block(block)


def expensive_model_load():
    """Stand-in for per-worker one-time setup (model, mmap, DB handle)."""
    time.sleep(0.05)


def make_worker(process_fn):
    """Return a zero-arg worker that drives its own block loop. Lets us
    reuse `process_block` in worker-function mode."""

    def worker():
        expensive_model_load()
        client = daisy.Client()
        while True:
            try:
                with client.acquire_block() as block:
                    if block is None:
                        break
                    process_fn(block)
            except Exception:
                # Per-worker logs already captured the traceback;
                # don't let one bad block kill the whole worker.
                pass

    return worker


def make_task(process_fn):
    py_fn = process_fn if MODE == "block_function" else make_worker(process_fn)
    return daisy.Task(
        task_id=TASK_ID,
        total_roi=daisy.Roi([0, 0], [H, W]),
        read_roi=daisy.Roi([0, 0], [BLOCK, BLOCK]),
        write_roi=daisy.Roi([0, 0], [BLOCK, BLOCK]),
        process_function=py_fn,
        read_write_conflict=False,
        max_workers=NUM_WORKERS,
        max_retries=2,
        tracking_path=str(MARKER_PATH),
    )

Run helpers#

run invokes run_blockwise, snapshots OUTPUT, and prints the per-task counters. show_run plots the output snapshot alongside the on-disk done-marker so you can see what each run actually did (or didn’t do) to both the in-memory output and the persistent state.

RUNS: list[dict] = []


def show_run(run_info):
    """Two-panel plot: output snapshot left, marker grid right. The
    failing tile is outlined in red on the output panel for visual
    reference."""
    fig, axes = plt.subplots(1, 2, figsize=(9, 4.5))

    axes[0].imshow(label_to_rgb(run_info["snapshot"]), interpolation="nearest")
    axes[0].add_patch(
        mpatches.Rectangle(
            (fc * BLOCK - 0.5, fr * BLOCK - 0.5),
            BLOCK,
            BLOCK,
            fill=False,
            edgecolor="red",
            linewidth=2.0,
        )
    )
    axes[0].set_title(
        f"output after {run_info['label']}\n"
        f"executed={run_info['executed']} skipped={run_info['skipped']} "
        f"failed={run_info['failed']}"
    )
    axes[0].axis("off")

    # The tracking store is a zarr group; `done` holds one byte per block.
    marker = np.asarray(zarr.open_group(str(MARKER_PATH), mode="r")["done"][:]).reshape(
        H // BLOCK,
        W // BLOCK,
    )
    axes[1].imshow(marker, cmap="Greens", vmin=0, vmax=1, interpolation="nearest")
    axes[1].set_title(f"done-marker  ({int(marker.sum())}/{marker.size} tiles done)")
    axes[1].set_xlabel("block col")
    axes[1].set_ylabel("block row")
    for (r, c), v in np.ndenumerate(marker):
        axes[1].text(
            c,
            r,
            str(int(v)),
            ha="center",
            va="center",
            color="white" if v else "black",
        )

    fig.tight_layout()


def run(label, task):
    OUTPUT.fill(0)
    t0 = time.perf_counter()
    if MULTIPROCESSING:
        # `Server` returns the per-task `TaskState` dict; the top-level
        # `daisy.run_blockwise` function returns just a bool.
        states = daisy.Server().run_blockwise([task], progress=False)
    else:
        from daisy._runner import _run_serial

        states = _run_serial([task])
    elapsed = time.perf_counter() - t0
    s = states[TASK_ID]
    snap = OUTPUT.copy()
    # `skipped_count` is a flag on top of `completed_count`: a block
    # that was pre-skipped via the marker counts toward both. We split
    # them here so `executed` is unambiguously "blocks that ran".
    executed = s.completed_count - s.skipped_count
    RUNS.append(
        {
            "label": label,
            "elapsed_ms": elapsed * 1e3,
            "executed": executed,
            "skipped": s.skipped_count,
            "failed": s.failed_count,
            "orphaned": s.orphaned_count,
            "snapshot": snap,
        }
    )
    print(
        f"{label:24s}  executed={executed:>2}  "
        f"skipped={s.skipped_count:>2}  failed={s.failed_count:>2}  "
        f"elapsed={elapsed * 1e3:7.1f} ms"
    )

Run 1 — clean run#

First invocation. Every block executes; the marker fills in for all 16 tiles.

clean_task = make_task(process_block)
run("1: clean", clean_task)
assert RUNS[-1]["executed"] == 16
assert RUNS[-1]["skipped"] == 0
show_run(RUNS[-1])
Execution Summary
-----------------

    task     blocks   completed  skipped  failed  orphaned
    ──────  ───────  ──────────  ───────  ──────  ────────
    mws ✔        16          16        0       0         0

Resource Utilization
--------------------

  mws   16 blocks, 4 workers, 349.4 ms wall
    blocks   round trip 15.4 ms ± 6.8 ms
    workers  startup 249.8 ms ± 36.1 ms   idle 129.1 µs ± 304.1 µs   teardown 35.0 ms ± 5.6 ms
             exits 4 clean
        id    startup  blocks       idle         idle / block    compute      compute / block   teardown  exit
         0   263.1 ms       3    29.0 µs      9.7 µs ± 6.0 µs    48.7 ms    16.2 ms ± 12.0 ms    28.4 ms  clean
         1   286.8 ms       2    19.9 µs     10.0 µs ± 0.2 µs    27.2 ms     13.6 ms ± 3.8 ms    35.1 ms  clean
         2   248.3 ms       3     1.4 ms  459.7 µs ± 683.6 µs    57.4 ms     19.1 ms ± 5.7 ms    42.2 ms  clean
         3   201.2 ms       8   636.9 µs    79.6 µs ± 69.3 µs   112.9 ms     14.1 ms ± 6.1 ms    34.4 ms  clean
1: clean                  executed=16  skipped= 0  failed= 0  elapsed=  365.0 ms
../_images/892ddd776b9482810f25b7cf7544f1b321f0744e30b99493d94a6944775c758a.png

Run 2 — re-run without resetting#

Same task, run again. The tracking store lives under set_tracking_basedir(...) (or the log basedir) for tasks with the default tracking_path=None; here we set the path explicitly so we can inspect it below. Either way, the scheduler’s pre-check sees every block already done and short-circuits without ever calling process_block. The post-run summary shows skipped=16.

run("2: re-run, no reset", clean_task)
assert RUNS[-1]["skipped"] == 16
assert RUNS[-1]["executed"] == 0
show_run(RUNS[-1])
# Output panel is all background colour: `OUTPUT.fill(0)` zeroed it
# before the run, and no `process_block` ran. Marker is unchanged
# from run 1 (still 16/16) — that's the whole point of the skip.
daisy server at 10.1.0.14:33683 is not reachable; assuming the run has ended — this worker will exit without processing blocks
daisy server at 10.1.0.14:33683 is not reachable; assuming the run has ended — this worker will exit without processing blocks
Execution Summary
-----------------

    task     blocks   completed  skipped  failed  orphaned
    ──────  ───────  ──────────  ───────  ──────  ────────
    mws ✔        16          16       16       0         0

Resource Utilization
--------------------

  mws   16 blocks (16 skipped), 4 workers, 283.6 ms wall
    workers  startup 208.8 ms   idle —   teardown 58.8 ms ± 16.5 ms
             exits 4 clean
        id    startup  blocks       idle         idle / block    compute      compute / block   teardown  exit
         0   208.8 ms       0          —                    —          —                    —    40.9 ms  clean
         1          —       0          —                    —          —                    —    48.9 ms  clean
         2          —       0          —                    —          —                    —    70.9 ms  clean
         3          —       0          —                    —          —                    —    74.6 ms  clean
2: re-run, no reset       executed= 0  skipped=16  failed= 0  elapsed=  359.9 ms
../_images/72d1ce965b2e164f78e4a431594f06271229529dffc481211a102df79225fc08.png

Run 3 — reset, then run a buggy process function#

task.reset() deletes the on-disk marker so the next invocation really executes every block. We then swap in process_block_buggy, which raises on FAILING_TILE.

Behavior diverges by runner here:

  • Multiprocessing: with max_retries=2 the scheduler hands the failing block back out twice more before marking it permanently failed; the other 15 tiles complete normally.

  • Serial: the in-process debugging path is fail-fast. The original RuntimeError propagates out of run_blockwise on the first attempt — no retries, no buried Failed status in a post-run summary. Runs 3 and 4 below only make sense for the mp runner; in serial mode we just demonstrate the fail-fast behavior.

clean_task.reset()
buggy_task = make_task(process_block_buggy)

if MULTIPROCESSING:
    run("3: buggy, after reset", buggy_task)
    assert RUNS[-1]["failed"] == 1
    assert RUNS[-1]["executed"] == 15
    show_run(RUNS[-1])
    # The output panel has 15 tiles populated and one black square at
    # the failing tile (red outline). The marker shows 15/16 done —
    # the failed tile is still 0 so a future run will retry it.
else:
    try:
        run("3: buggy, after reset", buggy_task)
    except RuntimeError as e:
        print(f"3: buggy, after reset    raised RuntimeError: {e}")
 WARN block failed block_id=mws/7 error=Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 568, in acquire_block
    yield block
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))

 WARN block failed block_id=mws/7 error=Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 568, in acquire_block
    yield block
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))

block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
worker function mws failed: daisy worker subprocess exited with code 1
 WARN spawn function failed worker_id=1 error=block processing failed: Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 647, in wrapped
    return orig(context=context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_worker_processes.py", line 325, in _spawn_worker_process
    raise _WorkerExitError(
_WorkerExitError: daisy worker subprocess exited with code 1
worker stderr tail:
block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
 WARN worker exited with error worker_id=1 task_id=mws
block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
worker function mws failed: daisy worker subprocess exited with code 1
 WARN spawn function failed worker_id=0 error=block processing failed: Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 647, in wrapped
    return orig(context=context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_worker_processes.py", line 325, in _spawn_worker_process
    raise _WorkerExitError(
_WorkerExitError: daisy worker subprocess exited with code 1
worker stderr tail:
block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
 WARN worker exited with error worker_id=0 task_id=mws
 WARN block failed block_id=mws/7 error=Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 568, in acquire_block
    yield block
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
worker function mws failed: daisy worker subprocess exited with code 1
 WARN spawn function failed worker_id=4 error=block processing failed: Traceback (most recent call last):
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 647, in wrapped
    return orig(context=context)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_worker_processes.py", line 325, in _spawn_worker_process
    raise _WorkerExitError(
_WorkerExitError: daisy worker subprocess exited with code 1
worker stderr tail:
block ('mws', 7) failed: simulated failure on tile (1, 2) (block_id=('mws', 7))
block ('mws', 7) failed:
Traceback (most recent call last):
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 90, in <module>
    _run()
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 86, in _run
    _run_block_loop(function)
  File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
    process_function(block)
  File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))
Execution Summary
-----------------

    task     blocks   completed  skipped  failed  orphaned
    ──────  ───────  ──────────  ───────  ──────  ────────
    mws ✗        16          15        0       1         0

    First failure in task 'mws':
      Traceback (most recent call last):
        File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_task.py", line 568, in acquire_block
          yield block
        File "/home/runner/work/daisy/daisy/daisy-py/python/daisy/_subprocess_worker.py", line 54, in _run_block_loop
          process_function(block)
        File "/tmp/ipykernel_3948/573184097.py", line 19, in process_block_buggy
      RuntimeError: simulated failure on tile (1, 2) (block_id=('mws', 7))

    See worker logs for details under /tmp/daisy_mws_ad_cltt3/logs/mws/

Resource Utilization
--------------------

  mws   15 blocks, 5 workers, 472.3 ms wall
    blocks   round trip 9.4 ms ± 1.5 ms
             3 failed attempt(s)
    workers  startup 216.4 ms ± 51.3 ms   idle 36.1 µs ± 41.3 µs   teardown 27.0 ms ± 1.6 ms
             exits 2 clean  3 error
        id    startup  blocks       idle         idle / block    compute      compute / block   teardown  exit
         0   239.0 ms       6   235.6 µs    39.3 µs ± 45.3 µs    44.6 ms      7.4 ms ± 3.2 ms          —  error
         1   238.9 ms       2    43.3 µs    21.7 µs ± 21.3 µs    15.1 ms      7.5 ms ± 5.8 ms          —  error
         2   242.7 ms       4   140.3 µs    35.1 µs ± 50.5 µs    40.3 ms     10.1 ms ± 1.1 ms    25.8 ms  clean
         3   236.5 ms       5   225.8 µs    45.2 µs ± 46.8 µs    45.9 ms      9.2 ms ± 1.2 ms    28.1 ms  clean
         4   124.7 ms       1     4.5 µs               4.5 µs     3.0 ms               3.0 ms          —  error
3: buggy, after reset     executed=15  skipped= 0  failed= 1  elapsed=  484.5 ms
../_images/8b936955e6be633aa75fb11134e94cfbfb3a2fde2c84635a30a87a70b0f8c47a.png

Run 4 — fix the bug, resume#

Pretend the traceback above pointed us at our bug and we’ve patched the function. Re-running the task without reset() consults the marker first: the 15 successfully-completed tiles are skipped, only the previously-failing tile re-executes. This is the production resume-from-failure workflow — write your processor, run, fix what failed, re-run, only pay for what’s actually new.

Skipped in serial mode: the buggy run above raised before any block was marked done, so there’s nothing partial to resume from.

if MULTIPROCESSING:
    fixed_task = make_task(process_block)
    run("4: fixed, resume", fixed_task)
    assert RUNS[-1]["executed"] == 1
    assert RUNS[-1]["skipped"] == 15
    show_run(RUNS[-1])
    # The marker is now 16/16: the previously-failing tile got marked
    # done by run 4. The output panel, by contrast, only shows the one
    # newly-executed tile filled in — `OUTPUT` is an in-memory numpy
    # array that we zero before each run, and skipped blocks (by
    # definition) never re-run their `process_block`. In a real
    # pipeline the output is usually a persistent zarr array so the
    # 15 already-written tiles survive across runs and a resume
    # produces a complete result.

plt.show()
daisy server at 10.1.0.14:46595 is not reachable; assuming the run has ended — this worker will exit without processing blocks
Execution Summary
-----------------

    task     blocks   completed  skipped  failed  orphaned
    ──────  ───────  ──────────  ───────  ──────  ────────
    mws ✔        16          16       15       0         0

Resource Utilization
--------------------

  mws   16 blocks (15 skipped), 4 workers, 276.0 ms wall
    blocks   round trip 12.4 ms
    workers  startup 236.6 ms ± 4.9 ms   idle 55.6 µs   teardown 33.2 ms ± 5.1 ms
             exits 4 clean
        id    startup  blocks       idle         idle / block    compute      compute / block   teardown  exit
         0   231.7 ms       1    55.6 µs              55.6 µs    12.4 ms              12.4 ms    27.1 ms  clean
         1          —       0          —                    —          —                    —    31.8 ms  clean
         2   241.4 ms       0          —                    —          —                    —    34.5 ms  clean
         3   236.6 ms       0          —                    —          —                    —    39.2 ms  clean
4: fixed, resume          executed= 1  skipped=15  failed= 0  elapsed=  284.0 ms
../_images/bd5ccba5a734c91b6627692d21f53c8e7429f526e7f84809a47d35fc0bbb1e4e.png