qubit-executor 0.4.0

Executor abstractions, task handles, and basic executor implementations for Qubit Rust libraries
Documentation

Qubit Executor

Rust CI Coverage Crates.io Rust License 中文文档

Executor abstractions and task-result primitives for Rust.

Overview

Qubit Executor provides the small common execution API used by the Qubit Rust concurrency crates. It separates lightweight execution strategies from managed executor services, and provides reusable task handles for implementations that need to publish task success, task failure, panic, cancellation, or dropped completion.

This crate deliberately avoids depending on Tokio, Rayon, or a concrete thread pool. Runtime-specific implementations live in smaller companion crates so libraries can depend only on the abstraction level they need.

Features

  • Strategy-level Executor trait for executing one task and returning a TrackedTask handle.
  • DirectExecutor for deterministic same-thread execution.
  • DelayExecutor for running work on a helper thread after a fixed delay.
  • ScheduleExecutor for running work on a helper thread at a monotonic Instant.
  • ThreadPerTaskExecutor for spawning one OS thread per task without queue management.
  • Managed ExecutorService trait with submit, submit_callable, shutdown, stop, lifecycle inspection, and blocking termination waiting.
  • ThreadPerTaskExecutorService as a basic managed service implementation.
  • TaskHandle, TrackedTask, TaskExecutionError, and TaskResult for sharing task completion semantics across crates.
  • Shared lifecycle, rejection, and stop report types through ExecutorServiceLifecycle, SubmissionError, and StopReport.

Executor vs ExecutorService

Executor is a low-level execution strategy. It answers: “how should this one task run?” Accepted task results are exposed uniformly through TrackedTask, even when the concrete executor runs the task inline.

ExecutorService is a managed service. It answers: “can this service accept a task, track it, shut down, and eventually terminate?” A successful submit means only that the service accepted the task. It does not mean the task has started or completed successfully.

ExecutorService Lifecycle

Every managed service follows the same high-level lifecycle:

State Meaning
Running The service accepts new tasks and may have accepted work queued, scheduled, or running.
ShuttingDown shutdown() has requested orderly shutdown. New submissions are rejected, while already accepted work is allowed to finish normally.
Stopping stop() has requested abrupt stop. New submissions are rejected, and the service attempts to cancel or abort accepted work that can still be stopped.
Terminated Shutdown or stop has been requested, and no accepted work remains active.

shutdown() and stop() are both terminal admission decisions: after either method is called, the service is no longer running and will not accept new tasks again. The difference is how accepted work is treated.

shutdown() is graceful. It preserves accepted work and lets queued, scheduled, or running tasks complete according to the concrete service's normal execution rules.

stop() is abrupt and best effort. It requests cancellation of queued, scheduled, or unstarted work, and may abort runtime-managed tasks when the runtime supports aborting them. It cannot forcibly interrupt arbitrary Rust code, blocking calls, or already-running OS threads, so termination may still wait for such work to return. The returned StopReport describes the queued, running, and cancelled work observed while handling the stop request.

wait_termination() blocks the current thread until either shutdown or stop has been requested and all accepted work has completed, failed, panicked, been cancelled, been dropped by its runner endpoint, or been aborted according to the concrete service's capabilities. Calling it while the service is still Running waits until another thread requests shutdown or stop; if that never happens, it can block forever. This API is deliberately synchronous and blocking, not an async or non-blocking wait.

Resource cleanup

Do not rely on dropping an ExecutorService handle to release service resources promptly. A concrete service may request shutdown from Drop, but destructor code should not be assumed to wait for worker threads, helper threads, runtime tasks, queues, or task-owned resources to finish. Blocking in Drop would make ordinary handle destruction unexpectedly wait for arbitrary user code, blocking calls, or OS-thread tasks that cannot be interrupted.

When deterministic cleanup matters, use an explicit lifecycle sequence:

  1. Call shutdown() to reject new work and drain accepted work, or call stop() to request best-effort cancellation or abort of work that can still be stopped.
  2. Call wait_termination() and let it return before assuming the service has quiesced.
  3. Drop the service handle and any task handles after the wait returns.

