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::future::join_all;
18use pingora::server::ShutdownWatch;
19use pingora::services::background::BackgroundService;
20use std::sync::atomic::{AtomicBool, 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 cycle that overruns must not be followed by a burst of catch-up
123        // ticks; the next one is due one interval after it finished.
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        loop {
131            tokio::select! {
132                _ = shutdown.changed() => {
133                    info!(
134                        target: LOG_TARGET,
135                        name = self.name,
136                        "background service is shutting down"
137                    );
138                    break;
139                }
140                _ = period.tick() => {
141                    let cycle_start = Instant::now();
142                    let count = self.count.fetch_add(1, Ordering::Relaxed);
143
144                    // Every task flags itself done, so a cycle that overruns
145                    // can name the ones still going.
146                    let done: Vec<AtomicBool> =
147                        self.tasks.iter().map(|_| AtomicBool::new(false)).collect();
148                    // Create a collection of futures to run all tasks concurrently.
149                    let futures = self.tasks.iter().zip(done.iter()).map(|((task_name, task), flag)| async move {
150                        let task_start = Instant::now();
151                        let result = task.execute(count).await;
152                        flag.store(true, Ordering::Relaxed);
153                        (task_name, result, task_start.elapsed())
154                    });
155
156                    // Await all tasks to complete in parallel. Nothing is
157                    // cancelled: a task is not stopped halfway through its
158                    // work, it is only reported.
159                    let mut all = std::pin::pin!(join_all(futures));
160                    let results = loop {
161                        tokio::select! {
162                            results = &mut all => break results,
163                            _ = tokio::time::sleep(self.interval) => {
164                                let running: Vec<&str> = self
165                                    .tasks
166                                    .iter()
167                                    .zip(done.iter())
168                                    .filter(|(_, flag)| !flag.load(Ordering::Relaxed))
169                                    .map(|((task_name, _), _)| task_name.as_str())
170                                    .collect();
171                                warn!(
172                                    target: LOG_TARGET,
173                                    name = self.name,
174                                    tasks = running.join(", "),
175                                    elapsed = duration_to_string(cycle_start.elapsed()),
176                                    "background tasks still running past the interval"
177                                );
178                            }
179                        }
180                    };
181
182                    let mut success_tasks = Vec::new();
183                    let mut failed_tasks = Vec::new();
184
185                    // Process results for logging.
186                    for (task_name, result, elapsed) in results {
187                        match result {
188                            Ok(true) => {
189                                success_tasks.push(task_name.as_str());
190                                debug!(
191                                    target: LOG_TARGET,
192                                    name = self.name,
193                                    task = task_name,
194                                    elapsed = duration_to_string(elapsed),
195                                    "background task executed successfully"
196                                );
197                            }
198                            Ok(false) => {
199                                // Task was skipped, do nothing.
200                            }
201                            Err(e) => {
202                                failed_tasks.push(task_name.as_str());
203                                error!(
204                                    target: LOG_TARGET,
205                                    name = self.name,
206                                    task = task_name,
207                                    error = %e,
208                                    "background task failed"
209                                );
210                            }
211                        }
212                    }
213
214                    // A routine cycle is debug noise; failures were logged
215                    // above at error level.
216                    if !success_tasks.is_empty() || !failed_tasks.is_empty() {
217                         debug!(
218                            target: LOG_TARGET,
219                            name = self.name,
220                            cycle = count,
221                            success_count = success_tasks.len(),
222                            failed_count = failed_tasks.len(),
223                            total_elapsed = duration_to_string(cycle_start.elapsed()),
224                            "background service cycle completed",
225                        );
226                    }
227                }
228            }
229        }
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236    use async_trait::async_trait;
237    use pretty_assertions::assert_eq;
238
239    #[test]
240    fn test_duration_to_string() {
241        assert_eq!(duration_to_string(Duration::from_secs(1)), "1.0s");
242        assert_eq!(duration_to_string(Duration::from_secs(60)), "1.0m");
243        assert_eq!(duration_to_string(Duration::from_secs(3600)), "1.0h");
244        assert_eq!(duration_to_string(Duration::from_secs(86400)), "1.0d");
245    }
246
247    #[test]
248    fn new_background_task_service() {
249        struct TestTask {}
250        #[async_trait]
251        impl BackgroundTask for TestTask {
252            async fn execute(&self, _count: u32) -> Result<bool, Error> {
253                Ok(true)
254            }
255        }
256        let mut service = BackgroundTaskService::new(
257            "test",
258            Duration::from_secs(1),
259            vec![
260                ("task1".to_string(), Box::new(TestTask {})),
261                ("task2".to_string(), Box::new(TestTask {})),
262            ],
263        );
264        service.add_task("task3", Box::new(TestTask {}));
265
266        assert_eq!(service.name(), "test");
267        assert_eq!(service.tasks.len(), 3);
268        assert_eq!(service.tasks[0].0, "task1");
269        assert_eq!(service.tasks[1].0, "task2");
270        assert_eq!(service.tasks[2].0, "task3");
271        assert_eq!(false, service.immediately);
272
273        let mut service = BackgroundTaskService::new_single(
274            "test",
275            Duration::from_secs(1),
276            "task1",
277            Box::new(TestTask {}),
278        );
279        service.set_immediately(true);
280        assert_eq!(service.name(), "test");
281        assert_eq!(service.tasks.len(), 1);
282        assert_eq!(true, service.immediately);
283    }
284}