1use 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#[derive(Clone)]
16pub struct Boson {
17 pub(crate) backend: Arc<dyn QueueBackend>,
18 pub(crate) registry: Arc<TaskRegistry>,
19 worker: WorkerSettings,
20 idempotency_mode: IdempotencyMode,
22 actor_policy: Option<Arc<dyn ActorJsonPolicy>>,
24}
25
26impl Boson {
27 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 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 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 #[must_use]
71 pub const fn idempotency_mode(&self) -> IdempotencyMode {
72 self.idempotency_mode
73 }
74
75 #[must_use]
77 pub const fn worker_settings(&self) -> &WorkerSettings {
78 &self.worker
79 }
80
81 #[must_use]
83 pub fn runtime_label(&self) -> &str {
84 &self.worker.runtime_label
85 }
86
87 #[must_use]
89 pub fn queue_backend(&self) -> Arc<dyn QueueBackend> {
90 Arc::clone(&self.backend)
91 }
92
93 #[must_use]
95 pub fn registry(&self) -> &TaskRegistry {
96 &self.registry
97 }
98
99 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 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 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 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 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 pub async fn get_job(&self, job_id: &str) -> Result<Option<Job>> {
220 self.backend.get_job(job_id).await
221 }
222
223 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 pub async fn cancel_job(&self, job_id: &str) -> Result<()> {
243 self.backend.cancel_job_if_active(job_id).await
244 }
245
246 pub async fn get_task_config(&self, task_name: &str) -> Result<TaskConfig> {
252 self.resolve_task_config(task_name).await
253 }
254
255 pub async fn upsert_task_config(&self, config: TaskConfig) -> Result<()> {
261 self.backend.upsert_task_config(&config).await
262 }
263
264 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 pub async fn get_run(&self, run_id: &str) -> Result<Option<Run>> {
284 self.backend.get_run(run_id).await
285 }
286
287 pub async fn count_jobs(&self, status_filter: Option<JobStatus>) -> Result<u64> {
293 self.backend.count_jobs(status_filter).await
294 }
295
296 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 pub async fn count_runs_since(&self, since: DateTime<Utc>) -> Result<u64> {
311 self.backend.count_runs_since(since).await
312 }
313
314 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 pub async fn task_run_stats(&self, task_name: &str) -> Result<TaskRunStats> {
333 self.backend.task_run_stats(task_name).await
334 }
335}