Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BJob: Synchronous Execution, State Listeners, and Dependencies

Use Haiku BSupportKit jobs as explicit work state machines, understand synchronous Run semantics, and coordinate listeners and prerequisites safely.

BSupportKit::BJob packages a unit of work with a title, result, state, optional listeners, and prerequisite relationships. Its name can suggest a scheduler, but the current implementation’s Run() calls the job’s Execute() directly on the calling thread. It does not automatically create a worker thread, dispatch a progress dialog, or run an entire dependency graph. That distinction is essential for responsive applications.

Use BJob when an operation benefits from a visible state model and reusable execution contract. The caller still decides where Run() is invoked, how work is scheduled, what cancellation means, how dependencies are made runnable, and how listener callbacks reach the UI. Treat the class as a stateful job abstraction, not as a complete background-task service.

State transitions and one-shot execution

The header defines waiting-to-run, started, in-progress, succeeded, failed, and aborted states. In the current upstream implementation, Run() accepts a job only while it is waiting to run. It changes state to started and notifies listeners, marks the job in progress, calls Execute(), calls Cleanup() with the result, maps B_OK to succeeded and B_CANCELED to aborted, maps other results to failed, then notifies listeners of the terminal state.

This sequence makes Run() a synchronous call: it does not return until Execute() and Cleanup() finish. Calling it on a window’s message thread for a long task freezes the window. Put the job on a worker or an application-owned task queue when the work can block, then marshal UI updates back through a BMessenger or the correct looper. The scheduler is your code, not BJob.

The state machine also means the same instance is not a reusable template. A second Run() after the first leaves the waiting state is rejected with B_NOT_ALLOWED. Create a fresh job instance for a distinct run, or implement an explicit reset design only if the documented API supports it. Do not mutate private state to force an object back to waiting.

Result() and ErrorString() provide outcome information; a boolean “finished” flag loses useful distinctions. Preserve the returned status from Run(). B_CANCELED is a terminal result represented as aborted, not as success. A job that returns a different error should use failed and expose a concise, safe explanation through its error string.

Execute() and Cleanup() contracts

Subclass BJob and implement Execute(). Keep the work’s input snapshot and ownership explicit. If the operation reads a file, capture a stable entry_ref or other appropriate model key, then resolve and validate it when executing. Do not hold raw UI object pointers across a background boundary.

Cleanup() is called after Execute() returns in the current implementation. Use it for resources whose release must happen on both success and failure, and do not duplicate cleanup in every return branch. Keep cleanup safe for partial initialization. If a worker thread is used externally, the caller must still join or otherwise synchronize it before destroying the job and its fields.

BJob does not expose a built-in cancel request method in its public header. If cancellation is a requirement, define an application-owned cancellation signal that the job’s Execute() can poll at bounded checkpoints. On cancellation, release partial outputs and return B_CANCELED. Do not kill the thread abruptly while it holds a lock or is updating a file; graceful cancellation needs a rollback or checkpoint policy.

The current Run() implementation notifies listeners when it enters started and at the terminal transition. It sets the in-progress state before calling Execute() but does not itself publish a numeric progress fraction. A subclass that wants intermediate notifications must implement a careful progress contract using the protected state/listener APIs; it should not invent a “percent” field that the base class does not define. Expose progress as application-specific data with documented units and monotonicity.

State listeners are callbacks, not a UI thread hop

BJobStateListener has callbacks for started, progress, succeeded, failed, and aborted. In the current implementation the callbacks are called directly while Run() is executing. Therefore, if a job runs on a worker, its listeners run on that worker for those transitions. A listener must not directly update a BWindow, BView, or other UI object from the worker. Send a typed message to the UI looper and check whether delivery succeeds.

Keep listener lifetime longer than any run that may notify it. Remove listeners before destroying them if the job can outlive them. Conversely, do not destroy the job from its own listener callback while Run() still needs to update terminal state or return; defer destruction to the owner after execution completes.

Listener callbacks should be short and nonblocking. They can run during state changes while internal execution is in progress. Do not wait in a callback for the same job to finish, acquire locks in an order that Execute() reverses, or perform I/O. A callback that blocks can hold up the worker and make progress notifications worse than useless.

Dependencies are relationships, not a workflow engine

AddDependency(job) records that this job depends on another, and IsRunnable() reports whether the dependency list is empty. The API also exposes dependency removal, count, and dependent-job lookup. This is useful metadata for an application scheduler, but the class does not execute prerequisites for you. Your controller must schedule prerequisites, observe their completion, remove satisfied dependencies as appropriate, and then decide when to call Run().

Build a directed acyclic graph before starting a set of jobs. The current AddDependency() implementation checks duplicate links and records the relationship, but callers should not rely on it to detect every cycle or enforce a scheduling policy. Validate cycles and ensure a failed prerequisite does not leave dependent work silently waiting forever. Represent prerequisite failure separately from “not yet complete.”

Avoid dangling dependency pointers. Keep job objects alive until all dependent jobs no longer reference them, or use an owner that tears down the entire graph in a defined order. Test duplicate addition, removal of a missing dependency, dependency completion, failure, and cancellation. Dependency objects are not permanent IDs suitable for persistence.

Error handling and idempotency

An operation can fail after partial progress. Design Execute() to report the first meaningful failure and leave its target in a known state. For file generation, write to a temporary path and replace the destination only after complete validation if that is supported by the file-system operation. For a network operation, distinguish retryable from permanent failures. A second execution attempt should not corrupt output, even though a completed BJob instance itself is one-shot.

Record a ticket number for correlation where the caller’s job queue assigns one, but do not treat it as a globally persistent identifier. Log job title, ticket, start/end, state, result, and cancellation reason. Avoid logging full file paths or user content unless required and approved by the application.

Example architecture

A responsive UI can own a job and a worker that invokes Run(). The listener posts a message containing a stable job key and state to the window. The window verifies that the job still belongs to its model before updating progress. On close, the window requests cancellation through the job’s own cancellation flag, waits or transfers ownership according to a documented policy, and ignores late results after its model is gone.

For a dependency chain, a controller runs the first job, observes its terminal result, removes the corresponding dependency from the next job only on success, and schedules the next job on a worker. If a prerequisite fails, the controller marks dependents as blocked or failed with a useful reason instead of calling their Run() and showing a misleading not-runnable error.

Verification criteria

Test a job that succeeds, returns a normal error, returns B_CANCELED, throws no exceptions across the API boundary, and fails after partial setup. Confirm Cleanup() runs with the result and the state listener sees the expected transitions. Call Run() twice and verify the second call is rejected. Confirm a deliberately slow Execute() does not run on the UI thread.

Test listener removal, listener destruction timing, worker-to-window message delivery after the window closes, duplicate dependencies, prerequisite failure, and a deliberately cyclic dependency proposal rejected by the controller. Measure cancellation latency and ensure every blocking operation has an interruption or timeout strategy. Do not claim “background” just because the object is called a job; verify the actual thread ID in tests.

BJob is a small stateful foundation for work queues. Its value is explicit results, lifecycle notifications, and prerequisite metadata. Applications that provide the scheduler, thread boundary, cancellation policy, and ownership rules can use it without confusing synchronous execution with asynchronous orchestration.

Related:

Sources:

Comments