1use std::{
10 collections::BTreeMap,
11 sync::{Arc, Mutex as StdMutex},
12 time::Duration,
13};
14
15use async_trait::async_trait;
16use serde_json::Value as JsonValue;
17use tokio::{task::JoinHandle, time::sleep};
18use tuwunel_core::{
19 Result,
20 arrayvec::ArrayString,
21 implement,
22 utils::{rand::string_array, time::now_millis},
23};
24
25const TASK_ID_LEN: usize = 16;
27
28const RETENTION_MS: u64 = 7 * 24 * 60 * 60 * 1000;
30
31const CAPACITY: usize = 1024;
33
34const GC_INTERVAL: Duration = Duration::from_hours(1);
36
37type TaskId = ArrayString<TASK_ID_LEN>;
39
40pub struct Service {
46 services: Arc<crate::services::OnceServices>,
47 tasks: StdMutex<BTreeMap<TaskId, Task>>,
48}
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum Status {
58 Scheduled,
60
61 Active,
63
64 Complete,
66
67 Failed,
69}
70
71#[derive(Clone, Debug)]
77pub struct TaskInfo {
78 pub id: TaskId,
80
81 pub action: &'static str,
83
84 pub resource_id: String,
86
87 pub status: Status,
89
90 pub timestamp_ms: u64,
92
93 pub result: Option<JsonValue>,
95
96 pub error: Option<String>,
98}
99
100struct Task {
101 action: &'static str,
102 resource_id: String,
103 status: Status,
104 timestamp_ms: u64,
105 result: Option<JsonValue>,
106 error: Option<String>,
107 handle: Option<JoinHandle<()>>,
108}
109
110#[async_trait]
111impl crate::Service for Service {
112 fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
113 Ok(Arc::new(Self {
114 services: args.services.clone(),
115 tasks: StdMutex::new(BTreeMap::new()),
116 }))
117 }
118
119 async fn worker(self: Arc<Self>) -> Result {
120 loop {
121 self.prune();
122
123 tokio::select! {
124 () = sleep(GC_INTERVAL) => {},
125 () = self.services.server.until_shutdown() => return Ok(()),
126 }
127 }
128 }
129
130 async fn interrupt(&self) { self.abort_all(); }
131
132 fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
133}
134
135#[implement(Service)]
141pub fn spawn<F>(self: &Arc<Self>, action: &'static str, resource_id: String, work: F) -> TaskId
142where
143 F: Future<Output = Result<JsonValue>> + Send + 'static,
144{
145 let id = string_array::<TASK_ID_LEN>();
146
147 let mut tasks = self.tasks.lock().expect("locked");
150 let this = Arc::clone(self);
151 let task_id = id;
152 let handle = self.services.server.runtime().spawn(async move {
153 this.set_active(&task_id);
154 let outcome = work.await;
155 this.finish(&task_id, outcome);
156 });
157
158 tasks.insert(id, Task {
159 action,
160 resource_id,
161 status: Status::Scheduled,
162 timestamp_ms: now_millis(),
163 result: None,
164 error: None,
165 handle: Some(handle),
166 });
167
168 id
169}
170
171#[implement(Service)]
176pub fn get(&self, id: &str) -> Option<TaskInfo> {
177 self.tasks
178 .lock()
179 .expect("locked")
180 .get_key_value(id)
181 .map(|(id, task)| task.info(id))
182}
183
184#[implement(Service)]
189pub fn by_resource(&self, resource_id: &str) -> Vec<TaskInfo> {
190 self.tasks
191 .lock()
192 .expect("locked")
193 .iter()
194 .filter(|(_, task)| task.resource_id.as_str() == resource_id)
195 .map(|(id, task)| task.info(id))
196 .collect()
197}
198
199#[implement(Service)]
204pub fn has_nonterminal(&self, action: &str, resource_id: &str) -> bool {
205 self.tasks
206 .lock()
207 .expect("locked")
208 .values()
209 .any(|task| matches_nonterminal(task, action, resource_id))
210}
211
212fn matches_nonterminal(task: &Task, action: &str, resource_id: &str) -> bool {
213 task.action == action && task.resource_id == resource_id && !task.status.is_terminal()
214}
215
216#[implement(Service)]
221pub fn list(&self) -> Vec<TaskInfo> {
222 self.tasks
223 .lock()
224 .expect("locked")
225 .iter()
226 .map(|(id, task)| task.info(id))
227 .collect()
228}
229
230#[implement(Service)]
231fn set_active(&self, id: &str) {
232 if let Some(task) = self.tasks.lock().expect("locked").get_mut(id) {
233 task.status = Status::Active;
234 }
235}
236
237#[implement(Service)]
238fn finish(&self, id: &str, outcome: Result<JsonValue>) {
239 let mut tasks = self.tasks.lock().expect("locked");
240 let Some(task) = tasks.get_mut(id) else {
241 return;
242 };
243
244 match outcome {
245 | Ok(value) => {
246 task.status = Status::Complete;
247 task.result = Some(value);
248 },
249 | Err(error) => {
250 task.status = Status::Failed;
251 task.error = Some(error.to_string());
252 },
253 }
254}
255
256#[implement(Service)]
257fn prune(&self) {
258 let now = now_millis();
259
260 prune_tasks(&mut self.tasks.lock().expect("locked"), now);
261}
262
263#[implement(Service)]
264fn abort_all(&self) {
265 self.tasks
266 .lock()
267 .expect("locked")
268 .values()
269 .filter_map(|task| task.handle.as_ref())
270 .for_each(JoinHandle::abort);
271}
272
273impl Status {
274 #[must_use]
279 pub fn is_terminal(self) -> bool { matches!(self, Self::Complete | Self::Failed) }
280
281 #[must_use]
286 pub fn as_str(self) -> &'static str {
287 match self {
288 | Self::Scheduled => "scheduled",
289 | Self::Active => "active",
290 | Self::Complete => "complete",
291 | Self::Failed => "failed",
292 }
293 }
294}
295
296impl Task {
297 fn info(&self, id: &TaskId) -> TaskInfo {
298 TaskInfo {
299 id: *id,
300 action: self.action,
301 resource_id: self.resource_id.clone(),
302 status: self.status,
303 timestamp_ms: self.timestamp_ms,
304 result: self.result.clone(),
305 error: self.error.clone(),
306 }
307 }
308}
309
310fn prune_tasks(tasks: &mut BTreeMap<TaskId, Task>, now_ms: u64) {
312 tasks.retain(|_, task| {
313 !task.status.is_terminal() || now_ms.saturating_sub(task.timestamp_ms) < RETENTION_MS
314 });
315
316 let mut timestamps: Vec<u64> = tasks
317 .values()
318 .filter(|task| task.status.is_terminal())
319 .map(|task| task.timestamp_ms)
320 .collect();
321
322 if timestamps.len() <= CAPACITY {
323 return;
324 }
325
326 timestamps.sort_unstable();
327
328 let cutoff = timestamps[timestamps.len().saturating_sub(CAPACITY)];
329
330 tasks.retain(|_, task| !task.status.is_terminal() || task.timestamp_ms >= cutoff);
331}
332
333#[cfg(test)]
334mod tests;