Skip to main content

boson_runtime/worker/
loop_.rs

1//! Background worker loop and lifecycle host implementation.
2
3use 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/// Handle to a spawned worker task (optional join).
21#[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
195/// Spawn background worker loop.
196pub 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}