1use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use boson_core::{
8 ExecutionContextFactory, Job, JobStatus, QueueBackend, Result, Run, RunStatus, TaskConfig,
9};
10use chrono::Utc;
11use tokio::time::sleep;
12
13use super::claim::claim_next_job;
14use super::config::WorkerSettings;
15use super::execute::{execute_job, record_run_start};
16use super::lifecycle::{finish_job_execution, sleep_retry_delay, RunLifecycleHost};
17use crate::registry::TaskRegistry;
18use crate::telemetry;
19
20#[derive(Debug)]
22pub struct WorkerHandle {
23 _label: String,
24}
25
26pub struct WorkerEngine {
27 pub(crate) backend: Arc<dyn QueueBackend>,
28 pub(crate) registry: Arc<TaskRegistry>,
29 pub(crate) identity: Arc<dyn ExecutionContextFactory>,
30 pub(crate) worker: WorkerSettings,
31}
32
33impl WorkerEngine {
34 pub(crate) async fn drive_run(self: &Arc<Self>, job: Job, lease_id: Option<String>) {
35 if self.worker.skip_run_persistence && lease_id.is_none() {
36 self.drive_run_without_run_rows(job).await;
37 return;
38 }
39 let run = Run::new(&job.job_id, &job.task_name, job.attempt);
40 let run_id = run.run_id.clone();
41 if record_run_start(&self.backend, &run).await.is_err() {
42 telemetry::record_handler_error(
43 &job.task_name,
44 &job.job_id,
45 &run_id,
46 "failed to persist run start",
47 );
48 if let Some(ref lid) = lease_id {
49 let _ = self.backend.release_lease(lid).await;
50 }
51 let _ = self.backend.revert_job_to_queued(&job.job_id).await;
52 return;
53 }
54 telemetry::record_task_started(
55 &job.task_name,
56 &job.job_id,
57 &run_id,
58 &self.worker.runtime_label,
59 );
60 let start = Utc::now();
61 let result = execute_job(&self.registry, &self.identity, &job).await;
62 if let Err(ref e) = result {
63 telemetry::record_handler_error(
64 &job.task_name,
65 &job.job_id,
66 &run_id,
67 &e.to_string(),
68 );
69 }
70 let duration_ms = (Utc::now() - start).num_milliseconds();
71 finish_job_execution(self.as_ref(), run_id, job, result, duration_ms).await;
72 if let Some(ref lid) = lease_id {
73 let _ = self.backend.release_lease(lid).await;
74 }
75 }
76
77 async fn drive_run_without_run_rows(self: &Arc<Self>, job: Job) {
78 let run_id = format!("fast-{}", job.job_id);
79 telemetry::record_task_started(
80 &job.task_name,
81 &job.job_id,
82 &run_id,
83 &self.worker.runtime_label,
84 );
85 let start = Utc::now();
86 let result = execute_job(&self.registry, &self.identity, &job).await;
87 let duration_ms = (Utc::now() - start).num_milliseconds();
88 match result {
89 Ok(()) => {
90 telemetry::record_task_completed(&job.task_name, &job.job_id, &run_id, duration_ms);
91 let mut finished = job;
92 finished.status = JobStatus::Success;
93 self.upsert_job(finished).await;
94 }
95 Err(e) => {
96 telemetry::record_handler_error(
97 &job.task_name,
98 &job.job_id,
99 &run_id,
100 &e.to_string(),
101 );
102 telemetry::record_task_failed(
103 &job.task_name,
104 &job.job_id,
105 &run_id,
106 &e.to_string(),
107 false,
108 );
109 let _ = self.backend.revert_job_to_queued(&job.job_id).await;
110 }
111 }
112 }
113
114 async fn upsert_job(&self, job: Job) {
115 if let Err(e) = self.backend.upsert_job(&job).await {
116 telemetry::log_job_upsert_failed(&job.job_id, &job.task_name, &e.to_string());
117 }
118 }
119
120 async fn tick(self: &Arc<Self>) {
121 let discovered = self
122 .backend
123 .distinct_pools_queued()
124 .await
125 .unwrap_or_default();
126 let pools = self.worker.pools_to_poll(discovered);
127 for pool in pools {
128 if let Ok(Some((job, lease_id))) = claim_next_job(
129 &self.backend,
130 &pool,
131 &self.worker.worker_id,
132 self.worker.lease_ttl_secs,
133 )
134 .await
135 {
136 self.drive_run(job, lease_id).await;
137 }
138 }
139 }
140
141 async fn reap_expired_leases(self: Arc<Self>) {
142 if self.worker.lease_ttl_secs <= 0 {
143 return;
144 }
145 loop {
146 sleep(Duration::from_secs(15)).await;
147 let pairs = self
148 .backend
149 .expired_lease_job_pairs()
150 .await
151 .unwrap_or_default();
152 let count = pairs.len();
153 for (lease_id, job_id) in pairs {
154 let _ = self.backend.release_lease(&lease_id).await;
155 let _ = self.backend.revert_job_to_queued(&job_id).await;
156 }
157 telemetry::log_lease_reclaim(count, &self.worker.runtime_label);
158 }
159 }
160}
161
162#[async_trait]
163impl RunLifecycleHost for WorkerEngine {
164 async fn record_run_finish(
165 &self,
166 run_id: &str,
167 status: RunStatus,
168 duration_ms: Option<i64>,
169 error_message: Option<String>,
170 ) -> Result<()> {
171 self.backend
172 .finish_run(run_id, status, duration_ms, error_message)
173 .await
174 }
175
176 async fn put_job(&self, job: Job) {
177 self.upsert_job(job).await;
178 }
179
180 async fn load_task_config(&self, task_name: &str) -> Result<TaskConfig> {
181 if let Some(c) = self.backend.get_task_config(task_name).await? {
182 return Ok(c);
183 }
184 Ok(self.registry.get_or_err(task_name)?.to_task_config())
185 }
186
187 async fn schedule_retry(&self, mut job: Job, delay_ms: u64) {
188 sleep_retry_delay(delay_ms).await;
189 job.attempt += 1;
190 job.status = JobStatus::Queued;
191 self.upsert_job(job).await;
192 }
193}
194
195pub fn spawn_worker(
197 backend: Arc<dyn QueueBackend>,
198 registry: Arc<TaskRegistry>,
199 identity: Arc<dyn ExecutionContextFactory>,
200 worker: WorkerSettings,
201) -> WorkerHandle {
202 let label = worker.runtime_label.clone();
203 let engine = Arc::new(WorkerEngine {
204 backend,
205 registry,
206 identity,
207 worker,
208 });
209 if engine.worker.lease_ttl_secs > 0 {
210 let reaper = Arc::clone(&engine);
211 tokio::spawn(async move {
212 reaper.reap_expired_leases().await;
213 });
214 }
215 tokio::spawn(async move {
216 let poll_ms = engine.worker.worker_poll_interval_ms;
217 loop {
218 engine.tick().await;
219 if poll_ms > 0 {
220 sleep(Duration::from_millis(poll_ms)).await;
221 } else {
222 tokio::task::yield_now().await;
223 }
224 }
225 });
226 WorkerHandle { _label: label }
227}