1use std::collections::hash_map::DefaultHasher;
2use std::collections::HashMap;
3use std::hash::{Hash, Hasher};
4use std::sync::atomic::AtomicBool;
5use std::sync::Arc;
6use std::sync::RwLock;
7
8const IN_FLIGHT_SHARDS: usize = 64;
9
10pub(crate) type RunLease = i64;
11pub(crate) type InFlightKey = (i64, RunLease);
12pub(crate) type InFlightMap = Arc<InFlightRegistry>;
13type CancelFlag = Arc<AtomicBool>;
14type InFlightShard = RwLock<HashMap<InFlightKey, InFlightState>>;
15
16pub(crate) struct InFlightState {
18 pub cancel: CancelFlag,
19 pub progress: Arc<std::sync::Mutex<ProgressState>>,
20}
21
22#[derive(Debug)]
24pub struct ProgressState {
25 pub(crate) latest: Option<serde_json::Value>,
27 pub(crate) generation: u64,
29 pub(crate) acked_generation: u64,
31 pub(crate) in_flight: Option<(u64, serde_json::Value)>,
33}
34
35impl ProgressState {
36 pub fn new(initial: Option<serde_json::Value>) -> Self {
37 Self {
38 latest: initial,
39 generation: 0,
40 acked_generation: 0,
41 in_flight: None,
42 }
43 }
44
45 pub fn has_pending(&self) -> bool {
47 self.generation > self.acked_generation
48 }
49
50 pub fn latest(&self) -> Option<&serde_json::Value> {
52 self.latest.as_ref()
53 }
54
55 pub fn clone_latest(&self) -> Option<serde_json::Value> {
57 self.latest.clone()
58 }
59
60 pub fn set_progress(&mut self, percent: u8, message: Option<&str>) {
63 let percent = percent.min(100);
64 let existing_metadata = self
65 .latest
66 .as_ref()
67 .and_then(|v| v.get("metadata"))
68 .cloned();
69
70 let mut value = serde_json::json!({ "percent": percent });
71 if let Some(msg) = message {
72 value["message"] = serde_json::Value::String(msg.to_string());
73 }
74 if let Some(meta) = existing_metadata {
75 value["metadata"] = meta;
76 }
77 self.latest = Some(value);
78 self.generation += 1;
79 }
80
81 pub fn merge_metadata(&mut self, updates: &serde_json::Map<String, serde_json::Value>) -> bool {
84 let progress = self.latest.get_or_insert_with(|| serde_json::json!({}));
85 let metadata = progress
86 .as_object_mut()
87 .expect("progress is always an object")
88 .entry("metadata")
89 .or_insert_with(|| serde_json::json!({}));
90
91 if let Some(meta_obj) = metadata.as_object_mut() {
92 for (k, v) in updates {
93 meta_obj.insert(k.clone(), v.clone());
94 }
95 self.generation += 1;
96 true
97 } else {
98 false
99 }
100 }
101
102 pub fn pending_snapshot(&self) -> Option<(serde_json::Value, u64)> {
104 if self.acked_generation >= self.generation {
105 return None;
106 }
107 self.latest.as_ref().map(|v| (v.clone(), self.generation))
108 }
109
110 pub fn ack(&mut self, generation: u64) {
113 if generation > self.acked_generation {
114 self.acked_generation = generation;
115 }
116 }
117}
118
119pub(crate) struct InFlightRegistry {
120 shards: Box<[InFlightShard]>,
121}
122
123impl Default for InFlightRegistry {
124 fn default() -> Self {
125 let mut shards = Vec::with_capacity(IN_FLIGHT_SHARDS);
126 for _ in 0..IN_FLIGHT_SHARDS {
127 shards.push(RwLock::new(HashMap::new()));
128 }
129 Self {
130 shards: shards.into_boxed_slice(),
131 }
132 }
133}
134
135impl InFlightRegistry {
136 pub fn insert(&self, key: InFlightKey, state: InFlightState) {
137 let mut guard = self
138 .shard_for(&key)
139 .write()
140 .expect("in_flight shard poisoned");
141 guard.insert(key, state);
142 }
143
144 pub fn remove(&self, key: InFlightKey) -> Option<InFlightState> {
145 let mut guard = self
146 .shard_for(&key)
147 .write()
148 .expect("in_flight shard poisoned");
149 guard.remove(&key)
150 }
151
152 pub fn get_cancel(&self, key: InFlightKey) -> Option<CancelFlag> {
153 let guard = self
154 .shard_for(&key)
155 .read()
156 .expect("in_flight shard poisoned");
157 guard.get(&key).map(|s| s.cancel.clone())
158 }
159
160 pub fn keys(&self) -> Vec<InFlightKey> {
161 let mut keys = Vec::new();
162 for shard in &*self.shards {
163 let guard = shard.read().expect("in_flight shard poisoned");
164 keys.extend(guard.keys().copied());
165 }
166 keys
167 }
168
169 pub fn flags(&self) -> Vec<CancelFlag> {
170 let mut flags = Vec::new();
171 for shard in &*self.shards {
172 let guard = shard.read().expect("in_flight shard poisoned");
173 flags.extend(guard.values().map(|s| s.cancel.clone()));
174 }
175 flags
176 }
177
178 pub fn snapshot_pending_progress(&self) -> Vec<(i64, i64, u64, serde_json::Value)> {
184 let mut result = Vec::new();
185 for shard in &*self.shards {
186 let guard = shard.read().expect("in_flight shard poisoned");
187 for (&(job_id, run_lease), state) in guard.iter() {
188 let mut progress = state.progress.lock().expect("progress lock poisoned");
189 if progress.has_pending() && progress.in_flight.is_none() {
190 if let Some(ref value) = progress.latest {
191 let gen = progress.generation;
192 let snapshot = value.clone();
193 progress.in_flight = Some((gen, snapshot.clone()));
194 result.push((job_id, run_lease, gen, snapshot));
195 }
196 }
197 }
198 }
199 result
200 }
201
202 pub fn ack_progress(&self, acked: &[(i64, i64, u64)]) {
204 for &(job_id, run_lease, generation) in acked {
205 let key = (job_id, run_lease);
206 let guard = self
207 .shard_for(&key)
208 .read()
209 .expect("in_flight shard poisoned");
210 if let Some(state) = guard.get(&key) {
211 let mut progress = state.progress.lock().expect("progress lock poisoned");
212 if progress
213 .in_flight
214 .as_ref()
215 .is_some_and(|(gen, _)| *gen == generation)
216 {
217 progress.acked_generation = progress.acked_generation.max(generation);
218 progress.in_flight = None;
219 }
220 }
221 }
222 }
223
224 pub fn clear_in_flight_progress(&self, failed: &[(i64, i64, u64)]) {
226 for &(job_id, run_lease, generation) in failed {
227 let key = (job_id, run_lease);
228 let guard = self
229 .shard_for(&key)
230 .read()
231 .expect("in_flight shard poisoned");
232 if let Some(state) = guard.get(&key) {
233 let mut progress = state.progress.lock().expect("progress lock poisoned");
234 if progress
235 .in_flight
236 .as_ref()
237 .is_some_and(|(gen, _)| *gen == generation)
238 {
239 progress.in_flight = None;
240 }
241 }
242 }
243 }
244
245 fn shard_for(&self, key: &InFlightKey) -> &InFlightShard {
246 let mut hasher = DefaultHasher::new();
247 key.hash(&mut hasher);
248 let idx = (hasher.finish() as usize) % self.shards.len();
249 &self.shards[idx]
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn ack_progress_does_not_roll_back_newer_explicit_flush() {
259 let registry = InFlightRegistry::default();
260 let key = (42, 7);
261 let progress = Arc::new(std::sync::Mutex::new(ProgressState {
262 latest: Some(serde_json::json!({"percent": 90})),
263 generation: 2,
264 acked_generation: 2,
265 in_flight: Some((1, serde_json::json!({"percent": 50}))),
266 }));
267
268 registry.insert(
269 key,
270 InFlightState {
271 cancel: Arc::new(AtomicBool::new(false)),
272 progress: progress.clone(),
273 },
274 );
275
276 registry.ack_progress(&[(key.0, key.1, 1)]);
277
278 let state = progress.lock().expect("progress lock poisoned");
279 assert_eq!(state.acked_generation, 2);
280 assert!(state.in_flight.is_none());
281 }
282}