tests and docs for on-demand input pattern

This commit is contained in:
Aleksey Kladov 2019-09-20 12:25:12 +03:00
parent 60a475202a
commit ec5b92ea20
2 changed files with 161 additions and 0 deletions

View file

@ -1 +1,52 @@
# Common patterns
## On Demnd (Lazy) Inputs
Salsa input quries work best if you can easily provide all of the inputs upfront.
However sometimes the set of inputs is not known beforehand.
A typical example is reading files from disk.
While it is possible to eagarly scan a particular directory and create an in-memory file tree in a salsa input query, a more straight forward approach is to read the files lazily.
That is, when someone requests the text of a file for the first time:
1. Read the file from disk and cache it.
2. Setup a file-system watcher for this path.
3. Innvalidate the cached file once the watcher sends a change notification.
This is possible to achive in salsa, using a derived query and `report_synthetic_read` and `invalidate` queries.
The setup looks roughtly like this:
```rust
#[salsa::query_group(VfsDatabaseStorage)]
trait VfsDatabase: salsa::Database + FileWathcer {
fn read(&self, path: PathBuf) -> String;
}
trait FileWatcher {
fn watch(&self, path: &Path);
fn did_change_file(&self, path: &Path);
}
fn read(db: &impl salsa::Database, path: PathBuf) -> String {
db.salsa_runtime()
.report_synthetic_read(salsa::Durability::LOW);
db.watch(&path);
std::fs::read_to_string(&path).unwrap_or_default()
}
#[salsa::database(VfsDatabaseStorage)]
struct MyDatabase { ... }
impl FileWatcher for MyDatabase {
fn watch(&self, path: &Path) { ... }
fn did_change_file(&self, path: &Path) {
self.query_mut(ReadQuery).invalidate(path);
}
}
```
* We declare the query as a derived query (which is the default).
* In the query implementation, we don't call any other query and just directly read file from disk.
* Because the query doesn't read any inputs, it will be assigned a `HIGH` durability by default, which we override with `report_synthetic_read`.
* The result of the query is cached, and we must call `invalidate` to clear this cache.

110
tests/on_demand_inputs.rs Normal file
View file

@ -0,0 +1,110 @@
//! Test that "on-demand" input pattern works.
//!
//! On-demand inputs are inputs computed lazily on the fly. They are simulated
//! via a b query with zero inputs, which uses `add_synthetic_read` to
//! tweak durability and `invalidate` to clear the input.
use std::{cell::Cell, collections::HashMap, rc::Rc};
use salsa::{Database as _, Durability};
#[salsa::query_group(QueryGroupStorage)]
trait QueryGroup: salsa::Database + AsRef<HashMap<u32, u32>> {
fn a(&self, x: u32) -> u32;
fn b(&self, x: u32) -> u32;
fn c(&self, x: u32) -> u32;
}
fn a(db: &impl QueryGroup, x: u32) -> u32 {
let durability = if x % 2 == 0 {
Durability::LOW
} else {
Durability::HIGH
};
db.salsa_runtime().report_synthetic_read(durability);
let external_state: &HashMap<u32, u32> = db.as_ref();
external_state[&x]
}
fn b(db: &impl QueryGroup, x: u32) -> u32 {
db.a(x)
}
fn c(db: &impl QueryGroup, x: u32) -> u32 {
db.b(x)
}
#[salsa::database(QueryGroupStorage)]
#[derive(Default)]
struct Database {
runtime: salsa::Runtime<Database>,
external_state: HashMap<u32, u32>,
on_event: Option<Box<dyn Fn(salsa::Event<Database>)>>,
}
impl salsa::Database for Database {
fn salsa_runtime(&self) -> &salsa::Runtime<Database> {
&self.runtime
}
fn salsa_event(&self, event_fn: impl Fn() -> salsa::Event<Self>) {
if let Some(cb) = &self.on_event {
cb(event_fn())
}
}
}
impl AsRef<HashMap<u32, u32>> for Database {
fn as_ref(&self) -> &HashMap<u32, u32> {
&self.external_state
}
}
#[test]
fn on_demand_input_works() {
let mut db = Database::default();
db.external_state.insert(1, 10);
assert_eq!(db.b(1), 10);
assert_eq!(db.a(1), 10);
// We changed external state, but haven't signaled about this yet,
// so we expect to see the old answer
db.external_state.insert(1, 92);
assert_eq!(db.b(1), 10);
assert_eq!(db.a(1), 10);
db.query_mut(AQuery).invalidate(&1);
assert_eq!(db.b(1), 92);
assert_eq!(db.a(1), 92);
}
#[test]
fn on_demand_input_durability() {
let mut db = Database::default();
db.external_state.insert(1, 10);
db.external_state.insert(2, 20);
assert_eq!(db.b(1), 10);
assert_eq!(db.b(2), 20);
let validated = Rc::new(Cell::new(0));
db.on_event = Some(Box::new({
let validated = Rc::clone(&validated);
move |event| match event.kind {
salsa::EventKind::DidValidateMemoizedValue { .. } => validated.set(validated.get() + 1),
_ => (),
}
}));
db.salsa_runtime().synthetic_write(Durability::LOW);
validated.set(0);
assert_eq!(db.c(1), 10);
assert_eq!(db.c(2), 20);
assert_eq!(validated.get(), 2);
db.salsa_runtime().synthetic_write(Durability::HIGH);
validated.set(0);
assert_eq!(db.c(1), 10);
assert_eq!(db.c(2), 20);
assert_eq!(validated.get(), 4);
}