pipedash_core/infrastructure/
deduplication.rs1use std::collections::hash_map::DefaultHasher;
2use std::future::Future;
3use std::hash::{
4 Hash,
5 Hasher,
6};
7use std::sync::Arc;
8
9use dashmap::DashMap;
10use sha2::{
11 Digest,
12 Sha256,
13};
14use tokio::sync::oneshot;
15
16use crate::application::Generation;
17
18type RequestId = u64;
19type Waiters<T, E> = Vec<oneshot::Sender<Result<Arc<T>, E>>>;
20type InFlight<T, E> = DashMap<RequestId, Waiters<T, E>>;
21
22pub struct RequestDeduplicator<T: Clone, E: Clone> {
23 in_flight: Arc<InFlight<T, E>>,
24}
25
26struct InFlightGuard<'a, T, E> {
27 in_flight: &'a InFlight<T, E>,
28 request_id: RequestId,
29 armed: bool,
30}
31
32impl<T, E> InFlightGuard<'_, T, E> {
33 fn take_waiters(mut self) -> Waiters<T, E> {
34 self.armed = false;
35 self.in_flight
36 .remove(&self.request_id)
37 .map(|(_, waiters)| waiters)
38 .unwrap_or_default()
39 }
40}
41
42impl<T, E> Drop for InFlightGuard<'_, T, E> {
43 fn drop(&mut self) {
44 if self.armed {
45 self.in_flight.remove(&self.request_id);
46 }
47 }
48}
49
50impl<T: Clone, E: Clone> RequestDeduplicator<T, E> {
51 pub fn new() -> Self {
52 Self {
53 in_flight: Arc::new(DashMap::new()),
54 }
55 }
56
57 pub async fn deduplicate<F, Fut>(&self, request_id: RequestId, operation: F) -> Result<T, E>
58 where
59 F: FnOnce() -> Fut,
60 Fut: Future<Output = Result<T, E>>,
61 {
62 let waiter = match self.in_flight.entry(request_id) {
63 dashmap::Entry::Occupied(mut entry) => {
64 let (tx, rx) = oneshot::channel();
65 entry.get_mut().push(tx);
66 Some(rx)
67 }
68 dashmap::Entry::Vacant(entry) => {
69 entry.insert(Vec::new());
70 None
71 }
72 };
73
74 if let Some(rx) = waiter {
75 return match rx.await {
76 Ok(shared) => shared.map(|value| (*value).clone()),
77 Err(_) => operation().await,
78 };
79 }
80
81 let guard = InFlightGuard {
82 in_flight: &self.in_flight,
83 request_id,
84 armed: true,
85 };
86 let result = operation().await;
87 let waiters = guard.take_waiters();
88
89 if !waiters.is_empty() {
90 let shared = match &result {
91 Ok(value) => Ok(Arc::new(value.clone())),
92 Err(e) => Err(e.clone()),
93 };
94 for waiter in waiters {
95 let _ = waiter.send(shared.clone());
96 }
97 }
98
99 result
100 }
101}
102
103impl<T: Clone, E: Clone> Default for RequestDeduplicator<T, E> {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109pub fn hash_request(provider_id: i64, generation: Generation, endpoint: &str) -> RequestId {
110 let mut hasher = DefaultHasher::new();
111 provider_id.hash(&mut hasher);
112 generation.hash(&mut hasher);
113 endpoint.hash(&mut hasher);
114 hasher.finish()
115}
116
117pub fn hash_pipeline_run(
118 run_number: i64, status: &str, branch: Option<&str>, started_at: &str,
119 duration_seconds: Option<i64>, commit_sha: Option<&str>,
120) -> String {
121 let mut hasher = Sha256::new();
122 hasher.update(run_number.to_string().as_bytes());
123 hasher.update(status.as_bytes());
124 hasher.update(branch.unwrap_or("").as_bytes());
125 hasher.update(started_at.as_bytes());
126 if let Some(duration) = duration_seconds {
127 hasher.update(duration.to_string().as_bytes());
128 }
129 hasher.update(commit_sha.unwrap_or("").as_bytes());
130
131 hasher
132 .finalize()
133 .iter()
134 .map(|b| format!("{b:02x}"))
135 .collect()
136}
137
138impl<T: Clone, E: Clone> Clone for RequestDeduplicator<T, E> {
139 fn clone(&self) -> Self {
140 Self {
141 in_flight: self.in_flight.clone(),
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use std::sync::atomic::{
149 AtomicUsize,
150 Ordering,
151 };
152
153 use super::*;
154
155 #[tokio::test]
156 async fn test_request_deduplication() {
157 let dedup = RequestDeduplicator::<i32, ()>::new();
158 let call_count = Arc::new(AtomicUsize::new(0));
159
160 let request_id = hash_request(1, Generation::next(), "test");
161
162 let dedup1 = dedup.clone();
163 let count1 = call_count.clone();
164 let handle1 = tokio::spawn(async move {
165 dedup1
166 .deduplicate(request_id, || async {
167 count1.fetch_add(1, Ordering::SeqCst);
168 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
169 Ok::<_, ()>(42)
170 })
171 .await
172 });
173
174 let dedup2 = dedup.clone();
175 let count2 = call_count.clone();
176 let handle2 = tokio::spawn(async move {
177 dedup2
178 .deduplicate(request_id, || async {
179 count2.fetch_add(1, Ordering::SeqCst);
180 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
181 Ok::<_, ()>(42)
182 })
183 .await
184 });
185
186 let (r1, r2) = tokio::join!(handle1, handle2);
187 assert_eq!(r1.unwrap().unwrap(), 42);
188 assert_eq!(r2.unwrap().unwrap(), 42);
189
190 assert_eq!(call_count.load(Ordering::SeqCst), 1);
191 }
192
193 #[tokio::test]
194 async fn failed_request_does_not_block_the_next_one() {
195 let dedup = RequestDeduplicator::<i32, &str>::new();
196 let request_id = hash_request(1, Generation::next(), "fetch_runs_x_100");
197
198 let first = dedup
199 .deduplicate(request_id, || async { Err::<i32, &str>("provider error") })
200 .await;
201 assert_eq!(first, Err("provider error"));
202
203 let second = tokio::time::timeout(
204 tokio::time::Duration::from_secs(2),
205 dedup.deduplicate(request_id, || async { Ok::<_, &str>(7) }),
206 )
207 .await;
208 assert_eq!(second, Ok(Ok(7)));
209 }
210
211 #[tokio::test]
212 async fn dropped_request_does_not_block_the_next_one() {
213 let dedup = RequestDeduplicator::<i32, &str>::new();
214 let request_id = hash_request(1, Generation::next(), "fetch_runs_x_100");
215
216 let abandoned = tokio::time::timeout(
217 tokio::time::Duration::from_millis(20),
218 dedup.deduplicate(request_id, || async {
219 tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
220 Ok::<_, &str>(1)
221 }),
222 )
223 .await;
224 assert!(abandoned.is_err());
225
226 let next = tokio::time::timeout(
227 tokio::time::Duration::from_secs(2),
228 dedup.deduplicate(request_id, || async { Ok::<_, &str>(2) }),
229 )
230 .await;
231 assert_eq!(next, Ok(Ok(2)));
232 }
233
234 #[tokio::test]
235 async fn failure_reaches_every_waiter_and_runs_once() {
236 let dedup = RequestDeduplicator::<i32, &str>::new();
237 let request_id = hash_request(1, Generation::next(), "fetch_runs_x_100");
238 let calls = Arc::new(AtomicUsize::new(0));
239
240 let leader = dedup.clone();
241 let leader_calls = calls.clone();
242 let first = tokio::spawn(async move {
243 leader
244 .deduplicate(request_id, || async move {
245 leader_calls.fetch_add(1, Ordering::SeqCst);
246 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
247 Err::<i32, &str>("provider error")
248 })
249 .await
250 });
251 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
252
253 let waiter_calls = calls.clone();
254 let waiter = tokio::time::timeout(
255 tokio::time::Duration::from_secs(2),
256 dedup.deduplicate(request_id, || async move {
257 waiter_calls.fetch_add(1, Ordering::SeqCst);
258 Ok::<_, &str>(3)
259 }),
260 )
261 .await;
262
263 assert_eq!(first.await.unwrap(), Err("provider error"));
264 assert_eq!(waiter, Ok(Err("provider error")));
265 assert_eq!(calls.load(Ordering::SeqCst), 1);
266 }
267
268 #[test]
269 fn test_hash_request() {
270 let generation = Generation::next();
271 let h1 = hash_request(1, generation, "pipelines");
272 let h2 = hash_request(1, generation, "pipelines");
273 let h3 = hash_request(2, generation, "pipelines");
274 let h4 = hash_request(1, Generation::next(), "pipelines");
275
276 assert_eq!(h1, h2);
277 assert_ne!(h1, h3);
278 assert_ne!(h1, h4);
279 }
280
281 #[tokio::test]
282 async fn different_generations_are_not_shared() {
283 let dedup = RequestDeduplicator::<i32, &str>::new();
284 let calls = Arc::new(AtomicUsize::new(0));
285 let old = hash_request(1, Generation::next(), "fetch_runs_x_100");
286 let new = hash_request(1, Generation::next(), "fetch_runs_x_100");
287 let run = |request_id, value| {
288 let dedup = dedup.clone();
289 let calls = calls.clone();
290 async move {
291 dedup
292 .deduplicate(request_id, || async move {
293 calls.fetch_add(1, Ordering::SeqCst);
294 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
295 Ok::<_, &str>(value)
296 })
297 .await
298 }
299 };
300
301 let (first, second) = tokio::join!(run(old, 1), run(new, 2));
302
303 assert_eq!(first, Ok(1));
304 assert_eq!(second, Ok(2));
305 assert_eq!(calls.load(Ordering::SeqCst), 2);
306 }
307
308 #[test]
309 fn test_hash_pipeline_run() {
310 let h1 = hash_pipeline_run(
311 1,
312 "success",
313 Some("main"),
314 "2024-01-01T00:00:00Z",
315 Some(60),
316 None,
317 );
318 let h2 = hash_pipeline_run(
319 1,
320 "success",
321 Some("main"),
322 "2024-01-01T00:00:00Z",
323 Some(60),
324 None,
325 );
326 let h3 = hash_pipeline_run(
327 1,
328 "failed",
329 Some("main"),
330 "2024-01-01T00:00:00Z",
331 Some(60),
332 None,
333 );
334
335 assert_eq!(h1, h2);
336 assert_ne!(h1, h3);
337 }
338}