use std::panic::UnwindSafe; use expect_test::expect; use salsa::{Durability, ParallelDatabase, Snapshot}; // Axes: // // Threading // * Intra-thread // * Cross-thread -- part of cycle is on one thread, part on another // // Recovery strategies: // * Panic // * Fallback // * Mixed -- multiple strategies within cycle participants // // Across revisions: // * N/A -- only one revision // * Present in new revision, not old // * Present in old revision, not new // * Present in both revisions // // Dependencies // * Tracked // * Untracked -- cycle participant(s) contain untracked reads // // Layers // * Direct -- cycle participant is directly invoked from test // * Indirect -- invoked a query that invokes the cycle // // // | Thread | Recovery | Old, New | Dep style | Layers | Test Name | // | ------ | -------- | -------- | --------- | ------ | --------- | // | Intra | Panic | N/A | Tracked | direct | cycle_memoized | // | Intra | Panic | N/A | Untracked | direct | cycle_volatile | // | Intra | Fallback | N/A | Tracked | direct | cycle_cycle | // | Intra | Fallback | N/A | Tracked | indirect | inner_cycle | // | Intra | Fallback | Both | Tracked | direct | cycle_revalidate | // | Intra | Fallback | New | Tracked | direct | cycle_appears | // | Intra | Fallback | Old | Tracked | direct | cycle_disappears | // | Intra | Fallback | Old | Tracked | direct | cycle_disappears_durability | // | Intra | Mixed | N/A | Tracked | direct | cycle_mixed_1 | // | Intra | Mixed | N/A | Tracked | direct | cycle_mixed_2 | // | Cross | Fallback | N/A | Tracked | both | parallel/cycles.rs: recover_parallel_cycle | // | Cross | Panic | N/A | Tracked | both | parallel/cycles.rs: panic_parallel_cycle | #[derive(PartialEq, Eq, Hash, Clone, Debug)] struct Error { cycle: Vec, } #[salsa::database(GroupStruct)] #[derive(Default)] struct DatabaseImpl { storage: salsa::Storage, } impl salsa::Database for DatabaseImpl {} impl ParallelDatabase for DatabaseImpl { fn snapshot(&self) -> Snapshot { Snapshot::new(DatabaseImpl { storage: self.storage.snapshot() }) } } /// The queries A, B, and C in `Database` can be configured /// to invoke one another in arbitrary ways using this /// enum. #[derive(Debug, Copy, Clone, PartialEq, Eq)] enum CycleQuery { None, A, B, C, AthenC, } #[salsa::query_group(GroupStruct)] trait Database: salsa::Database { // `a` and `b` depend on each other and form a cycle fn memoized_a(&self) -> (); fn memoized_b(&self) -> (); fn volatile_a(&self) -> (); fn volatile_b(&self) -> (); #[salsa::input] fn a_invokes(&self) -> CycleQuery; #[salsa::input] fn b_invokes(&self) -> CycleQuery; #[salsa::input] fn c_invokes(&self) -> CycleQuery; #[salsa::cycle(recover_a)] fn cycle_a(&self) -> Result<(), Error>; #[salsa::cycle(recover_b)] fn cycle_b(&self) -> Result<(), Error>; fn cycle_c(&self) -> Result<(), Error>; } fn recover_a(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> { Err(Error { cycle: cycle.all_participants(db) }) } fn recover_b(db: &dyn Database, cycle: &salsa::Cycle) -> Result<(), Error> { Err(Error { cycle: cycle.all_participants(db) }) } fn memoized_a(db: &dyn Database) { db.memoized_b() } fn memoized_b(db: &dyn Database) { db.memoized_a() } fn volatile_a(db: &dyn Database) { db.salsa_runtime().report_untracked_read(); db.volatile_b() } fn volatile_b(db: &dyn Database) { db.salsa_runtime().report_untracked_read(); db.volatile_a() } impl CycleQuery { fn invoke(self, db: &dyn Database) -> Result<(), Error> { match self { CycleQuery::A => db.cycle_a(), CycleQuery::B => db.cycle_b(), CycleQuery::C => db.cycle_c(), CycleQuery::AthenC => { let _ = db.cycle_a(); db.cycle_c() } CycleQuery::None => Ok(()), } } } fn cycle_a(db: &dyn Database) -> Result<(), Error> { db.a_invokes().invoke(db) } fn cycle_b(db: &dyn Database) -> Result<(), Error> { db.b_invokes().invoke(db) } fn cycle_c(db: &dyn Database) -> Result<(), Error> { db.c_invokes().invoke(db) } #[track_caller] fn extract_cycle(f: impl FnOnce() + UnwindSafe) -> salsa::Cycle { let v = std::panic::catch_unwind(f); if let Err(d) = &v { if let Some(cycle) = d.downcast_ref::() { return cycle.clone(); } } panic!("unexpected value: {v:?}") } #[test] fn cycle_memoized() { let db = DatabaseImpl::default(); let cycle = extract_cycle(|| db.memoized_a()); expect![[r#" [ "cycles::MemoizedAQuery::memoized_a(())", "cycles::MemoizedBQuery::memoized_b(())", ] "#]] .assert_debug_eq(&cycle.unexpected_participants(&db)); } #[test] fn cycle_volatile() { let db = DatabaseImpl::default(); let cycle = extract_cycle(|| db.volatile_a()); expect![[r#" [ "cycles::VolatileAQuery::volatile_a(())", "cycles::VolatileBQuery::volatile_b(())", ] "#]] .assert_debug_eq(&cycle.unexpected_participants(&db)); } #[test] fn cycle_cycle() { let mut query = DatabaseImpl::default(); // A --> B // ^ | // +-----+ query.set_a_invokes(CycleQuery::B); query.set_b_invokes(CycleQuery::A); assert!(query.cycle_a().is_err()); } #[test] fn inner_cycle() { let mut query = DatabaseImpl::default(); // A --> B <-- C // ^ | // +-----+ query.set_a_invokes(CycleQuery::B); query.set_b_invokes(CycleQuery::A); query.set_c_invokes(CycleQuery::B); let err = query.cycle_c(); assert!(err.is_err()); let cycle = err.unwrap_err().cycle; expect![[r#" [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ] "#]] .assert_debug_eq(&cycle); } #[test] fn cycle_revalidate() { let mut db = DatabaseImpl::default(); // A --> B // ^ | // +-----+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::A); assert!(db.cycle_a().is_err()); db.set_b_invokes(CycleQuery::A); // same value as default assert!(db.cycle_a().is_err()); } #[test] fn cycle_revalidate_unchanged_twice() { let mut db = DatabaseImpl::default(); // A --> B // ^ | // +-----+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::A); assert!(db.cycle_a().is_err()); db.set_c_invokes(CycleQuery::A); // force new revisi5on // on this run expect![[r#" Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ) "#]] .assert_debug_eq(&db.cycle_a()); } #[test] fn cycle_appears() { let mut db = DatabaseImpl::default(); // A --> B db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::None); assert!(db.cycle_a().is_ok()); // A --> B // ^ | // +-----+ db.set_b_invokes(CycleQuery::A); tracing::debug!("Set Cycle Leaf"); assert!(db.cycle_a().is_err()); } #[test] fn cycle_disappears() { let mut db = DatabaseImpl::default(); // A --> B // ^ | // +-----+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::A); assert!(db.cycle_a().is_err()); // A --> B db.set_b_invokes(CycleQuery::None); assert!(db.cycle_a().is_ok()); } /// A variant on `cycle_disappears` in which the values of /// `a_invokes` and `b_invokes` are set with durability values. /// If we are not careful, this could cause us to overlook /// the fact that the cycle will no longer occur. #[test] fn cycle_disappears_durability() { let mut db = DatabaseImpl::default(); db.set_a_invokes_with_durability(CycleQuery::B, Durability::LOW); db.set_b_invokes_with_durability(CycleQuery::A, Durability::HIGH); let res = db.cycle_a(); assert!(res.is_err()); // At this point, `a` read `LOW` input, and `b` read `HIGH` input. However, // because `b` participates in the same cycle as `a`, its final durability // should be `LOW`. // // Check that setting a `LOW` input causes us to re-execute `b` query, and // observe that the cycle goes away. db.set_a_invokes_with_durability(CycleQuery::None, Durability::LOW); let res = db.cycle_b(); assert!(res.is_ok()); } #[test] fn cycle_mixed_1() { let mut db = DatabaseImpl::default(); // A --> B <-- C // | ^ // +-----+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::C); db.set_c_invokes(CycleQuery::B); let u = db.cycle_c(); expect![[r#" Err( Error { cycle: [ "cycles::CycleBQuery::cycle_b(())", "cycles::CycleCQuery::cycle_c(())", ], }, ) "#]] .assert_debug_eq(&u); } #[test] fn cycle_mixed_2() { let mut db = DatabaseImpl::default(); // Configuration: // // A --> B --> C // ^ | // +-----------+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::C); db.set_c_invokes(CycleQuery::A); let u = db.cycle_a(); expect![[r#" Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", "cycles::CycleCQuery::cycle_c(())", ], }, ) "#]] .assert_debug_eq(&u); } #[test] fn cycle_deterministic_order() { // No matter whether we start from A or B, we get the same set of participants: let db = || { let mut db = DatabaseImpl::default(); // A --> B // ^ | // +-----+ db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::A); db }; let a = db().cycle_a(); let b = db().cycle_b(); expect![[r#" ( Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ), Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ), ) "#]] .assert_debug_eq(&(a, b)); } #[test] fn cycle_multiple() { // No matter whether we start from A or B, we get the same set of participants: let mut db = DatabaseImpl::default(); // Configuration: // // A --> B <-- C // ^ | ^ // +-----+ | // | | // +-----+ // // Here, conceptually, B encounters a cycle with A and then // recovers. db.set_a_invokes(CycleQuery::B); db.set_b_invokes(CycleQuery::AthenC); db.set_c_invokes(CycleQuery::B); let c = db.cycle_c(); let b = db.cycle_b(); let a = db.cycle_a(); expect![[r#" ( Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ), Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ), Err( Error { cycle: [ "cycles::CycleAQuery::cycle_a(())", "cycles::CycleBQuery::cycle_b(())", ], }, ), ) "#]] .assert_debug_eq(&(a, b, c)); } #[test] fn cycle_recovery_set_but_not_participating() { let mut db = DatabaseImpl::default(); // A --> C -+ // ^ | // +--+ db.set_a_invokes(CycleQuery::C); db.set_c_invokes(CycleQuery::C); // Here we expect C to panic and A not to recover: let r = extract_cycle(|| drop(db.cycle_a())); expect![[r#" [ "cycles::CycleCQuery::cycle_c(())", ] "#]] .assert_debug_eq(&r.all_participants(&db)); }