1use std::sync::Arc;
47use std::time::Duration;
48
49use crate::error::{Error, Result};
50use crate::handler::IHostedService;
51
52pub use tokio_util::sync::CancellationToken;
53
54pub const STOP_GRACE: Duration = Duration::from_secs(5);
60
61#[async_trait::async_trait]
68pub trait BackgroundService: Send + Sync + 'static {
69 async fn execute(&self, ct: CancellationToken) -> Result<()>;
75}
76
77pub async fn delay(ct: &CancellationToken, duration: Duration) -> bool {
91 tokio::select! {
92 _ = ct.cancelled() => false,
93 _ = tokio::time::sleep(duration) => true,
94 }
95}
96
97#[async_trait::async_trait]
134pub trait PeriodicBackgroundService: Send + Sync + 'static {
135 fn interval(&self) -> Duration;
140
141 async fn tick(&self) -> Result<()>;
144
145 fn stop_on_error(&self) -> bool {
151 false
152 }
153}
154
155#[async_trait::async_trait]
156impl<T: PeriodicBackgroundService + ?Sized> BackgroundService for T {
157 async fn execute(&self, ct: CancellationToken) -> Result<()> {
158 let interval = self.interval();
159 if interval.is_zero() {
160 return Err(Error::Internal(
161 "periodic background service interval must be greater than zero".to_string(),
162 ));
163 }
164
165 while delay(&ct, interval).await {
168 if let Err(err) = self.tick().await {
169 tracing::error!(
170 error = %err,
171 interval_secs = interval.as_secs_f64(),
172 "periodic background service tick failed"
173 );
174 if self.stop_on_error() {
175 return Err(err);
176 }
177 }
178 }
179 Ok(())
180 }
181}
182
183pub struct BackgroundServiceHost {
188 service: Arc<dyn BackgroundService>,
189 run: tokio::sync::Mutex<Option<Running>>,
190}
191
192struct Running {
193 stop: CancellationToken,
194 handle: tokio::task::JoinHandle<Result<()>>,
195}
196
197impl BackgroundServiceHost {
198 pub fn new(service: Arc<dyn BackgroundService>) -> Self {
200 Self {
201 service,
202 run: tokio::sync::Mutex::new(None),
203 }
204 }
205
206 pub(crate) async fn stop_with_grace(&self, grace: Duration) -> Result<()> {
212 let Some(running) = self.run.lock().await.take() else {
214 return Ok(());
215 };
216
217 running.stop.cancel();
218 let mut handle = running.handle;
219 match tokio::time::timeout(grace, &mut handle).await {
220 Ok(Ok(result)) => result,
221 Ok(Err(join_error)) if join_error.is_cancelled() => Ok(()),
223 Ok(Err(join_error)) => Err(Error::Internal(format!(
224 "background service panicked: {join_error}"
225 ))),
226 Err(_) => {
227 handle.abort();
228 Err(Error::Internal(format!(
229 "background service did not observe cancellation within {}s and was aborted",
230 grace.as_secs()
231 )))
232 }
233 }
234 }
235}
236
237#[async_trait::async_trait]
238impl IHostedService for BackgroundServiceHost {
239 async fn start(&self) -> Result<()> {
240 let mut slot = self.run.lock().await;
241 if slot.is_some() {
242 return Err(Error::State(
243 "background service is already running".to_string(),
244 ));
245 }
246
247 let stop = CancellationToken::new();
248 let service = Arc::clone(&self.service);
249 let token = stop.clone();
250 let handle = tokio::spawn(async move {
251 let result = service.execute(token).await;
252 if let Err(err) = &result {
255 tracing::error!(error = %err, "background service exited with an error");
256 }
257 result
258 });
259
260 *slot = Some(Running { stop, handle });
261 Ok(())
262 }
263
264 async fn stop(&self) -> Result<()> {
265 self.stop_with_grace(STOP_GRACE).await
266 }
267}
268
269pub(crate) struct OrderedHostedService<T: ?Sized> {
275 order: i32,
276 inner: Arc<T>,
277}
278
279impl<T: IHostedService + ?Sized> OrderedHostedService<T> {
280 pub(crate) fn new(order: i32, inner: Arc<T>) -> Self {
281 Self { order, inner }
282 }
283}
284
285#[async_trait::async_trait]
286impl<T: IHostedService + ?Sized> IHostedService for OrderedHostedService<T> {
287 async fn start(&self) -> Result<()> {
288 self.inner.start().await
289 }
290
291 async fn stop(&self) -> Result<()> {
292 self.inner.stop().await
293 }
294
295 fn order(&self) -> i32 {
296 self.order
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
304 use std::time::Instant;
305
306 struct Idle;
307
308 #[async_trait::async_trait]
309 impl BackgroundService for Idle {
310 async fn execute(&self, _ct: CancellationToken) -> Result<()> {
311 Ok(())
312 }
313 }
314
315 #[tokio::test]
316 async fn delay_is_interrupted_by_cancellation() {
317 let ct = CancellationToken::new();
318 let cancel = ct.clone();
319 tokio::spawn(async move {
320 tokio::time::sleep(Duration::from_millis(50)).await;
321 cancel.cancel();
322 });
323
324 let started = Instant::now();
325 let slept = delay(&ct, Duration::from_secs(60)).await;
326
327 assert!(!slept, "a cancelled delay must report cancellation");
328 assert!(
329 started.elapsed() < Duration::from_millis(100),
330 "cancellation must interrupt the sleep within 100ms, took {:?}",
331 started.elapsed()
332 );
333 }
334
335 #[tokio::test]
336 async fn delay_waits_the_full_duration_without_cancellation() {
337 let ct = CancellationToken::new();
338 assert!(delay(&ct, Duration::from_millis(10)).await);
339 }
340
341 struct Counting(Arc<AtomicBool>);
342
343 #[async_trait::async_trait]
344 impl BackgroundService for Counting {
345 async fn execute(&self, ct: CancellationToken) -> Result<()> {
346 while delay(&ct, Duration::from_secs(60)).await {
347 self.0.store(true, Ordering::SeqCst);
348 }
349 Ok(())
350 }
351 }
352
353 #[tokio::test]
354 async fn stop_cancels_the_worker_and_joins_it() {
355 let stopped = Arc::new(AtomicBool::new(false));
356 let host = BackgroundServiceHost::new(Arc::new(Counting(Arc::clone(&stopped))));
357
358 host.start().await.expect("start must succeed");
359 tokio::time::sleep(Duration::from_millis(20)).await;
360
361 let started = Instant::now();
362 host.stop().await.expect("stop must succeed");
363 assert!(
364 started.elapsed() < Duration::from_millis(500),
365 "stop must interrupt the worker's sleep"
366 );
367 assert!(!stopped.load(Ordering::SeqCst), "the loop never ticked");
368
369 host.stop().await.expect("second stop must be a no-op");
372 }
373
374 struct Failing;
375
376 #[async_trait::async_trait]
377 impl BackgroundService for Failing {
378 async fn execute(&self, _ct: CancellationToken) -> Result<()> {
379 Err(Error::Internal("worker exploded".to_string()))
380 }
381 }
382
383 #[tokio::test]
384 async fn worker_failure_is_reported_as_a_typed_error_from_stop() {
385 let host = BackgroundServiceHost::new(Arc::new(Failing));
386 host.start().await.expect("start must succeed");
387 tokio::time::sleep(Duration::from_millis(20)).await;
389
390 let err = host
391 .stop()
392 .await
393 .expect_err("failure must not be swallowed");
394 assert!(err.to_string().contains("worker exploded"), "{err}");
395 }
396
397 struct Panicking;
398
399 #[async_trait::async_trait]
400 impl BackgroundService for Panicking {
401 async fn execute(&self, _ct: CancellationToken) -> Result<()> {
402 panic!("boom");
403 }
404 }
405
406 #[tokio::test]
407 async fn worker_panic_becomes_a_typed_error_instead_of_propagating() {
408 let host = BackgroundServiceHost::new(Arc::new(Panicking));
409 host.start().await.expect("start must succeed");
410 tokio::time::sleep(Duration::from_millis(20)).await;
411
412 let err = host.stop().await.expect_err("panic must be a typed error");
413 assert!(
414 err.to_string().contains("background service panicked"),
415 "{err}"
416 );
417 }
418
419 struct Stubborn;
420
421 #[async_trait::async_trait]
422 impl BackgroundService for Stubborn {
423 async fn execute(&self, _ct: CancellationToken) -> Result<()> {
424 tokio::time::sleep(Duration::from_secs(600)).await;
426 Ok(())
427 }
428 }
429
430 #[tokio::test]
431 async fn a_worker_that_ignores_cancellation_is_aborted() {
432 let host = BackgroundServiceHost::new(Arc::new(Stubborn));
433 host.start().await.expect("start must succeed");
434
435 let result = tokio::time::timeout(Duration::from_secs(10), host.stop()).await;
438 let err = result
439 .expect("stop must not hang forever")
440 .expect_err("aborted worker is reported as an error");
441 assert!(err.to_string().contains("aborted"), "{err}");
442 }
443
444 #[tokio::test]
445 async fn idle_worker_stops_cleanly() {
446 let host = BackgroundServiceHost::new(Arc::new(Idle));
447 host.start().await.expect("start must succeed");
448 tokio::time::sleep(Duration::from_millis(20)).await;
449 host.stop().await.expect("a finished worker stops cleanly");
450 }
451
452 struct Ticker {
453 ticks: Arc<AtomicU32>,
454 }
455
456 #[async_trait::async_trait]
457 impl PeriodicBackgroundService for Ticker {
458 fn interval(&self) -> Duration {
459 Duration::from_millis(10)
460 }
461
462 async fn tick(&self) -> Result<()> {
463 self.ticks.fetch_add(1, Ordering::SeqCst);
464 Ok(())
465 }
466 }
467
468 #[tokio::test]
469 async fn periodic_worker_ticks_until_stopped() {
470 let ticks = Arc::new(AtomicU32::new(0));
471 let host = BackgroundServiceHost::new(Arc::new(Ticker {
472 ticks: Arc::clone(&ticks),
473 }));
474
475 host.start().await.expect("start must succeed");
476 let deadline = Instant::now() + Duration::from_secs(2);
478 while ticks.load(Ordering::SeqCst) < 2 && Instant::now() < deadline {
479 tokio::time::sleep(Duration::from_millis(5)).await;
480 }
481 assert!(
482 ticks.load(Ordering::SeqCst) >= 2,
483 "the periodic worker must tick, saw {}",
484 ticks.load(Ordering::SeqCst)
485 );
486
487 let started = Instant::now();
488 host.stop().await.expect("stop must succeed");
489 assert!(
490 started.elapsed() < Duration::from_millis(500),
491 "stop must not wait for the next interval"
492 );
493
494 let after_stop = ticks.load(Ordering::SeqCst);
496 tokio::time::sleep(Duration::from_millis(50)).await;
497 assert_eq!(ticks.load(Ordering::SeqCst), after_stop);
498 }
499
500 struct LongInterval(Arc<AtomicU32>);
501
502 #[async_trait::async_trait]
503 impl PeriodicBackgroundService for LongInterval {
504 fn interval(&self) -> Duration {
505 Duration::from_secs(60)
506 }
507
508 async fn tick(&self) -> Result<()> {
509 self.0.fetch_add(1, Ordering::SeqCst);
510 Ok(())
511 }
512 }
513
514 #[tokio::test]
515 async fn periodic_worker_shutdown_interrupts_the_interval_wait() {
516 let ticks = Arc::new(AtomicU32::new(0));
517 let host = BackgroundServiceHost::new(Arc::new(LongInterval(Arc::clone(&ticks))));
518 host.start().await.expect("start must succeed");
519 tokio::time::sleep(Duration::from_millis(30)).await;
520 assert_eq!(
521 ticks.load(Ordering::SeqCst),
522 0,
523 "first tick is one interval away"
524 );
525
526 let started = Instant::now();
527 host.stop().await.expect("stop must succeed");
528 assert!(
529 started.elapsed() < Duration::from_millis(500),
530 "the 60s interval must not delay shutdown, took {:?}",
531 started.elapsed()
532 );
533 assert_eq!(ticks.load(Ordering::SeqCst), 0);
534 }
535
536 struct FailingTicker {
537 ticks: Arc<AtomicU32>,
538 fatal: bool,
539 }
540
541 #[async_trait::async_trait]
542 impl PeriodicBackgroundService for FailingTicker {
543 fn interval(&self) -> Duration {
544 Duration::from_millis(5)
545 }
546
547 async fn tick(&self) -> Result<()> {
548 self.ticks.fetch_add(1, Ordering::SeqCst);
549 Err(Error::Internal("tick failed".to_string()))
550 }
551
552 fn stop_on_error(&self) -> bool {
553 self.fatal
554 }
555 }
556
557 #[tokio::test]
558 async fn a_failed_tick_is_logged_and_the_loop_continues() {
559 let ticks = Arc::new(AtomicU32::new(0));
560 let host = BackgroundServiceHost::new(Arc::new(FailingTicker {
561 ticks: Arc::clone(&ticks),
562 fatal: false,
563 }));
564 host.start().await.expect("start must succeed");
565
566 let deadline = Instant::now() + Duration::from_secs(2);
567 while ticks.load(Ordering::SeqCst) < 3 && Instant::now() < deadline {
568 tokio::time::sleep(Duration::from_millis(5)).await;
569 }
570 assert!(
571 ticks.load(Ordering::SeqCst) >= 3,
572 "a transient failure must not stop a poller"
573 );
574 host.stop().await.expect("stop must succeed");
576 }
577
578 #[tokio::test]
579 async fn stop_on_error_surfaces_the_typed_tick_error() {
580 let ticks = Arc::new(AtomicU32::new(0));
581 let host = BackgroundServiceHost::new(Arc::new(FailingTicker { ticks, fatal: true }));
582 host.start().await.expect("start must succeed");
583
584 tokio::time::sleep(Duration::from_millis(50)).await;
586 let err = host
587 .stop()
588 .await
589 .expect_err("a fatal tick error must be reported");
590 assert!(err.to_string().contains("tick failed"), "{err}");
591 }
592
593 struct ZeroInterval;
594
595 #[async_trait::async_trait]
596 impl PeriodicBackgroundService for ZeroInterval {
597 fn interval(&self) -> Duration {
598 Duration::ZERO
599 }
600
601 async fn tick(&self) -> Result<()> {
602 Ok(())
603 }
604 }
605
606 #[tokio::test]
607 async fn zero_interval_is_a_typed_configuration_error() {
608 let host = BackgroundServiceHost::new(Arc::new(ZeroInterval));
609 host.start().await.expect("start must succeed");
610 tokio::time::sleep(Duration::from_millis(20)).await;
611
612 let err = host.stop().await.expect_err("zero interval must fail");
613 assert!(err.to_string().contains("greater than zero"), "{err}");
614 }
615}