Skip to main content

boson_runtime/
boson.rs

1//! Main Boson runtime type.
2
3use std::sync::Arc;
4
5use boson_core::{
6    BosonError, IdempotencyMode, Job, JobEnqueueDisposition, JobStatus, QueueBackend, Result, Run,
7    TaskConfig, TaskRunStats,
8};
9use chrono::{DateTime, Utc};
10
11use crate::registry::TaskRegistry;
12use crate::worker::WorkerSettings;
13
14/// Boson work engine — enqueue, admin reads, and worker orchestration.
15#[derive(Clone)]
16pub struct Boson {
17    pub(crate) backend: Arc<dyn QueueBackend>,
18    pub(crate) registry: Arc<TaskRegistry>,
19    worker: WorkerSettings,
20    /// Runtime default when [`TaskConfig::idempotency_mode`] is unset.
21    idempotency_mode: IdempotencyMode,
22}
23
24impl Boson {
25    /// Construct from injected parts (used by builder and tests).
26    pub fn from_parts(
27        backend: Arc<dyn QueueBackend>,
28        registry: Arc<TaskRegistry>,
29        worker: WorkerSettings,
30    ) -> Self {
31        Self::from_parts_with_idempotency(backend, registry, worker, IdempotencyMode::Lwt)
32    }
33
34    /// Construct with an explicit default idempotency mode.
35    pub fn from_parts_with_idempotency(
36        backend: Arc<dyn QueueBackend>,
37        registry: Arc<TaskRegistry>,
38        worker: WorkerSettings,
39        idempotency_mode: IdempotencyMode,
40    ) -> Self {
41        Self {
42            backend,
43            registry,
44            worker,
45            idempotency_mode,
46        }
47    }
48
49    /// Runtime default idempotency mode (builder / task override may change per enqueue).
50    #[must_use]
51    pub const fn idempotency_mode(&self) -> IdempotencyMode {
52        self.idempotency_mode
53    }
54
55    /// Worker settings this instance was built with.
56    #[must_use]
57    pub const fn worker_settings(&self) -> &WorkerSettings {
58        &self.worker
59    }
60
61    /// Telemetry/runtime label (topology slug or `embedded`).
62    #[must_use]
63    pub fn runtime_label(&self) -> &str {
64        &self.worker.runtime_label
65    }
66
67    /// Queue backend handle.
68    #[must_use]
69    pub fn queue_backend(&self) -> Arc<dyn QueueBackend> {
70        Arc::clone(&self.backend)
71    }
72
73    /// Task registry.
74    #[must_use]
75    pub fn registry(&self) -> &TaskRegistry {
76        &self.registry
77    }
78
79    /// Resolve task config from backend or registry defaults.
80    ///
81    /// Precedence: persisted backend config, else descriptor policy defaults, then runtime
82    /// idempotency fallback when the mode is unset.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if the task is unknown or the backend fails.
87    pub async fn resolve_task_config(&self, task_name: &str) -> Result<TaskConfig> {
88        let config = if let Some(c) = self.backend.get_task_config(task_name).await? {
89            c
90        } else {
91            self.registry.get_or_err(task_name)?.to_task_config()
92        };
93        Ok(config.with_runtime_idempotency_fallback(self.idempotency_mode()))
94    }
95
96    /// Resolve priority and pool for enqueue.
97    ///
98    /// # Errors
99    ///
100    /// Returns an error if the task is unknown or the backend fails.
101    pub async fn resolve_priority_pool(&self, task_name: &str) -> Result<(i32, String)> {
102        let config = self.resolve_task_config(task_name).await?;
103        Ok((config.priority, config.pool))
104    }
105
106    /// Enqueue helper used internally and by admin APIs.
107    pub(crate) async fn enqueue_internal(
108        &self,
109        task_name: &str,
110        actor_json: serde_json::Value,
111        params_json: serde_json::Value,
112        idempotency_key: Option<String>,
113    ) -> Result<String> {
114        let descriptor = self.registry.get_or_err(task_name)?;
115        let task_config = self.resolve_task_config(task_name).await?;
116        let priority = task_config.priority;
117        let pool = task_config.pool.clone();
118        let job = Job::new(
119            task_name,
120            actor_json,
121            params_json,
122            priority,
123            &pool,
124            descriptor.signature_hash,
125            idempotency_key,
126        );
127        let (job_id, disposition) = self
128            .backend
129            .enqueue_with_policies(job, &task_config)
130            .await
131            .map_err(|e| {
132                if matches!(e, BosonError::RateLimited(_)) {
133                    crate::telemetry::record_task_failed(
134                        task_name,
135                        "",
136                        "",
137                        &e.to_string(),
138                        false,
139                    );
140                }
141                e
142            })?;
143        if disposition == JobEnqueueDisposition::InsertedNew {
144            crate::telemetry::record_task_enqueued(task_name, self.runtime_label());
145        }
146        Ok(job_id)
147    }
148
149    /// Enqueue a job.
150    ///
151    /// Priority and pool come from persisted [`TaskConfig`](boson_core::TaskConfig) merged with
152    /// [`TaskDescriptor`](crate::registry::TaskDescriptor) defaults. Optional `idempotency_key`
153    /// deduplicates non-terminal jobs. Rate limits may return
154    /// [`BosonError::RateLimited`](boson_core::BosonError::RateLimited).
155    ///
156    /// # Errors
157    ///
158    /// Returns an error if the task is unknown, rate limits apply, or the backend fails.
159    pub async fn enqueue(
160        &self,
161        task_name: &str,
162        actor_json: serde_json::Value,
163        params_json: serde_json::Value,
164        idempotency_key: Option<String>,
165    ) -> Result<String> {
166        self.enqueue_internal(task_name, actor_json, params_json, idempotency_key)
167            .await
168    }
169
170    /// Get a job by id.
171    ///
172    /// # Errors
173    ///
174    /// Returns an error if the backend fails.
175    pub async fn get_job(&self, job_id: &str) -> Result<Option<Job>> {
176        self.backend.get_job(job_id).await
177    }
178
179    /// List jobs with optional status filter.
180    ///
181    /// # Errors
182    ///
183    /// Returns an error if the backend fails.
184    pub async fn list_jobs(
185        &self,
186        status_filter: Option<JobStatus>,
187        offset: usize,
188        limit: usize,
189    ) -> Result<Vec<Job>> {
190        self.backend.list_jobs(status_filter, offset, limit).await
191    }
192
193    /// Cancel a job if still active.
194    ///
195    /// # Errors
196    ///
197    /// Returns an error if the job is not found or the backend fails.
198    pub async fn cancel_job(&self, job_id: &str) -> Result<()> {
199        self.backend.cancel_job_if_active(job_id).await
200    }
201
202    /// Get or default task config.
203    ///
204    /// # Errors
205    ///
206    /// Returns an error if the task is unknown or the backend fails.
207    pub async fn get_task_config(&self, task_name: &str) -> Result<TaskConfig> {
208        self.resolve_task_config(task_name).await
209    }
210
211    /// Upsert task config.
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if the backend fails.
216    pub async fn upsert_task_config(&self, config: TaskConfig) -> Result<()> {
217        self.backend.upsert_task_config(&config).await
218    }
219
220    /// List runs.
221    ///
222    /// # Errors
223    ///
224    /// Returns an error if the backend fails.
225    pub async fn list_runs(
226        &self,
227        job_id_filter: Option<&str>,
228        offset: usize,
229        limit: usize,
230    ) -> Result<Vec<Run>> {
231        self.backend.list_runs(job_id_filter, offset, limit).await
232    }
233
234    /// Get run by id.
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if the backend fails.
239    pub async fn get_run(&self, run_id: &str) -> Result<Option<Run>> {
240        self.backend.get_run(run_id).await
241    }
242
243    /// Count jobs.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error if the backend fails.
248    pub async fn count_jobs(&self, status_filter: Option<JobStatus>) -> Result<u64> {
249        self.backend.count_jobs(status_filter).await
250    }
251
252    /// Count runs.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the backend fails.
257    pub async fn count_runs(&self, job_id_filter: Option<&str>) -> Result<u64> {
258        self.backend.count_runs(job_id_filter).await
259    }
260
261    /// Count runs since timestamp.
262    ///
263    /// # Errors
264    ///
265    /// Returns an error if the backend fails.
266    pub async fn count_runs_since(&self, since: DateTime<Utc>) -> Result<u64> {
267        self.backend.count_runs_since(since).await
268    }
269
270    /// Count jobs for one task.
271    ///
272    /// # Errors
273    ///
274    /// Returns an error if the backend fails.
275    pub async fn count_jobs_for_task(
276        &self,
277        task_name: &str,
278        status: Option<JobStatus>,
279    ) -> Result<u64> {
280        self.backend.count_jobs_for_task(task_name, status).await
281    }
282
283    /// Aggregate run stats for one task.
284    ///
285    /// # Errors
286    ///
287    /// Returns an error if the backend fails.
288    pub async fn task_run_stats(&self, task_name: &str) -> Result<TaskRunStats> {
289        self.backend.task_run_stats(task_name).await
290    }
291}