Progress and Cancellation¶
Hosts driving a long-running compute() need two things: a stream of
progress events to render, and a Cancel button that actually stops the
workflow. BioImageFlow exposes both through stable, thread-safe APIs.
ProgressEvent¶
The engine fires a ProgressEvent for every
node-level transition and every per-row update. Pass a callback at
workflow construction:
from bioimageflow import Workflow, ProgressEvent
def on_progress(ev: ProgressEvent):
print(ev.node_name, ev.status, ev.row, ev.total_rows)
wf = Workflow(on_progress=on_progress)
Fields:
Field |
Description |
|---|---|
|
Scoped node name (nested workflow scope included). |
|
One of |
|
V1 result key when the event is tied to a cacheable node result. |
|
Selected record ID for |
|
Index of the current row (per-row events). |
|
Total rows in this node’s input (per-row events). |
|
Optional sub-row text (e.g., from
|
|
Optional sub-row progress numerator. |
|
Optional sub-row progress denominator. |
|
Wall-clock seconds-since-epoch. |
result_key and record_id are the only cache identities exposed by
progress events. Diagnostic signatures are separate debug values and are not
cache keys. During a cache miss, "started" can include
result_key while record_id remains empty until publication succeeds.
The engine serializes callbacks via an internal lock — your
on_progress callable does not need to be thread-safe. It runs on
the engine’s progress dispatch path; expensive work in the callback
will slow down execution, so push UI updates onto a dispatch queue
rather than re-rendering inline.
Sub-row progress¶
Tools that drive RemoteTaskHandle.update(message=, current=, maximum=)
inside process_row produce status="row_progress" events with
message / current / maximum populated. Use these for tools
with a clear inner loop (training epochs, tile counts, optimizer
iterations); a one-row tool with no inner progress simply omits them.
Workflow.cancel() from a GUI¶
Workflow.cancel flips a
thread-safe flag. The engine checks it before each node and between
rows; in-flight rows have task.cancel() called on their Wetlands
handle (cooperative — see Cancellation).
The classic GUI wiring:
import threading
from bioimageflow.engine import WorkflowCancelledError
def run_compute():
try:
wf.compute(target)
except WorkflowCancelledError:
pass # render the partial state — see below
thread = threading.Thread(target=run_compute, daemon=True)
thread.start()
def on_cancel_button():
wf.cancel()
thread.join()
The flag is reset at the start of each compute() call, so calling
wf.cancel() before the next compute starts is harmless.
Cooperative tool-side check¶
Long-running ProcessingTool.process_row implementations should opt
in to cooperative cancellation by accepting the task keyword
argument and checking task.cancel_requested at safe points. The
full pattern is documented in Cancellation; this
page only re-states that the engine is the side that requests
cancellation — the tool is the side that honours it.
Handling WorkflowCancelledError¶
The engine raises WorkflowCancelledError
from compute() once the cancel flag is observed. Hosts catch it
where they call compute():
already-cached nodes are untouched — the cache survives;
partially-completed nodes leave their cache entries in their previous state (the new entry is not committed);
subsequent
plan()calls reflect the post-cancel state, so the GUI can re-render “needs rebuild” / “unexecuted” indicators correctly.
A typical handler:
try:
result = wf.compute(target)
except WorkflowCancelledError:
render_cancelled_state(wf.plan())