Skip to main content

boson_runtime/
boson.rs

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