Using Background Jobs

Submitting a Job

Use context.jobs for work that should not block the interface, including remote requests, large transforms, training, prediction and expensive file IO.

A directly submitted job function should accept the reserved cancel_token keyword:

def work(*, dataset_id, cancel_token):
    if cancel_token.cancelled():
        return None

    return run_expensive_operation(dataset_id)

handle = context.jobs.submit(
    work,
    title="Build result",
    key=f"example.build:{dataset_id}",
    dataset_id=dataset_id,
    on_done=on_done,
    on_error=on_error,
)

The manager runs jobs through a shared ThreadPoolExecutor and returns a JobHandle.

Stable Keys

Use a stable key when only one copy of the same request should run at a time.

If the same key is submitted while a job is still active, the JobManager returns the existing handle instead of starting duplicate work. New completion callbacks are attached to that existing job.

Do not reuse one key for requests whose inputs or expected outputs differ.

Cancellation

Call handle.cancel() to request cancellation. This always sets the cooperative cancellation token and also attempts to cancel the underlying future if it has not started.

Running work must check the token at useful boundaries, for example:

  • between dataset batches;

  • between remote pages or requests;

  • between training or inference stages;

  • before expensive post-processing;

  • before writing final output.

Cancellation is cooperative. Python code that is already running is not stopped forcibly.

Completion

Use on_done and on_error for completion handling.

The JobManager captures the current Panel/Bokeh document when each callback is registered and schedules those callbacks back onto that document’s next UI tick. This makes normal widget and pane updates safe from job completion callbacks.

When no Panel document exists, such as in tests or non-UI use, callbacks run immediately.

Do not update Panel or Bokeh objects from inside the worker function itself.

Ownership

Jobs started by PluginManager-managed operations, such as job-backed actions and panel construction, are tracked against their owning plugin and can be cancelled when that plugin is disabled.

A panel that calls context.jobs.submit() directly should retain its own job handles and request cancellation from dispose() when that work should not outlive the panel.

def dispose(self):
    if self._disposed:
        return
    self._disposed = True

    if self._job_handle is not None:
        self._job_handle.cancel()
        self._job_handle = None

Stale Results

Cancellation alone is not enough for rapidly changing UI state. A remote request may finish just as focus, dataset or settings change.

Compare completion results with the current request identity, generation or dataset before replacing visible state.

CPU-Bound Work

The current JobManager uses threads. Native libraries that release the GIL can still benefit from the shared thread pool, but pure-Python CPU-heavy work may need a process-based or remote execution backend if it becomes a bottleneck.

Caution

Do not create unmanaged threads inside panel callbacks. Use the JobManager so work participates in shared scheduling, diagnostics, deduplication and cooperative cancellation.