gatling/background.rs
1//! Single background job — the one-worker degenerate of the gatling pool.
2//!
3//! For **producer/consumer overlap** (double-buffering), not fan-out: run one
4//! `Send` closure on a dedicated thread while the caller keeps producing, then
5//! [`Job::join`] for its result. The canonical use is a spill/sort/write stage
6//! that overlaps the next decode pass — one job in flight at a time (join the
7//! previous before spawning the next), so it is a depth-1 pipeline, not a pool.
8//!
9//! This module is the sanctioned home for the raw `std::thread::spawn` that the
10//! one-engine "rayon-free" law forbids in every non-engine crate: callers get
11//! overlap by calling [`Job::spawn`] instead of hand-rolling a thread, so all
12//! threading in the constellation still originates in `gatling`. It deliberately
13//! owns no work-stealing, no shared cursor, no reader — for parallel fan-out use
14//! [`crate::gatling_forkjoin::gatling_for_each`] or the streaming
15//! [`crate::gatling::run`] engine instead.
16
17use std::thread::{self, JoinHandle};
18
19/// A single closure running on its own thread. Create with [`Job::spawn`], reap
20/// with [`Job::join`]. Dropping a `Job` without joining detaches the thread (the
21/// closure runs to completion but its result and any panic are lost) — the
22/// overlap pattern always joins, so prefer explicit [`Job::join`].
23#[must_use = "a Job runs in the background; join it to observe its result / propagate panics"]
24pub struct Job<T: Send + 'static> {
25 handle: JoinHandle<T>,
26}
27
28impl<T: Send + 'static> Job<T> {
29 /// Spawn `f` on a fresh background thread and return immediately. The caller
30 /// keeps working; call [`Job::join`] later to block for the result.
31 #[inline]
32 pub fn spawn<F>(f: F) -> Self
33 where
34 F: FnOnce() -> T + Send + 'static,
35 {
36 Job { handle: thread::spawn(f) }
37 }
38
39 /// Block until the job finishes and return its value. A panic inside the job
40 /// is surfaced as `Err` (identical semantics to [`JoinHandle::join`]) rather
41 /// than aborting the caller.
42 #[inline]
43 pub fn join(self) -> thread::Result<T> {
44 self.handle.join()
45 }
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn runs_and_returns_value() {
54 let job = Job::spawn(|| 2 + 2);
55 assert_eq!(job.join().unwrap(), 4);
56 }
57
58 #[test]
59 fn overlaps_caller_work() {
60 // The producer keeps working while the job runs; the sum is observed
61 // only after join — a depth-1 pipeline.
62 let job = Job::spawn(|| (0u64..1_000).sum::<u64>());
63 let mut local = 0u64;
64 for i in 0..1_000 {
65 local += i;
66 }
67 assert_eq!(job.join().unwrap(), local);
68 }
69
70 #[test]
71 fn panic_becomes_err_not_abort() {
72 let job = Job::spawn(|| panic!("boom"));
73 assert!(job.join().is_err());
74 }
75}