zeph_common/spawner.rs
1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Dependency-inversion trait for supervised blocking thread spawns.
5//!
6//! Crates that cannot depend on `zeph-core` (e.g. `zeph-index`) accept an
7//! `Option<Arc<dyn BlockingSpawner>>` and fall back to raw
8//! `tokio::task::spawn_blocking` when `None`. This breaks the cyclic
9//! dependency that would otherwise arise from `zeph-index` importing
10//! `TaskSupervisor` from `zeph-core`.
11
12/// Trait for spawning CPU-bound work on a supervised blocking thread pool.
13///
14/// Implementors register each spawned task in their supervision layer so it
15/// is visible to lifecycle management (snapshots, graceful shutdown, metrics).
16/// Callers that do not have a supervised spawner may fall back to
17/// `tokio::task::spawn_blocking` directly.
18///
19/// The trait is object-safe: it accepts a `Box<dyn FnOnce() + Send + 'static>`
20/// and returns a `JoinHandle<()>`. Callers that need a typed return value
21/// must communicate results through a channel or shared state.
22///
23/// # Examples
24///
25/// ```no_run
26/// use std::sync::Arc;
27/// use zeph_common::BlockingSpawner;
28///
29/// fn do_work(spawner: Arc<dyn BlockingSpawner>) {
30/// let handle = spawner.spawn_blocking_named(Arc::from("my_task"), Box::new(|| {
31/// // CPU-bound work
32/// }));
33/// // Caller can `.await` the handle.
34/// let _ = handle;
35/// }
36/// ```
37pub trait BlockingSpawner: Send + Sync + 'static {
38 /// Spawn a named blocking closure and return a `JoinHandle<()>` for completion.
39 ///
40 /// Pass an `Arc<str>` for the task name — this avoids the need to leak memory
41 /// when constructing dynamic task names. Static literals can be converted with
42 /// `Arc::from("my_task")`.
43 ///
44 /// The implementation registers the task in its supervision layer before
45 /// the closure begins executing. Results must be communicated via channels
46 /// or shared state if needed.
47 ///
48 /// If the closure panics, the implementation should log the error rather than
49 /// propagating a panic to the caller. The returned `JoinHandle<()>` resolves
50 /// to `Ok(())` in all non-abort cases; it resolves to `Err(JoinError)` only
51 /// if the bridge task itself is aborted externally.
52 #[must_use]
53 fn spawn_blocking_named(
54 &self,
55 name: std::sync::Arc<str>,
56 f: Box<dyn FnOnce() + Send + 'static>,
57 ) -> tokio::task::JoinHandle<()>;
58}