Skip to main content

pingap_core/
service.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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/// A unified trait for any task that can be run in the background.
39#[async_trait]
40pub trait BackgroundTask: Sync + Send {
41    /// Executes a single iteration of the task.
42    ///
43    /// # Arguments
44    /// * `count` - The current execution cycle number.
45    ///
46    /// # Returns
47    /// * `Ok(true)` if the task performed meaningful work and should be logged as "success".
48    /// * `Ok(false)` if the task was skipped or did no work.
49    /// * `Err(Error)` if the task failed.
50    async fn execute(&self, count: u32) -> Result<bool, Error>;
51}
52
53/// A unified background service runner that can handle one or more named tasks.
54pub struct BackgroundTaskService {
55    name: String,
56    count: AtomicU32,
57    tasks: Vec<(String, Box<dyn BackgroundTask>)>, // Holds named tasks
58    interval: Duration,
59    immediately: bool,
60    initial_delay: Option<Duration>,
61}
62
63impl BackgroundTaskService {
64    /// Creates a new service to run multiple background tasks.
65    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    /// A convenience constructor for creating a service with a single task.
80    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    /// Set whether the service should run immediately or wait for the interval
89    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    /// Add a task to the service
96    /// This is useful for adding tasks to the service after it has been created
97    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        // A tick that comes late must not be followed by a burst of
123        // catch-up ticks; the next one is due one interval after it.
124        period.set_missed_tick_behavior(MissedTickBehavior::Delay);
125        // The first tick fires immediately, which is often not desired. We skip it.
126        if !self.immediately {
127            period.tick().await;
128        }
129
130        // The tasks that are running, and since when each of them is.
131        //
132        // A round used to wait for every task before the next round could
133        // start. One slow task - an ACME order, the hourly sweep of a
134        // cache directory, a push to a gateway that does not answer - held
135        // all the others up for as long as it took: the log was not
136        // flushed, metrics were not pushed, and a task that acts on every
137        // sixtieth round drifted off its hour. Each task now keeps its own
138        // time: a round starts the ones that are free and leaves out the
139        // one that is still at it.
140        //
141        // And its own count, of the times it ran. A task that acts on
142        // every nth run still sees every number: left out of a round, it
143        // is given that round's turn the next time, where one count for
144        // all would have had it miss the number it was waiting for.
145        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                            // Task was skipped, do nothing.
208                        }
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        // Nothing is cancelled: a task is not stopped halfway through its
223        // work, as it was not when a round was waited for.
224        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    /// Regression: a round waited for every task, so one slow task held
243    /// up all the others for as long as it took.
244    #[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        // About twelve rounds went by. The slow task was started when it
298        // was free, twice; the quick one in every round, where it used to
299        // get the two that the slow one let through.
300        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        // Each task is given the number of its own run, so one that acts
306        // on every nth does not miss its number in a round it sat out.
307        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}