salsa/tests/cycles.rs
Markus Westerlind 0e01067d55 feat: Allow queries to avoid panics on cycles
Quickest POC I could create to get some potentially cyclic queries to
not panic and instead return a result I could act on. (gluon's module
importing need to error on cycles).

```
// Causes `db.query()` to actually return `Result<V, CycleError>`
fn query(&self, key: K, key2: K2) -> V;
```

A proper implementation of this would likely return
`Result<V, CycleError<(K, K2)>>` or maybe larger changes are needed.

cc #6
2019-08-16 20:37:54 +02:00

166 lines
3.7 KiB
Rust

use salsa::{ParallelDatabase, Snapshot};
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
struct Error {
cycle: Vec<String>,
}
#[salsa::database(GroupStruct)]
#[derive(Default)]
struct DatabaseImpl {
runtime: salsa::Runtime<DatabaseImpl>,
}
impl salsa::Database for DatabaseImpl {
fn salsa_runtime(&self) -> &salsa::Runtime<DatabaseImpl> {
&self.runtime
}
}
impl ParallelDatabase for DatabaseImpl {
fn snapshot(&self) -> Snapshot<Self> {
Snapshot::new(DatabaseImpl {
runtime: self.runtime.snapshot(self),
})
}
}
#[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) -> ();
fn cycle_leaf(&self) -> ();
#[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: &impl Database, cycle: &[String]) -> Result<(), Error> {
Err(Error {
cycle: cycle.to_owned(),
})
}
fn recover_b(_db: &impl Database, cycle: &[String]) -> Result<(), Error> {
Err(Error {
cycle: cycle.to_owned(),
})
}
fn memoized_a(db: &impl Database) -> () {
db.memoized_b()
}
fn memoized_b(db: &impl Database) -> () {
db.memoized_a()
}
fn volatile_a(db: &impl Database) -> () {
db.salsa_runtime().report_untracked_read();
db.volatile_b()
}
fn volatile_b(db: &impl Database) -> () {
db.salsa_runtime().report_untracked_read();
db.volatile_a()
}
fn cycle_leaf(_db: &impl Database) -> () {}
fn cycle_a(db: &impl Database) -> Result<(), Error> {
let _ = db.cycle_b();
Ok(())
}
fn cycle_b(db: &impl Database) -> Result<(), Error> {
db.cycle_leaf();
let _ = db.cycle_a();
Ok(())
}
fn cycle_c(db: &impl Database) -> Result<(), Error> {
db.cycle_b()
}
#[test]
#[should_panic(expected = "cycle detected")]
fn cycle_memoized() {
let query = DatabaseImpl::default();
query.memoized_a();
}
#[test]
#[should_panic(expected = "cycle detected")]
fn cycle_volatile() {
let query = DatabaseImpl::default();
query.volatile_a();
}
#[test]
fn cycle_cycle() {
let query = DatabaseImpl::default();
assert!(query.cycle_a().is_err());
}
#[test]
fn inner_cycle() {
let query = DatabaseImpl::default();
let err = query.cycle_c();
assert!(err.is_err());
let cycle = err.unwrap_err().cycle;
assert!(
cycle
.iter()
.zip(&["cycle_b", "cycle_a"])
.all(|(l, r)| l.contains(r)),
"{:#?}",
cycle
);
}
#[test]
fn parallel_cycle() {
let _ = env_logger::try_init();
let db = DatabaseImpl::default();
let thread1 = std::thread::spawn({
let db = db.snapshot();
move || {
let result = db.cycle_a();
assert!(result.is_err(), "Expected cycle error");
let cycle = result.unwrap_err().cycle;
assert!(
cycle
.iter()
.all(|l| ["cycle_b", "cycle_a"].iter().any(|r| l.contains(r))),
"{:#?}",
cycle
);
}
});
let thread2 = std::thread::spawn(move || {
let result = db.cycle_c();
assert!(result.is_err(), "Expected cycle error");
let cycle = result.unwrap_err().cycle;
assert!(
cycle
.iter()
.all(|l| ["cycle_b", "cycle_a"].iter().any(|r| l.contains(r))),
"{:#?}",
cycle
);
});
thread1.join().unwrap();
thread2.join().unwrap();
eprintln!("OK");
}