1use std::collections::VecDeque;
9use std::sync::{Arc, Mutex};
10
11use cronoq::{Priority, StatusEvent, TaskStatus};
12use rustc_hash::FxHashMap;
13
14#[derive(Clone, Debug, Default)]
16pub struct TaskMeta {
17 pub pool: Arc<str>,
19 pub key: Option<Arc<str>>,
21 pub kind: Option<Arc<str>>,
23 pub max_attempts: u32,
25 pub status: Option<TaskStatus>,
27 pub attempt: u32,
29 pub created_ns: Option<i64>,
31 pub started_ns: Option<i64>,
33 pub finished_ns: Option<i64>,
35 pub message: Option<Arc<str>>,
37 pub tenant: Option<Arc<str>>,
39 pub priority: Option<Priority>,
41}
42
43#[derive(Default)]
44struct Inner {
45 map: FxHashMap<u64, TaskMeta>,
46 by_key: std::collections::HashMap<Arc<str>, u64>,
48 order: VecDeque<u64>,
49}
50
51pub struct TaskInfoCache {
53 inner: Mutex<Inner>,
54 cap: usize,
55}
56
57impl TaskInfoCache {
58 pub fn new(cap: usize) -> Arc<Self> {
60 Arc::new(TaskInfoCache {
61 inner: Mutex::new(Inner::default()),
62 cap,
63 })
64 }
65
66 fn evict(st: &mut Inner, cap: usize) {
67 while st.map.len() > cap {
68 let Some(id) = st.order.pop_front() else {
69 break;
70 };
71 if let Some(m) = st.map.remove(&id)
72 && let Some(k) = m.key
73 && st.by_key.get(&k) == Some(&id)
74 {
75 st.by_key.remove(&k);
76 }
77 }
78 }
79
80 fn entry<'a>(st: &'a mut Inner, id: u64, pool: &Arc<str>) -> &'a mut TaskMeta {
81 if !st.map.contains_key(&id) {
82 st.order.push_back(id);
83 }
84 st.map.entry(id).or_insert_with(|| TaskMeta {
85 pool: pool.clone(),
86 ..Default::default()
87 })
88 }
89
90 pub fn on_submit(&self, id: u64, meta: TaskMeta) {
92 if self.cap == 0 {
93 return;
94 }
95 let mut st = self.inner.lock().unwrap();
96 if let Some(k) = &meta.key {
97 st.by_key.insert(k.clone(), id);
98 }
99 let pool = meta.pool.clone();
100 let e = Self::entry(&mut st, id, &pool);
101 let (status, attempt, started, finished, message) = (
102 e.status,
103 e.attempt,
104 e.started_ns,
105 e.finished_ns,
106 e.message.clone(),
107 );
108 *e = TaskMeta {
109 status,
110 attempt,
111 started_ns: started,
112 finished_ns: finished,
113 message,
114 ..meta
115 };
116 Self::evict(&mut st, self.cap);
117 }
118
119 pub fn on_event(&self, ev: &StatusEvent, pool: &Arc<str>, at_ns: i64) {
121 if self.cap == 0 {
122 return;
123 }
124 let id = ev.id.0;
125 let mut st = self.inner.lock().unwrap();
126 if let Some(k) = &ev.key {
127 st.by_key.entry(k.clone()).or_insert(id);
128 }
129 let e = Self::entry(&mut st, id, pool);
130 if e.key.is_none() {
131 e.key = ev.key.clone();
132 }
133 e.status = Some(ev.status);
134 e.attempt = e.attempt.max(ev.attempt);
135 if ev.status == TaskStatus::Running {
136 e.started_ns = Some(at_ns);
137 }
138 if ev.status.is_terminal() {
139 e.finished_ns = Some(at_ns);
140 }
141 if let Some(m) = &ev.message {
142 e.message = Some(m.clone());
143 }
144 Self::evict(&mut st, self.cap);
145 }
146
147 pub fn get(&self, id: u64) -> Option<TaskMeta> {
149 self.inner.lock().unwrap().map.get(&id).cloned()
150 }
151
152 pub fn list_finished(
155 &self,
156 pools: &[&str],
157 statuses: &[TaskStatus],
158 after: u64,
159 limit: usize,
160 ) -> Vec<(u64, TaskMeta)> {
161 let st = self.inner.lock().unwrap();
162 let mut ids: Vec<u64> = st
163 .map
164 .iter()
165 .filter(|(id, m)| {
166 **id > after
167 && m.status.is_some_and(|s| {
168 s.is_terminal() && (statuses.is_empty() || statuses.contains(&s))
169 })
170 && (pools.is_empty() || pools.contains(&&*m.pool))
171 })
172 .map(|(id, _)| *id)
173 .collect();
174 ids.sort_unstable();
175 ids.truncate(limit);
176 ids.into_iter()
177 .map(|id| (id, st.map[&id].clone()))
178 .collect()
179 }
180
181 pub fn id_for_key(&self, key: &str) -> Option<u64> {
183 self.inner.lock().unwrap().by_key.get(key).copied()
184 }
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn records_and_evicts() {
193 let c = TaskInfoCache::new(2);
194 let p: Arc<str> = "p".into();
195 c.on_submit(
196 1,
197 TaskMeta {
198 pool: p.clone(),
199 key: Some("k1".into()),
200 max_attempts: 3,
201 ..Default::default()
202 },
203 );
204 let ev = |status, message: Option<&str>| StatusEvent {
205 id: cronoq::TaskId(1),
206 pool: cronoq::PoolId(0),
207 key: None,
208 status,
209 attempt: 1,
210 at: cronoq::Time(0),
211 message: message.map(Arc::from),
212 };
213 c.on_event(&ev(TaskStatus::Running, None), &p, 10);
214 c.on_event(&ev(TaskStatus::Failed, Some("boom")), &p, 20);
215 let m = c.get(1).unwrap();
216 assert_eq!(
217 (m.status, m.attempt, m.started_ns, m.finished_ns),
218 (Some(TaskStatus::Failed), 1, Some(10), Some(20))
219 );
220 assert_eq!(m.message.as_deref(), Some("boom"));
221 assert_eq!(c.id_for_key("k1"), Some(1));
222 c.on_submit(
223 2,
224 TaskMeta {
225 pool: p.clone(),
226 ..Default::default()
227 },
228 );
229 c.on_submit(
230 3,
231 TaskMeta {
232 pool: p.clone(),
233 ..Default::default()
234 },
235 );
236 assert!(
237 c.get(1).is_none() && c.id_for_key("k1").is_none(),
238 "oldest evicted"
239 );
240 }
241
242 #[test]
243 fn lists_finished_tasks_by_pool_status_and_page() {
244 let c = TaskInfoCache::new(10);
245 let ev = |id, status| StatusEvent {
246 id: cronoq::TaskId(id),
247 pool: cronoq::PoolId(0),
248 key: None,
249 status,
250 attempt: 1,
251 at: cronoq::Time(0),
252 message: None,
253 };
254 let (a, b): (Arc<str>, Arc<str>) = ("a".into(), "b".into());
255 c.on_event(&ev(4, TaskStatus::Succeeded), &a, 1);
256 c.on_event(&ev(1, TaskStatus::Failed), &a, 1);
257 c.on_event(&ev(2, TaskStatus::Running), &a, 1);
258 c.on_event(&ev(3, TaskStatus::Succeeded), &b, 1);
259 let ids = |v: Vec<(u64, TaskMeta)>| v.into_iter().map(|(id, _)| id).collect::<Vec<_>>();
260 assert_eq!(ids(c.list_finished(&[], &[], 0, 10)), [1, 3, 4]);
261 assert_eq!(ids(c.list_finished(&["a"], &[], 0, 10)), [1, 4]);
262 assert_eq!(
263 ids(c.list_finished(&[], &[TaskStatus::Succeeded], 0, 10)),
264 [3, 4]
265 );
266 assert_eq!(ids(c.list_finished(&[], &[], 1, 1)), [3]);
267 }
268}