Already-running blocking or OS-thread task bodies can keep file descriptors, sockets, locks, reference-counted objects, or other external resources alive until those task bodies return. Services that need stronger cleanup guarantees should provide an explicit close/join API instead of relying on destructor side effects.

Task Results

TaskHandle represents the result of an accepted callable task. It supports blocking waits through get, async waits by value, non-blocking try_get, and completion checks through is_done.

TrackedTask adds status inspection and best-effort cancellation before the task starts. Managed services expose tracked handles through submit_tracked and submit_tracked_callable.

Task execution errors are represented by TaskExecutionError:

  • Failed(E) means the task returned its own error value.
  • Panicked means the task panicked while running.
  • Cancelled means the task was cancelled before producing a value.
  • Dropped means the runner-side completion endpoint disappeared without publishing a value, which is distinct from an explicit cancellation request.

Task Hooks

Executors may be configured with a TaskHook to observe lifecycle events. A rejected submission emits only on_rejected and never receives a task id. An accepted task emits on_accepted before any on_started or on_finished event for that task. Tasks cancelled before start, or accepted tasks whose runner endpoint is abandoned, may emit on_finished without on_started. Executors contain hook panics so hook implementations cannot prevent task result publication.

Quick Start

Direct execution

use std::io;

use qubit_executor::{DirectExecutor, Executor};

let executor = DirectExecutor::new();
let handle = executor.call(|| Ok::<usize, io::Error>(40 + 2))?;
let value = handle.get()?;
assert_eq!(value, 42);
# Ok::<(), Box<dyn std::error::Error>>(())

One thread per task

use std::io;

use qubit_executor::{Executor, ThreadPerTaskExecutor};

let executor = ThreadPerTaskExecutor::new();
let handle = executor.call(|| Ok::<usize, io::Error>(40 + 2))?;
assert_eq!(handle.get()?, 42);
# Ok::<(), Box<dyn std::error::Error>>(())

Managed service

use std::io;

use qubit_executor::{ExecutorService, ThreadPerTaskExecutorService};

let service = ThreadPerTaskExecutorService::new();
let handle = service.submit_callable(|| Ok::<usize, io::Error>(40 + 2))?;
assert_eq!(handle.get()?, 42);
service.shutdown();
service.wait_termination();
# Ok::<(), Box<dyn std::error::Error>>(())

Crate Boundaries

Use qubit-executor when you are defining APIs that should accept or return executor abstractions without committing to a runtime. Use a runtime-specific crate when you need a concrete implementation:

  • qubit-thread-pool provides dynamic and fixed OS-thread pools.
  • qubit-tokio-executor provides Tokio-backed blocking and async IO services.
  • qubit-rayon-executor provides a Rayon-backed CPU-bound service.
  • qubit-execution-services aggregates the concrete services for application-level wiring.

Testing

A minimal local run:

cargo test
cargo clippy --all-targets --all-features -- -D warnings

To mirror what continuous integration enforces, run the repository scripts from the project root: ./align-ci.sh brings local tooling and configuration in line with CI, then ./ci-check.sh runs the same checks the pipeline uses. For test coverage, use ./coverage.sh to generate or open reports.

Contributing

Issues and pull requests are welcome.

  • Open an issue for bug reports, design questions, or larger feature proposals when it helps align on direction.
  • Keep pull requests scoped to one behavior change, fix, or documentation update when practical.
  • Before submitting, run ./align-ci.sh and then ./ci-check.sh so your branch matches CI rules and passes the same checks as the pipeline.
  • Add or update tests when you change runtime behavior, and update this README or public rustdoc when user-visible API behavior changes.

By contributing, you agree to license your contributions under the Apache License, Version 2.0, the same license as this project.

License

Copyright (c) 2026. Haixing Hu.

This project is licensed under the Apache License, Version 2.0. See the LICENSE file in the repository for the full text.

Author

Haixing Hu — Qubit Co. Ltd.

Repository github.com/qubit-ltd/rs-executor
Documentation docs.rs/qubit-executor
Crate crates.io/crates/qubit-executor