1use 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#[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 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 let done: Vec<AtomicBool> =
147 self.tasks.iter().map(|_| AtomicBool::new(false)).collect();
148 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 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 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 }
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 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}