systemprompt_traits/
background_tasks.rs1use std::future::Future;
32use std::panic::AssertUnwindSafe;
33use std::time::Duration;
34
35use futures::FutureExt;
36use tokio::task::{JoinError, JoinHandle};
37use tokio_util::sync::CancellationToken;
38use tokio_util::task::TaskTracker;
39use tracing::Instrument;
40use tracing::instrument::WithSubscriber;
41
42#[derive(Debug, Clone, Default)]
44pub struct BackgroundTasks {
45 tracker: TaskTracker,
46 cancel: CancellationToken,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51#[must_use]
52pub enum DrainOutcome {
53 Drained,
54 TimedOut { in_flight: usize },
55}
56
57impl DrainOutcome {
58 #[must_use]
59 pub const fn is_drained(self) -> bool {
60 matches!(self, Self::Drained)
61 }
62}
63
64impl BackgroundTasks {
65 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 pub fn spawn<F>(&self, name: &'static str, task: F)
71 where
72 F: Future<Output = ()> + Send + 'static,
73 {
74 self.tracker.spawn(
75 supervised(name, task)
76 .in_current_span()
77 .with_current_subscriber(),
78 );
79 }
80
81 pub fn spawn_cancellable<F, Fut>(&self, name: &'static str, task: F)
82 where
83 F: FnOnce(CancellationToken) -> Fut,
84 Fut: Future<Output = ()> + Send + 'static,
85 {
86 let token = self.cancel.child_token();
87 self.spawn(name, task(token));
88 }
89
90 #[must_use]
91 pub fn cancellation_token(&self) -> CancellationToken {
92 self.cancel.child_token()
93 }
94
95 #[must_use]
96 pub fn in_flight(&self) -> usize {
97 self.tracker.len()
98 }
99
100 #[must_use]
101 pub fn is_shutting_down(&self) -> bool {
102 self.cancel.is_cancelled()
103 }
104
105 pub async fn drain(&self, timeout: Duration) -> DrainOutcome {
106 self.tracker.close();
107 let waited = tokio::time::timeout(timeout, self.tracker.wait()).await;
108 self.tracker.reopen();
109 match waited {
110 Ok(()) => DrainOutcome::Drained,
111 Err(_elapsed) => DrainOutcome::TimedOut {
112 in_flight: self.tracker.len(),
113 },
114 }
115 }
116
117 pub async fn shutdown(&self, timeout: Duration) -> DrainOutcome {
118 self.cancel.cancel();
119 self.drain(timeout).await
120 }
121}
122
123async fn supervised<F>(name: &'static str, task: F)
124where
125 F: Future<Output = ()> + Send + 'static,
126{
127 if AssertUnwindSafe(task).catch_unwind().await.is_err() {
128 tracing::error!(task = name, "Background task panicked");
129 }
130}
131
132#[derive(Debug)]
134#[must_use = "dropping an OwnedTask aborts it"]
135pub struct OwnedTask<T> {
136 name: &'static str,
137 handle: JoinHandle<T>,
138}
139
140impl<T: Send + 'static> OwnedTask<T> {
141 pub fn spawn<F>(name: &'static str, task: F) -> Self
142 where
143 F: Future<Output = T> + Send + 'static,
144 {
145 Self {
146 name,
147 handle: tokio::spawn(task.in_current_span().with_current_subscriber()),
148 }
149 }
150
151 #[must_use]
152 pub const fn name(&self) -> &'static str {
153 self.name
154 }
155
156 #[must_use]
157 pub fn is_finished(&self) -> bool {
158 self.handle.is_finished()
159 }
160
161 pub fn abort(&self) {
162 self.handle.abort();
163 }
164
165 pub async fn join(mut self) -> Result<T, JoinError> {
166 (&mut self.handle).await
167 }
168
169 pub async fn abort_and_join(mut self) -> Option<T> {
170 self.handle.abort();
171 match (&mut self.handle).await {
172 Ok(value) => Some(value),
173 Err(e) if e.is_cancelled() => None,
174 Err(e) => {
175 tracing::error!(task = self.name, error = %e, "Owned task panicked");
176 None
177 },
178 }
179 }
180}
181
182impl<T> Drop for OwnedTask<T> {
183 fn drop(&mut self) {
184 self.handle.abort();
185 }
186}