1use std::sync::Arc;
4use std::time::Duration;
5
6use async_trait::async_trait;
7use boson_core::{
8 BosonError, ExecutionContextFactory, Job, JobStatus, QueueBackend, Result, Run, RunStatus,
9 TaskConfig,
10};
11use chrono::Utc;
12use tokio::time::sleep;
13
14use super::claim::claim_next_job;
15use super::config::WorkerSettings;
16use super::execute::{execute_job, record_run_start};
17use super::lease_guard::AbortOnDrop;
18use super::lifecycle::{finish_job_execution, sleep_retry_delay, RunLifecycleHost};
19use crate::registry::TaskRegistry;
20use crate::telemetry;
21
22#[derive(Debug)]
24pub struct WorkerHandle {
25 _label: String,
26}
27
28pub struct WorkerEngine {
29 pub(crate) backend: Arc<dyn QueueBackend>,
30 pub(crate) registry: Arc<TaskRegistry>,
31 pub(crate) identity: Arc<dyn ExecutionContextFactory>,
32 pub(crate) worker: WorkerSettings,
33}
34
35impl WorkerEngine {
36 pub(crate) async fn drive_run(self: &Arc<Self>, job: Job, lease_id: Option<String>) {
37 if self.worker.skip_run_persistence && lease_id.is_none() {
38 self.drive_run_without_run_rows(job).await;
39 return;
40 }
41 let run = Run::new(&job.job_id, &job.task_name, job.attempt);
42 let run_id = run.run_id.clone();
43 if record_run_start(&self.backend, &run).await.is_err() {
44 telemetry::record_handler_error(
45 &job.task_name,
46 &job.job_id,
47 &run_id,
48 "failed to persist run start",
49 );
50 if let Some(ref lid) = lease_id {
51 let _ = self.backend.release_lease(lid).await;
52 }
53 let _ = self.backend.revert_job_to_queued(&job.job_id).await;
54 return;
55 }
56 telemetry::record_task_started(
57 &job.task_name,
58 &job.job_id,
59 &run_id,
60 &self.worker.runtime_label,
61 );
62 let _heartbeat_guard = AbortOnDrop::new(self.spawn_lease_heartbeat(lease_id.as_deref()));
63 let start = Utc::now();
64 let result = self.execute_job_catch_panic(&job).await;
65 let result = self.apply_cancel_if_needed(&job.job_id, result).await;
66 if let Err(ref e) = result {
67 let msg = boson_core::sanitize_error_message(&e.to_string());
68 telemetry::record_handler_error(&job.task_name, &job.job_id, &run_id, &msg);
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 execute_job_catch_panic(&self, job: &Job) -> Result<()> {
80 let registry = Arc::clone(&self.registry);
81 let identity = Arc::clone(&self.identity);
82 let backend = Arc::clone(&self.backend);
83 let job_id = job.job_id.clone();
84 let task_name = job.task_name.clone();
85 let job = job.clone();
86 match tokio::spawn(async move { execute_job(®istry, &identity, &backend, &job).await })
87 .await
88 {
89 Ok(result) => result,
90 Err(join_err) if join_err.is_panic() => {
91 telemetry::record_handler_error(&task_name, &job_id, "panic", "handler panicked");
92 Err(BosonError::internal("handler panicked"))
93 }
94 Err(_) => Err(BosonError::internal("handler task cancelled")),
95 }
96 }
97
98 fn spawn_lease_heartbeat(&self, lease_id: Option<&str>) -> Option<tokio::task::JoinHandle<()>> {
99 let lease_id = lease_id?.to_string();
100 let ttl = self.worker.lease_ttl_secs;
101 if ttl <= 0 {
102 return None;
103 }
104 let backend = Arc::clone(&self.backend);
105 let interval_secs = u64::try_from((ttl / 3).max(1)).unwrap_or(1);
106 Some(tokio::spawn(async move {
107 loop {
108 sleep(Duration::from_secs(interval_secs)).await;
109 if backend.extend_lease(&lease_id, ttl).await.is_err() {
110 break;
111 }
112 }
113 }))
114 }
115
116 async fn apply_cancel_if_needed(&self, job_id: &str, result: Result<()>) -> Result<()> {
119 result?;
120 match self.backend.get_job(job_id).await {
121 Ok(Some(j)) if j.status == JobStatus::Canceled => Err(
122 boson_core::BosonError::internal("job canceled during execution"),
123 ),
124 _ => Ok(()),
125 }
126 }
127
128 async fn drive_run_without_run_rows(self: &Arc<Self>, job: Job) {
129 let run_id = format!("fast-{}", job.job_id);
130 telemetry::record_task_started(
131 &job.task_name,
132 &job.job_id,
133 &run_id,
134 &self.worker.runtime_label,
135 );
136 let start = Utc::now();
137 let result = self.execute_job_catch_panic(&job).await;
138 let result = self.apply_cancel_if_needed(&job.job_id, result).await;
139 let duration_ms = (Utc::now() - start).num_milliseconds();
140 match result {
141 Ok(()) => {
142 telemetry::record_task_completed(&job.task_name, &job.job_id, &run_id, duration_ms);
143 let mut finished = job;
144 finished.status = JobStatus::Success;
145 self.upsert_job(finished).await;
146 }
147 Err(e) => {
148 let msg = boson_core::sanitize_error_message(&e.to_string());
149 telemetry::record_handler_error(&job.task_name, &job.job_id, &run_id, &msg);
150 if msg.contains("job canceled") {
151 let mut canceled = job;
152 canceled.status = JobStatus::Canceled;
153 self.upsert_job(canceled).await;
154 return;
155 }
156 telemetry::record_task_failed(&job.task_name, &job.job_id, &run_id, &msg, false);
157 let _ = self.backend.revert_job_to_queued(&job.job_id).await;
158 }
159 }
160 }
161
162 async fn upsert_job(&self, job: Job) {
163 if let Err(e) = self.backend.upsert_job(&job).await {
164 telemetry::log_job_upsert_failed(&job.job_id, &job.task_name, &e.to_string());
165 }
166 }
167
168 async fn tick(self: &Arc<Self>) {
169 let discovered = self
170 .backend
171 .distinct_pools_queued()
172 .await
173 .unwrap_or_default();
174 let pools = self.worker.pools_to_poll(discovered);
175 for pool in pools {
176 if let Ok(Some((job, lease_id))) = claim_next_job(
177 &self.backend,
178 &pool,
179 &self.worker.worker_id,
180 self.worker.lease_ttl_secs,
181 )
182 .await
183 {
184 self.drive_run(job, lease_id).await;
185 }
186 }
187 }
188
189 async fn reap_expired_leases(self: Arc<Self>) {
190 if self.worker.lease_ttl_secs <= 0 {
191 return;
192 }
193 loop {
194 sleep(Duration::from_secs(15)).await;
195 let pairs = self
196 .backend
197 .expired_lease_job_pairs()
198 .await
199 .unwrap_or_default();
200 let count = pairs.len();
201 for (lease_id, job_id) in pairs {
202 let _ = self.backend.release_lease(&lease_id).await;
203 let _ = self.backend.revert_job_to_queued(&job_id).await;
204 }
205 telemetry::log_lease_reclaim(count, &self.worker.runtime_label);
206 }
207 }
208}
209
210#[async_trait]
211impl RunLifecycleHost for WorkerEngine {
212 async fn record_run_finish(
213 &self,
214 run_id: &str,
215 status: RunStatus,
216 duration_ms: Option<i64>,
217 error_message: Option<String>,
218 ) -> Result<()> {
219 self.backend
220 .finish_run(run_id, status, duration_ms, error_message)
221 .await
222 }
223
224 async fn put_job(&self, job: Job) {
225 self.upsert_job(job).await;
226 }
227
228 async fn load_task_config(&self, task_name: &str) -> Result<TaskConfig> {
229 if let Some(c) = self.backend.get_task_config(task_name).await? {
230 return Ok(c);
231 }
232 Ok(self.registry.get_or_err(task_name)?.to_task_config())
233 }
234
235 async fn schedule_retry(&self, mut job: Job, delay_ms: u64) {
236 sleep_retry_delay(delay_ms).await;
237 job.attempt += 1;
238 job.status = JobStatus::Queued;
239 self.upsert_job(job).await;
240 }
241}
242
243pub fn spawn_worker(
245 backend: Arc<dyn QueueBackend>,
246 registry: Arc<TaskRegistry>,
247 identity: Arc<dyn ExecutionContextFactory>,
248 worker: WorkerSettings,
249) -> WorkerHandle {
250 let label = worker.runtime_label.clone();
251 let engine = Arc::new(WorkerEngine {
252 backend,
253 registry,
254 identity,
255 worker,
256 });
257 if engine.worker.lease_ttl_secs > 0 {
258 let reaper = Arc::clone(&engine);
259 tokio::spawn(async move {
260 reaper.reap_expired_leases().await;
261 });
262 }
263 tokio::spawn(async move {
264 let poll_ms = engine.worker.worker_poll_interval_ms;
265 loop {
266 engine.tick().await;
267 if poll_ms > 0 {
268 sleep(Duration::from_millis(poll_ms)).await;
269 } else {
270 tokio::task::yield_now().await;
271 }
272 }
273 });
274 WorkerHandle { _label: label }
275}