Keeping results across restarts
The first example used a memory database. Switching to disk storage changes the owner’s construction, not the query functions or their callers.
The persistence example
opens a temporary cache, computes a word count, closes it, then opens it again.
Run it with cargo run -p ajika-persistence.
Open, compute, close
extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::time::Duration;
use ajika::{Database, Db, Inputs, Retention};
#[ajika::input(key = String)]
struct File {
#[input(arc)]
text: String,
}
#[ajika::query]
fn words(db: &Db<'_>, path: String) -> ajika::Result<Vec<String>> {
let text = File::at(path).text(db)?;
Ok(text.split_whitespace().map(str::to_owned).collect())
}
#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
Ok(words(db, path)?.len())
}
fn main() -> anyhow::Result<()> {
// A real application chooses a persistent cache directory.
let directory = tempfile::tempdir()?;
let path = directory.path().join("queries");
let limits = Retention {
max_entries: 10_000,
max_bytes: 256 * 1024 * 1024,
max_idle: Duration::from_secs(24 * 60 * 60),
};
let mut database = Database::open(&path, "word-count-v1", Inputs::default(), (), limits)?;
database.update_inputs(|update| {
File::at("notes.txt".into()).set_text(update, "hello world".to_owned())
})?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 2);
drop(database);
// Simulate fresh acquisition by the application, without reusing the old Inputs.
let inputs = Inputs::default()
.edit(|update| File::at("notes.txt".into()).set_text(update, "hello world".to_owned()))?;
let mut database = Database::open(&path, "word-count-v1", inputs, (), limits)?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 2);
database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 0);
assert_eq!(database.statistics().decoded, 1);
Ok(())
}
path names a directory the application has chosen for rebuildable query data.
The example uses a temporary directory so running it leaves no permanent cache.
The operation checkpoints successful work before returning. No per-query save
call is needed, and dropping the owner releases its directory lock.
Only one owner may open that storage directory at a time. Parallel query calls share one owner through an operation; opening another database for each worker is not the concurrency model.
Reopening still needs fresh inputs
extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::time::Duration;
use ajika::{Database, Db, Inputs, Retention};
#[ajika::input(key = String)]
struct File {
#[input(arc)]
text: String,
}
#[ajika::query]
fn words(db: &Db<'_>, path: String) -> ajika::Result<Vec<String>> {
let text = File::at(path).text(db)?;
Ok(text.split_whitespace().map(str::to_owned).collect())
}
#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
Ok(words(db, path)?.len())
}
fn main() -> anyhow::Result<()> {
// A real application chooses a persistent cache directory.
let directory = tempfile::tempdir()?;
let path = directory.path().join("queries");
let limits = Retention {
max_entries: 10_000,
max_bytes: 256 * 1024 * 1024,
max_idle: Duration::from_secs(24 * 60 * 60),
};
let mut database = Database::open(&path, "word-count-v1", Inputs::default(), (), limits)?;
database.update_inputs(|update| {
File::at("notes.txt".into()).set_text(update, "hello world".to_owned())
})?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 2);
drop(database);
// Simulate fresh acquisition by the application, without reusing the old Inputs.
let inputs = Inputs::default()
.edit(|update| File::at("notes.txt".into()).set_text(update, "hello world".to_owned()))?;
let mut database = Database::open(&path, "word-count-v1", inputs, (), limits)?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 2);
database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 0);
assert_eq!(database.statistics().decoded, 1);
Ok(())
}
The cache contains the answer and the observations that justified it. It does not contain the application’s input snapshot. The application supplies fresh inputs on startup, either eagerly or through lazy acquisition.
This new owner has no proof that the stored answer matches its inputs. It follows the count’s dependency on words, checks the words’ saved input stamp, and finds the same text. It can then load the count without executing either function or decoding the words.
Valid does not mean resident
Ask again under the same inputs:
extern crate ajika;
extern crate anyhow;
extern crate tempfile;
use std::time::Duration;
use ajika::{Database, Db, Inputs, Retention};
#[ajika::input(key = String)]
struct File {
#[input(arc)]
text: String,
}
#[ajika::query]
fn words(db: &Db<'_>, path: String) -> ajika::Result<Vec<String>> {
let text = File::at(path).text(db)?;
Ok(text.split_whitespace().map(str::to_owned).collect())
}
#[ajika::query]
fn word_count(db: &Db<'_>, path: String) -> ajika::Result<usize> {
Ok(words(db, path)?.len())
}
fn main() -> anyhow::Result<()> {
// A real application chooses a persistent cache directory.
let directory = tempfile::tempdir()?;
let path = directory.path().join("queries");
let limits = Retention {
max_entries: 10_000,
max_bytes: 256 * 1024 * 1024,
max_idle: Duration::from_secs(24 * 60 * 60),
};
let mut database = Database::open(&path, "word-count-v1", Inputs::default(), (), limits)?;
database.update_inputs(|update| {
File::at("notes.txt".into()).set_text(update, "hello world".to_owned())
})?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 2);
drop(database);
// Simulate fresh acquisition by the application, without reusing the old Inputs.
let inputs = Inputs::default()
.edit(|update| File::at("notes.txt".into()).set_text(update, "hello world".to_owned()))?;
let mut database = Database::open(&path, "word-count-v1", inputs, (), limits)?;
assert_eq!(*database.run(|db| word_count(db, "notes.txt".into()))?, 2);
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 2);
database.run(|db| word_count(db, "notes.txt".into()))?;
assert_eq!(database.statistics().executed, 0);
assert_eq!(database.statistics().validated, 0);
assert_eq!(database.statistics().decoded, 1);
Ok(())
}
No dependency walk is needed: this owner already verified the answer for its current revision. But the previous operation released its decoded working set, so this one decodes the requested value again.
| When a query is requested | Typical work |
|---|---|
| Again within the same operation | Share its resident Arc. |
| In a later operation, with the same inputs | Recover its output description and load the value. |
| After a relevant input change | Validate dependencies and execute where needed. |
| After reopening | Validate against fresh inputs before trusting stored results. |
Validation can check a child’s output version without loading its payload. It does not imply decoding the whole reachable graph.
Values and schema changes
Persistent query values need PartialEq and Ajika’s Persist serialization bound.
For your own value types, deriving wincode::SchemaRead and SchemaWrite normally
supplies Persist. Query keys must also be serializable. All query keys and values
must meet the thread-safety and ownership bounds described in
query boundaries.
The "word-count-v1" argument is an application epoch. Change it when a new
query implementation, key encoding, or result schema makes old stored work unsafe
to reuse. Ajika tracks input changes; it cannot discover that you edited a function’s
Rust code. A different epoch discards the old cache on opening rather than migrating it.
Recovering a cache
A run that encounters a damaged value returns a storage error and leaves repair
pending. The next uncancelled run on the same database attempts repair before
invoking its callback. If repair fails, the caller receives that error.
Maintenance can also discover damage after a successful run, so
database.recovery_pending() reports whether repair is due between runs.
Live repair keeps the installed inputs and interner, as does
database.invalidate() when the host chooses to clear saved work. If
corruption forces Ajika to discard results, it logs a warning and recomputes
them on demand. A new process gets its identity assignments from disk, so it
starts with a fresh interner if the saved identity table is lost.
A checkpoint is not application acceptance
The host should reject incoherent input capture before returning successfully from
run. Ajika can then abandon the operation’s new query results.
The host still owns publication of an editor response or project generation.
Cancellation can arrive after a checkpoint has committed. Stored results
remain associated with the observations they actually used. See
Database::run and checkpointing.