Threading and cancellation audit¶
spaCR runs analysis entry points through spacr.qt.bridge.PipelineWorker
on a dedicated QThread. Large imports, image decoding, model fitting and
plate processing therefore do not run on Qt’s GUI thread. Stop is cooperative:
spaCR never uses QThread.terminate() on an analysis that may be writing an
array, TIFF, database transaction, or checkpoint.
Safe cancellation boundaries¶
The worker installs a thread-local spacr.cancellation.CancellationToken.
Long workflows call spacr.cancellation.checkpoint() only after a durable
unit, or before the next unit begins:
Mask checks between source plates, object types, and Cellpose batches.
Measure keeps at most one pool-width field batch outstanding and checks after every batch has returned and saved its results.
Format Converter checks before source groups; its atomic
spacr.checkpoint.CheckpointStorerecord checks after each complete field.UMAP checks before each grid/adaptive trial. Completed trial artifacts and adaptive state are persisted before cancellation is raised.
Batch Runner checks between jobs. While a child
spacr-runprocess is active it polls the token, terminates that child, persists the current job as resumable, and then stops the queue.
A Stop click therefore may wait for the current unit. The console reports
Stopped safely when the boundary is reached, and the reproducibility
manifest records status: cancelled rather than a failure traceback.
Shutdown behavior¶
The process-wide spacr.qt.bridge.RunRegistry owns strong references to
every active worker and thread. On application shutdown it requests
cancellation for all jobs and waits against one bounded deadline. If any job
has not reached a safe boundary, the close event is refused and the live
references are retained. The user can close again after the current unit
finishes. The same rule applies when an individual module screen is closed.
PipelineWorker.finished invokes QThread.quit directly because that Qt
method is thread-safe. This avoids a shutdown deadlock in which the GUI thread
waits for a worker while the worker’s queued quit request waits for the GUI
event loop.
API usage¶
Headless and plugin pipelines use the same small API:
from spacr.cancellation import checkpoint
for field in fields:
checkpoint() # previous field is durable; next has not started
process_field(field)
write_field_atomically(field)
Calling checkpoint() outside a managed GUI worker is a no-op. Code that
catches broad Exception values must re-raise
spacr.cancellation.PipelineCancelled; treating it as a failed field
would defeat the worker’s distinct cancellation status.
Stress coverage¶
tests/qt/test_threading_cancellation_audit.py exercises fifty rapid
Start/Stop cycles, registry-wide shutdown, repeated cancellation, screen close
while active, and refusal to destroy a stubborn worker.
tests/test_cancellation.py verifies thread isolation, idempotence,
durability-before-cancel, resumable queues, and termination of an active batch
subprocess.
Cooperative cancellation for long-running spaCR pipelines.
Qt cannot safely kill a Python thread that may be writing a TIFF, committing a
SQLite transaction, or updating several related artifacts. Instead, the GUI
installs one CancellationToken in each worker thread and pipeline code
calls checkpoint() only at boundaries where the previous unit is fully
durable and the next unit has not started.
The module is intentionally standard-library only. Core workflows may import it without importing Qt, torch, or any GUI dependency.
- class spacr.cancellation.CancellationToken(reason: str = 'cancelled by the user')[source]¶
Bases:
objectThread-safe, idempotent cancellation request shared with one worker.
- Parameters:
reason – default user-facing reason raised by
checkpoint().
- cancel(reason: str | None = None) bool[source]¶
Request cancellation and return True only for the first request.
Repeated Stop clicks are harmless and do not replace the original reason, which keeps logs and manifests deterministic.
- checkpoint() None[source]¶
Raise
PipelineCancelledwhen cancellation was requested.
- exception spacr.cancellation.PipelineCancelled[source]¶
Bases:
ExceptionRaised at a safe boundary after cancellation has been requested.
This is a normal control-flow outcome, not a pipeline failure. Callers that catch broad
Exceptionvalues must re-raise it so the worker can record the run ascancelledrather than as a failed item.
- spacr.cancellation.cancellation_requested() bool[source]¶
Return whether the calling worker has a pending cancellation request.
- spacr.cancellation.checkpoint() None[source]¶
Stop at this safe boundary when requested; otherwise do nothing.
Calls outside a managed worker are intentionally no-ops, so the same pipeline functions work through the GUI, CLI, notebooks, and tests.
- spacr.cancellation.current_token() CancellationToken | None[source]¶
Return the token installed for the calling thread, if any.
- spacr.cancellation.installed_token(token: CancellationToken) Iterator[CancellationToken][source]¶
Install
tokenfor this thread and restore any prior token on exit.