1use super::{Error, LOG_TARGET};
16use async_trait::async_trait;
17use futures::stream::{FuturesUnordered, StreamExt};
18use pingora::server::ShutdownWatch;
19use pingora::services::background::BackgroundService;
20use std::sync::atomic::{AtomicU32, Ordering};
21use std::time::{Duration, Instant};
22use tokio::time::{MissedTickBehavior, interval};
23use tracing::{debug, error, info, warn};
24
25fn duration_to_string(duration: Duration) -> String {
26 let secs = duration.as_secs_f64();
27 if secs < 60.0 {
28 format!("{secs:.1}s")
29 } else if secs < 3600.0 {
30 format!("{:.1}m", secs / 60.0)
31 } else if secs < 86400.0 {
32 format!("{:.1}h", secs / 3600.0)
33 } else {
34 format!("{:.1}d", secs / 86400.0)
35 }
36}
37
38#[async_trait]
40pub trait BackgroundTask: Sync + Send {
41 async fn execute(&self, count: u32) -> Result<bool, Error>;
51}
52
53pub struct BackgroundTaskService {
55 name: String,
56 count: AtomicU32,
57 tasks: Vec<(String, Box<dyn BackgroundTask>)>, interval: Duration,
59 immediately: bool,
60 initial_delay: Option<Duration>,
61}
62
63impl BackgroundTaskService {
64 pub fn new(
66 name: &str,
67 interval: Duration,
68 tasks: Vec<(String, Box<dyn BackgroundTask>)>,
69 ) -> Self {
70 Self {
71 name: name.to_string(),
72 count: AtomicU32::new(0),
73 tasks,
74 interval,
75 immediately: false,
76 initial_delay: None,
77 }
78 }
79 pub fn new_single(
81 name: &str,
82 interval: Duration,
83 task_name: &str,
84 task: Box<dyn BackgroundTask>,
85 ) -> Self {
86 Self::new(name, interval, vec![(task_name.to_string(), task)])
87 }
88 pub fn set_immediately(&mut self, immediately: bool) {
90 self.immediately = immediately;
91 }
92 pub fn set_initial_delay(&mut self, initial_delay: Option<Duration>) {
93 self.initial_delay = initial_delay;
94 }
95 pub fn add_task(&mut self, task_name: &str, task: Box<dyn BackgroundTask>) {
98 self.tasks.push((task_name.to_string(), task));
99 }
100 pub fn name(&self) -> &str {
101 &self.name
102 }
103}
104
105#[async_trait]
106impl BackgroundService for BackgroundTaskService {
107 async fn start(&self, mut shutdown: ShutdownWatch) {
108 let task_names: Vec<_> =
109 self.tasks.iter().map(|(name, _)| name.as_str()).collect();
110 info!(
111 target: LOG_TARGET,
112 name = self.name,
113 tasks = task_names.join(", "),
114 interval = duration_to_string(self.interval),
115 "background service is running",
116 );
117
118 if let Some(initial_delay) = self.initial_delay {
119 tokio::time::sleep(initial_delay).await;
120 }
121 let mut period = interval(self.interval);
122 period.set_missed_tick_behavior(MissedTickBehavior::Delay);
125 if !self.immediately {
127 period.tick().await;
128 }
129
130 let mut running = FuturesUnordered::new();
146 let mut started: Vec<Option<Instant>> = vec![None; self.tasks.len()];
147 let mut runs: Vec<u32> = vec![0; self.tasks.len()];
148
149 loop {
150 tokio::select! {
151 _ = shutdown.changed() => {
152 info!(
153 target: LOG_TARGET,
154 name = self.name,
155 "background service is shutting down"
156 );
157 break;
158 }
159 _ = period.tick() => {
160 let cycle = self.count.fetch_add(1, Ordering::Relaxed);
161 let mut skipped = vec![];
162 for (index, (task_name, task)) in
163 self.tasks.iter().enumerate()
164 {
165 if let Some(since) = started[index] {
166 skipped.push(format!(
167 "{task_name}({})",
168 duration_to_string(since.elapsed())
169 ));
170 continue;
171 }
172 started[index] = Some(Instant::now());
173 let count = runs[index];
174 runs[index] = count.wrapping_add(1);
175 running.push(async move {
176 let task_start = Instant::now();
177 let result = task.execute(count).await;
178 (index, result, task_start.elapsed())
179 });
180 }
181 if !skipped.is_empty() {
182 warn!(
183 target: LOG_TARGET,
184 name = self.name,
185 tasks = skipped.join(", "),
186 cycle,
187 "background tasks still running, left out of this round"
188 );
189 }
190 }
191 Some((index, result, elapsed)) = running.next(),
192 if !running.is_empty() =>
193 {
194 started[index] = None;
195 let task_name = self.tasks[index].0.as_str();
196 match result {
197 Ok(true) => {
198 debug!(
199 target: LOG_TARGET,
200 name = self.name,
201 task = task_name,
202 elapsed = duration_to_string(elapsed),
203 "background task executed successfully"
204 );
205 }
206 Ok(false) => {
207 }
209 Err(e) => {
210 error!(
211 target: LOG_TARGET,
212 name = self.name,
213 task = task_name,
214 error = %e,
215 "background task failed"
216 );
217 }
218 }
219 }
220 }
221 }
222 while running.next().await.is_some() {}
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use async_trait::async_trait;
232 use pretty_assertions::assert_eq;
233
234 #[test]
235 fn test_duration_to_string() {
236 assert_eq!(duration_to_string(Duration::from_secs(1)), "1.0s");
237 assert_eq!(duration_to_string(Duration::from_secs(60)), "1.0m");
238 assert_eq!(duration_to_string(Duration::from_secs(3600)), "1.0h");
239 assert_eq!(duration_to_string(Duration::from_secs(86400)), "1.0d");
240 }
241
242 #[tokio::test]
245 async fn test_slow_task_does_not_hold_up_the_others() {
246 use std::sync::Arc;
247
248 use std::sync::Mutex;
249
250 struct Counting {
251 runs: Arc<AtomicU32>,
252 counts: Arc<Mutex<Vec<u32>>>,
253 takes: Duration,
254 }
255 #[async_trait]
256 impl BackgroundTask for Counting {
257 async fn execute(&self, count: u32) -> Result<bool, Error> {
258 self.runs.fetch_add(1, Ordering::Relaxed);
259 self.counts.lock().unwrap().push(count);
260 tokio::time::sleep(self.takes).await;
261 Ok(true)
262 }
263 }
264 let quick = Arc::new(AtomicU32::new(0));
265 let slow = Arc::new(AtomicU32::new(0));
266 let quick_counts = Arc::new(Mutex::new(vec![]));
267 let slow_counts = Arc::new(Mutex::new(vec![]));
268 let service = BackgroundTaskService::new(
269 "test",
270 Duration::from_millis(50),
271 vec![
272 (
273 "quick".to_string(),
274 Box::new(Counting {
275 runs: quick.clone(),
276 counts: quick_counts.clone(),
277 takes: Duration::ZERO,
278 }),
279 ),
280 (
281 "slow".to_string(),
282 Box::new(Counting {
283 runs: slow.clone(),
284 counts: slow_counts.clone(),
285 takes: Duration::from_millis(400),
286 }),
287 ),
288 ],
289 );
290 let (stop, shutdown) = tokio::sync::watch::channel(false);
291 let stopper = async {
292 tokio::time::sleep(Duration::from_millis(620)).await;
293 stop.send(true).unwrap();
294 };
295 tokio::join!(service.start(shutdown), stopper);
296
297 let quick = quick.load(Ordering::Relaxed);
301 let slow = slow.load(Ordering::Relaxed);
302 assert_eq!(true, quick >= 8, "quick ran {quick} times");
303 assert_eq!(true, (1..=3).contains(&slow), "slow ran {slow} times");
304
305 assert_eq!((0..slow).collect::<Vec<_>>(), *slow_counts.lock().unwrap());
308 assert_eq!(
309 (0..quick).collect::<Vec<_>>(),
310 *quick_counts.lock().unwrap()
311 );
312 }
313
314 #[test]
315 fn new_background_task_service() {
316 struct TestTask {}
317 #[async_trait]
318 impl BackgroundTask for TestTask {
319 async fn execute(&self, _count: u32) -> Result<bool, Error> {
320 Ok(true)
321 }
322 }
323 let mut service = BackgroundTaskService::new(
324 "test",
325 Duration::from_secs(1),
326 vec![
327 ("task1".to_string(), Box::new(TestTask {})),
328 ("task2".to_string(), Box::new(TestTask {})),
329 ],
330 );
331 service.add_task("task3", Box::new(TestTask {}));
332
333 assert_eq!(service.name(), "test");
334 assert_eq!(service.tasks.len(), 3);
335 assert_eq!(service.tasks[0].0, "task1");
336 assert_eq!(service.tasks[1].0, "task2");
337 assert_eq!(service.tasks[2].0, "task3");
338 assert_eq!(false, service.immediately);
339
340 let mut service = BackgroundTaskService::new_single(
341 "test",
342 Duration::from_secs(1),
343 "task1",
344 Box::new(TestTask {}),
345 );
346 service.set_immediately(true);
347 assert_eq!(service.name(), "test");
348 assert_eq!(service.tasks.len(), 1);
349 assert_eq!(true, service.immediately);
350 }
351}