1use std::sync::{Arc, Mutex, MutexGuard};
8use std::time::{Duration, Instant};
9
10use tokio_util::sync::CancellationToken;
11
12use crate::{Result, TaskCoreError};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub struct TaskLease {
17 pub task_id: i64,
19 pub processing_token: i64,
21}
22
23impl TaskLease {
24 #[must_use]
26 pub const fn new(task_id: i64, processing_token: i64) -> Self {
27 Self {
28 task_id,
29 processing_token,
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct TaskLeaseGuard {
37 lease: TaskLease,
38 renewal_timeout: Duration,
39 shutdown_token: Option<CancellationToken>,
40 state: Arc<Mutex<TaskLeaseGuardState>>,
41}
42
43#[derive(Debug)]
44struct TaskLeaseGuardState {
45 last_renewed_at: Instant,
46 termination: Option<TaskLeaseTermination>,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50enum TaskLeaseTermination {
51 Lost,
52 RenewalTimedOut,
53 ShutdownRequested,
54}
55
56impl TaskLeaseGuard {
57 #[must_use]
59 pub fn new(lease: TaskLease, renewal_timeout: Duration) -> Self {
60 Self {
61 lease,
62 renewal_timeout,
63 shutdown_token: None,
64 state: Arc::new(Mutex::new(TaskLeaseGuardState {
65 last_renewed_at: Instant::now(),
66 termination: None,
67 })),
68 }
69 }
70
71 #[must_use]
73 pub fn with_shutdown_token(
74 lease: TaskLease,
75 renewal_timeout: Duration,
76 shutdown_token: CancellationToken,
77 ) -> Self {
78 Self {
79 shutdown_token: Some(shutdown_token),
80 ..Self::new(lease, renewal_timeout)
81 }
82 }
83
84 #[must_use]
86 pub const fn lease(&self) -> TaskLease {
87 self.lease
88 }
89
90 pub fn record_renewed(&self) {
92 let mut state = self.lock_state();
93 if state.termination.is_none() {
94 state.last_renewed_at = Instant::now();
95 }
96 }
97
98 #[must_use]
100 pub fn mark_lost(&self) -> TaskCoreError {
101 let mut state = self.lock_state();
102 state.termination = Some(TaskLeaseTermination::Lost);
103 task_lease_lost(self.lease)
104 }
105
106 #[must_use]
108 pub fn mark_shutdown_requested(&self) -> TaskCoreError {
109 let mut state = self.lock_state();
110 state.termination = Some(TaskLeaseTermination::ShutdownRequested);
111 task_worker_shutdown_requested(self.lease)
112 }
113
114 pub fn ensure_active(&self) -> Result<()> {
120 let mut state = self.lock_state();
121 match state.termination {
122 Some(TaskLeaseTermination::Lost) => return Err(task_lease_lost(self.lease)),
123 Some(TaskLeaseTermination::RenewalTimedOut) => {
124 return Err(task_lease_renewal_timed_out(self.lease));
125 }
126 Some(TaskLeaseTermination::ShutdownRequested) => {
127 return Err(task_worker_shutdown_requested(self.lease));
128 }
129 None => {}
130 }
131 if self
132 .shutdown_token
133 .as_ref()
134 .is_some_and(CancellationToken::is_cancelled)
135 {
136 state.termination = Some(TaskLeaseTermination::ShutdownRequested);
137 return Err(task_worker_shutdown_requested(self.lease));
138 }
139 if state.last_renewed_at.elapsed() >= self.renewal_timeout {
140 state.termination = Some(TaskLeaseTermination::RenewalTimedOut);
141 return Err(task_lease_renewal_timed_out(self.lease));
142 }
143 Ok(())
144 }
145
146 fn lock_state(&self) -> MutexGuard<'_, TaskLeaseGuardState> {
147 match self.state.lock() {
148 Ok(guard) => guard,
149 Err(poisoned) => poisoned.into_inner(),
150 }
151 }
152}
153
154#[derive(Debug, Clone)]
156pub struct TaskExecutionContext {
157 lease_guard: TaskLeaseGuard,
158 shutdown_token: CancellationToken,
159}
160
161impl TaskExecutionContext {
162 #[must_use]
164 pub fn new(
165 lease: TaskLease,
166 renewal_timeout: Duration,
167 shutdown_token: CancellationToken,
168 ) -> Self {
169 Self {
170 lease_guard: TaskLeaseGuard::with_shutdown_token(
171 lease,
172 renewal_timeout,
173 shutdown_token.clone(),
174 ),
175 shutdown_token,
176 }
177 }
178
179 #[must_use]
181 pub const fn lease_guard(&self) -> &TaskLeaseGuard {
182 &self.lease_guard
183 }
184
185 pub fn ensure_active(&self) -> Result<()> {
191 self.lease_guard.ensure_active()
192 }
193
194 pub async fn sleep_or_shutdown(&self, duration: Duration) -> Result<()> {
200 self.lease_guard.ensure_active()?;
201
202 tokio::select! {
203 biased;
204 () = self.shutdown_token.cancelled() => Err(self.lease_guard.mark_shutdown_requested()),
205 () = tokio::time::sleep(duration) => Ok(()),
206 }
207 }
208
209 pub async fn shutdown_requested(&self) -> Result<()> {
215 self.shutdown_token.cancelled().await;
216 Err(self.lease_guard.mark_shutdown_requested())
217 }
218}
219
220pub const fn task_lease_lost(lease: TaskLease) -> TaskCoreError {
222 TaskCoreError::LeaseLost {
223 task_id: lease.task_id,
224 processing_token: lease.processing_token,
225 }
226}
227
228pub const fn task_lease_renewal_timed_out(lease: TaskLease) -> TaskCoreError {
230 TaskCoreError::LeaseRenewalTimedOut {
231 task_id: lease.task_id,
232 processing_token: lease.processing_token,
233 }
234}
235
236pub const fn task_worker_shutdown_requested(lease: TaskLease) -> TaskCoreError {
238 TaskCoreError::WorkerShutdownRequested {
239 task_id: lease.task_id,
240 processing_token: lease.processing_token,
241 }
242}
243
244#[must_use]
246pub fn task_lease_expires_at(
247 now: chrono::DateTime<chrono::Utc>,
248 processing_stale_secs: i64,
249) -> chrono::DateTime<chrono::Utc> {
250 now + chrono::Duration::seconds(processing_stale_secs.max(1))
251}
252
253#[must_use]
255pub fn task_lease_renewal_timeout(processing_stale_secs: i64, heartbeat_secs: u64) -> Duration {
256 let stale_secs = i64_to_u64_saturating(processing_stale_secs.max(1));
257 let heartbeat_secs = heartbeat_secs.max(1);
258 Duration::from_secs(stale_secs.saturating_sub(heartbeat_secs).max(1))
259}
260
261fn i64_to_u64_saturating(value: i64) -> u64 {
262 u64::try_from(value).unwrap_or(u64::MAX)
263}
264
265#[cfg(test)]
266mod tests {
267 use std::time::Duration;
268
269 use chrono::Utc;
270 use tokio_util::sync::CancellationToken;
271
272 use super::{
273 TaskExecutionContext, TaskLease, TaskLeaseGuard, task_lease_expires_at,
274 task_lease_renewal_timeout,
275 };
276
277 #[test]
278 fn lease_guard_reports_lost_lease_after_mark_lost() {
279 let guard = TaskLeaseGuard::new(TaskLease::new(7, 2), Duration::from_mins(1));
280
281 let error = guard.mark_lost();
282
283 assert!(error.is_task_lease_lost());
284 assert!(
285 guard
286 .ensure_active()
287 .is_err_and(|error| error.is_task_lease_lost())
288 );
289 }
290
291 #[test]
292 fn lease_guard_reports_renewal_timeout() {
293 let guard = TaskLeaseGuard::new(TaskLease::new(7, 2), Duration::ZERO);
294
295 let error = guard.ensure_active().expect_err("lease should time out");
296
297 assert!(error.is_task_lease_renewal_timed_out());
298 }
299
300 #[test]
301 fn lease_guard_observes_shutdown_token() {
302 let shutdown_token = CancellationToken::new();
303 let guard = TaskLeaseGuard::with_shutdown_token(
304 TaskLease::new(7, 2),
305 Duration::from_mins(1),
306 shutdown_token.clone(),
307 );
308
309 shutdown_token.cancel();
310
311 assert!(
312 guard
313 .ensure_active()
314 .is_err_and(|error| error.is_task_worker_shutdown_requested())
315 );
316 }
317
318 #[tokio::test]
319 async fn execution_context_sleep_returns_on_shutdown() {
320 let shutdown_token = CancellationToken::new();
321 let context = TaskExecutionContext::new(
322 TaskLease::new(7, 2),
323 Duration::from_mins(1),
324 shutdown_token.clone(),
325 );
326
327 shutdown_token.cancel();
328 let error = context
329 .sleep_or_shutdown(Duration::from_mins(1))
330 .await
331 .expect_err("sleep should stop for shutdown");
332
333 assert!(error.is_task_worker_shutdown_requested());
334 }
335
336 #[test]
337 fn lease_timing_helpers_match_yggdrasil_and_drive_policy() {
338 let now = Utc::now();
339
340 assert_eq!(
341 task_lease_expires_at(now, 60),
342 now + chrono::Duration::seconds(60)
343 );
344 assert_eq!(
345 task_lease_expires_at(now, 0),
346 now + chrono::Duration::seconds(1)
347 );
348 assert_eq!(task_lease_renewal_timeout(60, 10), Duration::from_secs(50));
349 assert_eq!(task_lease_renewal_timeout(1, 10), Duration::from_secs(1));
350 }
351}