Cancellation¶
Long-running workflows — interactive GUIs, server-driven pipelines, or
just any compute that may need to stop mid-flight — should be cancellable.
BioImageFlow exposes a single cancel() method
that flips a thread-safe flag the engine checks before each row.
Requesting cancellation¶
Workflow.cancel() sets a flag on the workflow object. The flag is
thread-safe (backed by a threading.Event), so a GUI thread can call
it while compute() is running on a background thread:
wf.cancel()
The engine checks Workflow.cancel_requested before each node and
between rows. When the flag is set, the engine raises
WorkflowCancelledError from the in-flight
compute() call.
In-flight rows¶
When a row is mid-execution at the moment the flag flips, the engine
calls task.cancel() on the corresponding Wetlands handle. The cancel
is cooperative: Wetlands tells the worker process that cancellation
was requested, but cannot interrupt arbitrary native code. Tools that
expect to run for a long time should poll the cancellation hint
themselves.
Tool-side cooperative cancellation¶
A ProcessingTool opts in by adding a task keyword argument to
process_row (or process_batch). The runtime injects the Wetlands
RemoteTaskHandle; the tool checks task.cancel_requested at safe
points in its loop:
class SlowSegment(ProcessingTool):
display_name = "Slow Segment"
environment = EnvironmentSpec(name="torch", dependencies={...})
class Inputs:
image: Annotated[Path, ImageSpec()]
class Outputs:
mask: Annotated[Path, ImageSpec()] = Template("{image.stem}_mask.tif")
def process_row(self, arguments, *, task=None):
img = self._load(arguments.image)
for i, tile in enumerate(self._tiles(img)):
if task is not None and task.cancel_requested:
raise RuntimeError("cancelled")
self._process_tile(tile)
...
The runtime detects the keyword via signature inspection — tools without
a task parameter run unchanged.
DataFrameTools are not cooperatively cancellable: transform and
merge_dataframes run on the main thread, so a long pandas operation
will complete before the cancel takes effect.
Surfacing the cancellation¶
Once the engine raises WorkflowCancelledError,
compute() propagates it to the caller. Catch it where you call
compute():
from bioimageflow.engine import WorkflowCancelledError
try:
result = wf.compute(target)
except WorkflowCancelledError:
# GUI: render the partial state, leave already-cached nodes alone
...
Worked example: cancellable background compute¶
A typical GUI pattern: spawn compute() on a thread, expose a “Cancel”
button that calls wf.cancel():
import threading
from bioimageflow.engine import WorkflowCancelledError
def run():
try:
wf.compute(target)
except WorkflowCancelledError:
print("cancelled")
thread = threading.Thread(target=run, daemon=True)
thread.start()
# Later — perhaps from a GUI button handler:
wf.cancel()
thread.join()