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 heartbeat = self.spawn_lease_heartbeat(lease_id.as_deref());
61 let start = Utc::now();
62 let result = execute_job(&self.registry, &self.identity, &self.backend, &job).await;
63 if let Some(handle) = heartbeat {
64 handle.abort();
65 }
66 let result = self.apply_cancel_if_needed(&job.job_id, result).await;
67 if let Err(ref e) = result {
68 let msg = boson_core::sanitize_error_message(&e.to_string());
69 telemetry::record_handler_error(&job.task_name, &job.job_id, &run_id, &msg);
70 }
71 let duration_ms = (Utc::now() - start).num_milliseconds();
72 finish_job_execution(self.as_ref(), run_id, job, result, duration_ms).await;
73 if let Some(ref lid) = lease_id {
74 let _ = self.backend.release_lease(lid).await;
75 }
76 }
77
78 fn spawn_lease_heartbeat(&self, lease_id: Option<&str>) -> Option<tokio::task::JoinHandle<()>> {
79 let lease_id = lease_id?.to_string();
80 let ttl = self.worker.lease_ttl_secs;
81 if ttl <= 0 {
82 return None;
83 }
84 let backend = Arc::clone(&self.backend);
85 let interval_secs = u64::try_from((ttl / 3).max(1)).unwrap_or(1);
86 Some(tokio::spawn(async move {
87 loop {
88 sleep(Duration::from_secs(interval_secs)).await;
89 if backend.extend_lease(&lease_id, ttl).await.is_err() {
90 break;
91 }
92 }
93 }))
94 }
95
96 async fn apply_cancel_if_needed(&self, job_id: &str, result: Result<()>) -> Result<()> {
99 result?;
100 match self.backend.get_job(job_id).await {
101 Ok(Some(j)) if j.status == JobStatus::Canceled => Err(
102 boson_core::BosonError::internal("job canceled during execution"),
103 ),
104 _ => Ok(()),
105 }
106 }
107
108 async fn drive_run_without_run_rows(self: &Arc<Self>, job: Job) {
109 let run_id = format!("fast-{}", job.job_id);
110 telemetry::record_task_started(
111 &job.task_name,
112 &job.job_id,
113 &run_id,
114 &self.worker.runtime_label,
115 );
116 let start = Utc::now();
117 let result = execute_job(&self.registry, &self.identity, &self.backend, &job).await;
118 let result = self.apply_cancel_if_needed(&job.job_id, result).await;
119 let duration_ms = (Utc::now() - start).num_milliseconds();
120 match result {
121 Ok(()) => {
122 telemetry::record_task_completed(&job.task_name, &job.job_id, &run_id, duration_ms);
123 let mut finished = job;
124 finished.status = JobStatus::Success;
125 self.upsert_job(finished).await;
126 }
127 Err(e) => {
128 let msg = boson_core::sanitize_error_message(&e.to_string());
129 telemetry::record_handler_error(&job.task_name, &job.job_id, &run_id, &msg);
130 if msg.contains("job canceled") {
131 let mut canceled = job;
132 canceled.status = JobStatus::Canceled;
133 self.upsert_job(canceled).await;
134 return;
135 }
136 telemetry::record_task_failed(&job.task_name, &job.job_id, &run_id, &msg, false);
137 let _ = self.backend.revert_job_to_queued(&job.job_id).await;
138 }
139 }
140 }
141
142 async fn upsert_job(&self, job: Job) {
143 if let Err(e) = self.backend.upsert_job(&job).await {
144 telemetry::log_job_upsert_failed(&job.job_id, &job.task_name, &e.to_string());
145 }
146 }
147
148 async fn tick(self: &Arc<Self>) {
149 let discovered = self
150 .backend
151 .distinct_pools_queued()
152 .await
153 .unwrap_or_default();
154 let pools = self.worker.pools_to_poll(discovered);
155 for pool in pools {
156 if let Ok(Some((job, lease_id))) = claim_next_job(
157 &self.backend,
158 &pool,
159 &self.worker.worker_id,
160 self.worker.lease_ttl_secs,
161 )
162 .await
163 {
164 self.drive_run(job, lease_id).await;
165 }
166 }
167 }
168
169 async fn reap_expired_leases(self: Arc<Self>) {
170 if self.worker.lease_ttl_secs <= 0 {
171 return;
172 }
173 loop {
174 sleep(Duration::from_secs(15)).await;
175 let pairs = self
176 .backend
177 .expired_lease_job_pairs()
178 .await
179 .unwrap_or_default();
180 let count = pairs.len();
181 for (lease_id, job_id) in pairs {
182 let _ = self.backend.release_lease(&lease_id).await;
183 let _ = self.backend.revert_job_to_queued(&job_id).await;
184 }
185 telemetry::log_lease_reclaim(count, &self.worker.runtime_label);
186 }
187 }
188}
189
190#[async_trait]
191impl RunLifecycleHost for WorkerEngine {
192 async fn record_run_finish(
193 &self,
194 run_id: &str,
195 status: RunStatus,
196 duration_ms: Option<i64>,
197 error_message: Option<String>,
198 ) -> Result<()> {
199 self.backend
200 .finish_run(run_id, status, duration_ms, error_message)
201 .await
202 }
203
204 async fn put_job(&self, job: Job) {
205 self.upsert_job(job).await;
206 }
207
208 async fn load_task_config(&self, task_name: &str) -> Result<TaskConfig> {
209 if let Some(c) = self.backend.get_task_config(task_name).await? {
210 return Ok(c);
211 }
212 Ok(self.registry.get_or_err(task_name)?.to_task_config())
213 }
214
215 async fn schedule_retry(&self, mut job: Job, delay_ms: u64) {
216 sleep_retry_delay(delay_ms).await;
217 job.attempt += 1;
218 job.status = JobStatus::Queued;
219 self.upsert_job(job).await;
220 }
221}
222
223pub fn spawn_worker(
225 backend: Arc<dyn QueueBackend>,
226 registry: Arc<TaskRegistry>,
227 identity: Arc<dyn ExecutionContextFactory>,
228 worker: WorkerSettings,
229) -> WorkerHandle {
230 let label = worker.runtime_label.clone();
231 let engine = Arc::new(WorkerEngine {
232 backend,
233 registry,
234 identity,
235 worker,
236 });
237 if engine.worker.lease_ttl_secs > 0 {
238 let reaper = Arc::clone(&engine);
239 tokio::spawn(async move {
240 reaper.reap_expired_leases().await;
241 });
242 }
243 tokio::spawn(async move {
244 let poll_ms = engine.worker.worker_poll_interval_ms;
245 loop {
246 engine.tick().await;
247 if poll_ms > 0 {
248 sleep(Duration::from_millis(poll_ms)).await;
249 } else {
250 tokio::task::yield_now().await;
251 }
252 }
253 });
254 WorkerHandle { _label: label }
255}