Skip to main content

boson_runtime/worker/
manual.rs

1//! Manual single-step worker for tests (no background task).
2
3use std::sync::{Arc, Mutex};
4
5use boson_core::{ExecutionContextFactory, QueueBackend};
6
7use super::claim::claim_next_job;
8use super::config::WorkerSettings;
9use super::loop_::WorkerEngine;
10use crate::registry::TaskRegistry;
11
12/// Manual single-step worker for tests (no background task).
13///
14/// Use [`BosonBuilder::build_manual`](crate::BosonBuilder::build_manual) to obtain one alongside
15/// [`Boson`](crate::Boson). Call [`try_run_next`](Self::try_run_next) to claim and execute at most
16/// one queued job — useful in unit tests and the [`task_macro` example](https://github.com/unified-field-dev/boson/blob/main/boson/examples/task_macro.rs).
17///
18/// # Example
19///
20/// ```rust,no_run
21/// use std::sync::Arc;
22///
23/// use boson_backend_mem::MemQueueBackend;
24/// use boson_core::{ExecutionContext, JsonExecutionContextFactory};
25/// use boson_macros::task;
26/// use boson_runtime::{configure, Boson, ManualWorker};
27///
28/// #[task(name = "ping")]
29/// async fn ping(_ctx: Box<dyn ExecutionContext>) -> boson_core::Result<()> {
30///     Ok(())
31/// }
32///
33/// # async fn run() -> boson_core::Result<()> {
34/// let (boson, manual) = Boson::builder()
35///     .queue_backend(Arc::new(MemQueueBackend::new()))
36///     .execution_context_factory(JsonExecutionContextFactory)
37///     .auto_registry()
38///     .build_manual()?;
39/// configure(boson);
40///
41/// Ping::send_with(serde_json::json!({"System": {}}), PingParams {}).await?;
42/// assert!(manual.try_run_next().await); // runs the handler once
43/// # Ok(())
44/// # }
45/// ```
46pub struct ManualWorker {
47    inner: Arc<WorkerEngine>,
48    /// True while a claim/execute step is in flight (never held across `.await`).
49    in_flight: Mutex<bool>,
50}
51
52struct ClearInFlight<'a>(&'a Mutex<bool>);
53
54impl Drop for ClearInFlight<'_> {
55    fn drop(&mut self) {
56        if let Ok(mut guard) = self.0.lock() {
57            *guard = false;
58        }
59    }
60}
61
62impl ManualWorker {
63    /// Create a worker that can be driven step-by-step in tests.
64    pub fn new(
65        backend: Arc<dyn QueueBackend>,
66        registry: Arc<TaskRegistry>,
67        identity: Arc<dyn ExecutionContextFactory>,
68        worker: WorkerSettings,
69    ) -> Self {
70        Self {
71            inner: Arc::new(WorkerEngine {
72                backend,
73                registry,
74                identity,
75                worker,
76            }),
77            in_flight: Mutex::new(false),
78        }
79    }
80
81    /// Process at most one job across all pools.
82    pub async fn try_run_next(&self) -> bool {
83        {
84            let Ok(mut in_flight) = self.in_flight.lock() else {
85                return false;
86            };
87            if *in_flight {
88                return false;
89            }
90            *in_flight = true;
91        }
92        let _clear = ClearInFlight(&self.in_flight);
93
94        let discovered = self
95            .inner
96            .backend
97            .distinct_pools_queued()
98            .await
99            .unwrap_or_default();
100        let pools = self.inner.worker.pools_to_poll(discovered);
101        for pool in pools {
102            if let Ok(Some((job, lease_id))) = claim_next_job(
103                &self.inner.backend,
104                &pool,
105                &self.inner.worker.worker_id,
106                self.inner.worker.lease_ttl_secs,
107            )
108            .await
109            {
110                self.inner.drive_run(job, lease_id).await;
111                return true;
112            }
113        }
114        false
115    }
116}