1use crate::journal::Journal;
2use crate::{AdapterApplyError, AppliedOp, Op};
3use alembic_core::Uid;
4use anyhow::{anyhow, Result};
5use async_trait::async_trait;
6use serde_json::Value;
7use std::collections::{BTreeMap, BTreeSet};
8
9#[derive(Debug)]
10pub struct RetryApplyResult {
11 pub applied: Vec<AppliedOp>,
12 pub pending: Vec<Op>,
13}
14
15#[async_trait]
16pub trait RetryApplyDriver {
17 async fn apply_non_delete(&mut self, op: &Op) -> Result<AppliedOp>;
18 fn is_retryable(&self, err: &anyhow::Error) -> bool;
19}
20
21pub async fn apply_non_delete_with_retries(
22 ops: &[Op],
23 mut journal: Option<&mut Journal>,
24 driver: &mut impl RetryApplyDriver,
25) -> Result<RetryApplyResult> {
26 let mut applied = Vec::new();
27 let mut pending: Vec<Op> = ops
28 .iter()
29 .filter(|op| !matches!(op, Op::Delete { .. }))
30 .cloned()
31 .collect();
32
33 if let Some(journal) = journal.as_mut() {
34 let done_ops = journal.done_ops();
35 let done_ops_len = done_ops.len();
36
37 let mut done = done_ops
38 .into_iter()
39 .collect::<std::collections::HashSet<_>>();
40
41 if done.len() != done_ops_len {
42 return Err(anyhow!("journal contained duplicated ops (same uid, typename and hash) which is not supported"));
47 }
48
49 pending.retain(|op| !done.remove(&(op.uid(), op.type_name().clone(), op.hashed())));
50
51 if !done.is_empty() {
52 return Err(anyhow!(
53 "journal contains done ops that are not present in the provided ops"
54 ));
55 }
56 }
57
58 while !pending.is_empty() {
59 let current = std::mem::take(&mut pending);
60 let applied_before = applied.len();
61
62 for op in current {
63 match driver.apply_non_delete(&op).await {
64 Ok(applied_op) => {
65 if let Some(journal) = journal.as_mut() {
68 journal.mark_op_as_done(&op)?;
69 }
70 applied.push(applied_op);
71 }
72 Err(err) if driver.is_retryable(&err) => pending.push(op),
73 Err(err) => {
74 if let Some(journal) = journal.as_mut() {
78 if let Err(save_err) = journal.save() {
79 tracing::warn!(
80 error = %save_err,
81 "failed to persist journal after apply error"
82 );
83 }
84 }
85 return Err(err);
86 }
87 }
88 }
89
90 if applied.len() == applied_before {
92 break;
93 }
94 }
95
96 if let Some(journal) = journal.as_mut() {
97 if journal.is_completed() {
98 journal.delete_backing_file()?;
99 } else {
100 journal.save()?;
102 }
103 }
104
105 Ok(RetryApplyResult { applied, pending })
106}
107
108pub async fn apply_non_delete_journaled(
112 state: &crate::StateStore,
113 adapter_name: &str,
114 creates_updates: &[Op],
115 driver: &mut impl RetryApplyDriver,
116) -> Result<(RetryApplyResult, Option<usize>)> {
117 let mut journal = match state.journal_dir() {
118 Some(dir) => Some(Journal::load_or_create(dir, adapter_name, creates_updates)?),
119 None => None,
120 };
121 let previously_applied = journal.as_ref().map(|j| j.done_ops_count()).unwrap_or(0);
122 let result = apply_non_delete_with_retries(creates_updates, journal.as_mut(), driver).await?;
123 Ok((
124 result,
125 (previously_applied > 0).then_some(previously_applied),
126 ))
127}
128
129pub fn is_missing_ref_error(err: &anyhow::Error) -> bool {
131 err.downcast_ref::<AdapterApplyError>()
132 .is_some_and(|e| matches!(e, AdapterApplyError::MissingRef { .. }))
133}
134
135pub fn describe_missing_refs<V>(ops: &[Op], resolved: &BTreeMap<Uid, V>) -> String {
137 let mut missing = BTreeSet::new();
138 for op in ops {
139 if let Op::Create { desired, .. } | Op::Update { desired, .. } = op {
140 for value in desired.attrs.values() {
141 collect_missing_refs(value, resolved, &mut missing);
142 }
143 }
144 }
145 missing
146 .into_iter()
147 .map(|uid| uid.to_string())
148 .collect::<Vec<_>>()
149 .join(", ")
150}
151
152fn collect_missing_refs<V>(
153 value: &Value,
154 resolved: &BTreeMap<Uid, V>,
155 missing: &mut BTreeSet<Uid>,
156) {
157 match value {
158 Value::String(raw) => {
159 if let Ok(uid) = Uid::parse_str(raw) {
160 if !resolved.contains_key(&uid) {
161 missing.insert(uid);
162 }
163 }
164 }
165 Value::Array(items) => {
166 for item in items {
167 collect_missing_refs(item, resolved, missing);
168 }
169 }
170 Value::Object(map) => {
171 for value in map.values() {
172 collect_missing_refs(value, resolved, missing);
173 }
174 }
175 _ => {}
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::BackendId;
183 use alembic_core::{JsonMap, Key, Object, TypeName, Uid};
184 use anyhow::anyhow;
185 use rand::rng;
186 use rand::seq::SliceRandom;
187 use serde_json::json;
188 use tempfile::tempdir;
189
190 #[test]
191 fn is_missing_ref_error_matches_only_missing_ref() {
192 let err = anyhow::Error::from(AdapterApplyError::MissingRef {
193 uid: Uid::from_u128(1),
194 });
195 assert!(is_missing_ref_error(&err));
196 assert!(!is_missing_ref_error(&anyhow!("some other error")));
197 }
198
199 #[test]
200 fn describe_missing_refs_reports_unresolved_nested_refs() {
201 let present = Uid::from_u128(1);
202 let missing = Uid::from_u128(2);
203
204 let attrs = JsonMap::from(BTreeMap::from([
205 ("resolved_ref".to_string(), json!(present.to_string())),
206 (
207 "nested".to_string(),
208 json!({ "list": [missing.to_string()] }),
209 ),
210 ]));
211 let op = Op::Create {
212 uid: present,
213 type_name: TypeName::new("test.item"),
214 desired: Object {
215 uid: present,
216 type_name: TypeName::new("test.item"),
217 key: Key::default(),
218 attrs,
219 source: None,
220 },
221 };
222
223 let mut resolved = BTreeMap::new();
224 resolved.insert(present, BackendId::Int(1));
225
226 let described = describe_missing_refs(&[op], &resolved);
227 assert!(described.contains(&missing.to_string()));
228 assert!(!described.contains(&present.to_string()));
229 }
230
231 fn create_op(uid: Uid) -> Op {
232 Op::Create {
233 uid,
234 type_name: TypeName::new("test.item"),
235 desired: Object {
236 uid,
237 type_name: TypeName::new("test.item"),
238 key: Key::default(),
239 attrs: JsonMap::default(),
240 source: None,
241 },
242 }
243 }
244
245 #[derive(Clone, Copy)]
246 enum Mode {
247 RetryThenOk,
248 AlwaysRetry,
249 Fatal,
250 }
251
252 struct TestDriver {
253 attempts: usize,
254 mode: Mode,
255 }
256
257 #[async_trait]
258 impl RetryApplyDriver for TestDriver {
259 async fn apply_non_delete(&mut self, op: &Op) -> Result<AppliedOp> {
260 self.attempts += 1;
261 match self.mode {
262 Mode::RetryThenOk if self.attempts == 1 => {
263 Err(anyhow!("missing referenced uid {}", op.uid()))
264 }
265 Mode::AlwaysRetry => Err(anyhow!("missing referenced uid {}", op.uid())),
266 Mode::Fatal => Err(anyhow!("boom")),
267 Mode::RetryThenOk => Ok(AppliedOp {
268 uid: op.uid(),
269 type_name: op.type_name().clone(),
270 backend_id: Some(BackendId::Int(1)),
271 }),
272 }
273 }
274
275 fn is_retryable(&self, err: &anyhow::Error) -> bool {
276 err.to_string().contains("missing referenced uid")
277 }
278 }
279
280 #[tokio::test]
281 async fn retries_then_applies() {
282 let uid1 = Uid::from_u128(1);
283 let uid2 = Uid::from_u128(2);
284 let ops = vec![create_op(uid1), create_op(uid2)];
285 let mut driver = TestDriver {
286 attempts: 0,
287 mode: Mode::RetryThenOk,
288 };
289
290 let result = apply_non_delete_with_retries(&ops, None, &mut driver)
291 .await
292 .unwrap();
293
294 assert_eq!(driver.attempts, 3);
295 assert_eq!(result.applied.len(), 2);
296 assert!(result.pending.is_empty());
297 }
298
299 #[tokio::test]
300 async fn returns_pending_when_stuck() {
301 let uid = Uid::from_u128(1);
302 let ops = vec![create_op(uid)];
303 let mut driver = TestDriver {
304 attempts: 0,
305 mode: Mode::AlwaysRetry,
306 };
307
308 let result = apply_non_delete_with_retries(&ops, None, &mut driver)
309 .await
310 .unwrap();
311
312 assert!(result.applied.is_empty());
313 assert_eq!(result.pending.len(), 1);
314 }
315
316 #[tokio::test]
317 async fn returns_non_retryable_error() {
318 let uid = Uid::from_u128(1);
319 let ops = vec![create_op(uid)];
320 let mut driver = TestDriver {
321 attempts: 0,
322 mode: Mode::Fatal,
323 };
324
325 let err = apply_non_delete_with_retries(&ops, None, &mut driver)
326 .await
327 .unwrap_err();
328
329 assert!(err.to_string().contains("boom"));
330 }
331
332 #[tokio::test]
333 async fn ignores_delete_ops() {
334 let uid = Uid::from_u128(1);
335 let ops = vec![Op::Delete {
336 uid,
337 type_name: TypeName::new("test.item"),
338 key: Key::default(),
339 backend_id: None,
340 }];
341 let mut driver = TestDriver {
342 attempts: 0,
343 mode: Mode::Fatal,
344 };
345
346 let result = apply_non_delete_with_retries(&ops, None, &mut driver)
347 .await
348 .unwrap();
349
350 assert_eq!(driver.attempts, 0);
351 assert!(result.pending.is_empty());
352 assert!(result.applied.is_empty());
353 }
354
355 struct ErraticDriver {
356 countdown_to_crash: u32,
357 applied_ops: Vec<AppliedOp>,
358 }
359
360 #[async_trait]
361 impl RetryApplyDriver for ErraticDriver {
362 async fn apply_non_delete(&mut self, op: &Op) -> Result<AppliedOp> {
363 self.countdown_to_crash -= 1;
364
365 if self.countdown_to_crash == 0 {
366 return Err(anyhow!("planned error"));
367 }
368
369 let applied_op = AppliedOp {
370 uid: op.uid(),
371 type_name: op.type_name().clone(),
372 backend_id: None,
373 };
374 self.applied_ops.push(applied_op.clone());
375
376 Ok(applied_op)
377 }
378
379 fn is_retryable(&self, _err: &anyhow::Error) -> bool {
380 false
381 }
382 }
383 #[tokio::test]
384 async fn erratic_driver_first_fails_then_succeeds() {
385 let uid1 = Uid::from_u128(1);
386 let uid2 = Uid::from_u128(2);
387 let ops = vec![create_op(uid1), create_op(uid2)];
388 let mut driver = ErraticDriver {
389 countdown_to_crash: 2,
390 applied_ops: vec![],
391 };
392 let dir = tempdir().unwrap();
393 let mut journal = Journal::load_or_create(dir.path(), "erratic_driver", &ops).unwrap();
394
395 apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
396 .await
397 .expect_err("should fail (on second op applied this run)");
398 assert_eq!(driver.applied_ops.len(), 1);
399 assert!(!journal.is_completed());
400
401 driver.countdown_to_crash = 99999;
403 _ = apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
404 .await
405 .unwrap();
406 assert_eq!(
407 driver.applied_ops.iter().map(|a| a.uid).collect::<Vec<_>>(),
408 vec![uid1, uid2]
409 );
410 assert!(journal.is_completed());
411 }
412
413 #[tokio::test]
414 async fn resumes_from_disk_after_error() {
415 let uid1 = Uid::from_u128(1);
416 let uid2 = Uid::from_u128(2);
417 let uid3 = Uid::from_u128(3);
418 let ops = vec![create_op(uid1), create_op(uid2), create_op(uid3)];
419 let dir = tempdir().unwrap();
420
421 {
424 let mut driver = ErraticDriver {
425 countdown_to_crash: 2,
426 applied_ops: vec![],
427 };
428 let mut journal = Journal::load_or_create(dir.path(), "resume_test", &ops).unwrap();
429 apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
430 .await
431 .expect_err("should fail on the second op");
432 assert_eq!(driver.applied_ops.len(), 1);
433 }
434
435 {
437 let mut driver = ErraticDriver {
438 countdown_to_crash: 99999,
439 applied_ops: vec![],
440 };
441 let mut journal = Journal::load_or_create(dir.path(), "resume_test", &ops).unwrap();
442 let result = apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
443 .await
444 .unwrap();
445 assert_eq!(
446 driver.applied_ops.iter().map(|a| a.uid).collect::<Vec<_>>(),
447 vec![uid2, uid3]
448 );
449 assert_eq!(result.applied.len(), 2);
450 assert!(result.pending.is_empty());
451 assert!(journal.is_completed());
452 }
453 }
454
455 #[tokio::test]
456 async fn erratic_driver_with_shuffled_ops() {
457 let mut ops = Vec::new();
458 for i in 1..10 {
459 ops.push(create_op(Uid::from_u128(i)));
460 }
461
462 let mut rng = rng();
463 ops.shuffle(&mut rng);
464
465 let mut driver = ErraticDriver {
466 countdown_to_crash: 5,
467 applied_ops: vec![],
468 };
469 let dir = tempdir().unwrap();
470 let mut journal = Journal::load_or_create(dir.path(), "erratic_driver", &ops).unwrap();
471
472 apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
473 .await
474 .expect_err("should fail (on fifth op applied this run)");
475 assert_eq!(driver.applied_ops.len(), 4);
476 assert!(!journal.is_completed());
477
478 ops.shuffle(&mut rng);
479
480 driver.countdown_to_crash = 99999;
482 _ = apply_non_delete_with_retries(&ops, Some(&mut journal), &mut driver)
483 .await
484 .unwrap();
485
486 let mut applied_uids = driver.applied_ops.iter().map(|a| a.uid).collect::<Vec<_>>();
487 applied_uids.sort();
488 let mut op_uids = ops.iter().map(|op| op.uid()).collect::<Vec<_>>();
489 op_uids.sort();
490 assert_eq!(applied_uids, op_uids,);
491 assert!(journal.is_completed());
492 }
493
494 async fn run_journaled_apply(
498 state: &crate::StateStore,
499 ops: &[Op],
500 driver: &mut impl RetryApplyDriver,
501 ) -> Result<crate::ApplyReport> {
502 let creates_updates: Vec<Op> = ops
503 .iter()
504 .filter(|op| !matches!(op, Op::Delete { .. }))
505 .cloned()
506 .collect();
507 let (result, previously_applied_count) =
508 apply_non_delete_journaled(state, "test", &creates_updates, driver).await?;
509 Ok(crate::ApplyReport {
510 applied: result.applied,
511 previously_applied_count,
512 ..Default::default()
513 })
514 }
515
516 #[tokio::test]
517 async fn journaled_apply_via_state_store_resumes_and_reports() {
518 let uid1 = Uid::from_u128(1);
519 let uid2 = Uid::from_u128(2);
520 let uid3 = Uid::from_u128(3);
521 let ops = vec![create_op(uid1), create_op(uid2), create_op(uid3)];
522 let dir = tempdir().unwrap();
523 let state = crate::StateStore::new(None, crate::StateData::default())
524 .with_journal_dir(dir.path().to_path_buf());
525 let journal_path = crate::Journal::stable_file_name(dir.path(), "test", &ops);
526
527 {
529 let mut driver = ErraticDriver {
530 countdown_to_crash: 2,
531 applied_ops: vec![],
532 };
533 run_journaled_apply(&state, &ops, &mut driver)
534 .await
535 .expect_err("should crash on the second op");
536 assert_eq!(driver.applied_ops.len(), 1);
537 assert!(journal_path.exists());
538 }
539
540 {
543 let mut driver = ErraticDriver {
544 countdown_to_crash: 99999,
545 applied_ops: vec![],
546 };
547 let report = run_journaled_apply(&state, &ops, &mut driver)
548 .await
549 .unwrap();
550 assert_eq!(
551 driver.applied_ops.iter().map(|a| a.uid).collect::<Vec<_>>(),
552 vec![uid2, uid3]
553 );
554 assert_eq!(report.applied.len(), 2);
555 assert_eq!(report.previously_applied_count, Some(1));
556 assert!(!journal_path.exists());
557 }
558 }
559
560 #[tokio::test]
561 async fn journaled_apply_without_journal_dir_reports_no_resume() {
562 let ops = vec![create_op(Uid::from_u128(1))];
563 let state = crate::StateStore::new(None, crate::StateData::default());
564 let mut driver = ErraticDriver {
565 countdown_to_crash: 99999,
566 applied_ops: vec![],
567 };
568 let report = run_journaled_apply(&state, &ops, &mut driver)
569 .await
570 .unwrap();
571 assert_eq!(report.applied.len(), 1);
572 assert_eq!(report.previously_applied_count, None);
573 }
574}