Skip to main content

rustlavel_model_cache/
lib.rs

1//! A second-level cache for models — Hibernate's, in Rustlavel's vocabulary.
2//!
3//! The first level is the query you already ran. This is the one underneath:
4//! entities kept by primary key and query results kept by the SQL that
5//! produced them, shared by every request in the process and, with Redis
6//! behind it, by every process.
7//!
8//! ```ignore
9//! let cache = ModelCache::new(store)
10//!     .region::<User>(Region::new().ttl(Duration::from_secs(300)))
11//!     .region::<Country>(Region::read_only());
12//!
13//! let user = cache.find::<User>(&db, 7).await?;              // by key
14//! let admins = cache.get::<User>(&db, User::query()          // by query
15//!     .filter("role", "admin")).await?;
16//!
17//! cache.update(&db, &user).await?;   // writes, and invalidates what it must
18//! ```
19//!
20//! ## What is hard about this, and how it is handled
21//!
22//! Caching an entity is easy: one key, and the write that changes it knows
23//! which key to drop. **Caching a query result is the hard half**, because a
24//! write to one row invalidates an unknown number of cached result sets and
25//! there is no way to enumerate them — a cached `WHERE role = 'admin'` is
26//! invalidated by an insert that this cache never sees the shape of.
27//!
28//! Hibernate's answer, and the one here: a **generation counter per table**.
29//! Every cached query records the generation it was computed under; every
30//! write bumps it. A read whose recorded generation is not the current one is
31//! a miss, whatever its key. One counter invalidates every stale result set
32//! for that table at once, without knowing anything about them.
33//!
34//! That is deliberately blunt. A write to any row of `users` costs you every
35//! cached `users` query, which is the correct trade: the alternative is
36//! serving a list that is missing a row somebody just added, and a cache that
37//! is occasionally wrong is worse than no cache at all.
38//!
39//! ## What this does not do
40//!
41//! There is **no transactional strategy**. Hibernate's `TRANSACTIONAL` mode
42//! keeps the cache consistent with an in-flight transaction that may still
43//! roll back; this one is updated when the write returns, so a rollback after
44//! a cached write leaves the cache holding a value the database does not have.
45//! Invalidating rather than writing through — which is what [`ModelCache`]
46//! does — makes that window a stale read rather than a wrong one, and it
47//! closes on the next generation bump. If you need better than that, do not
48//! cache that table.
49//!
50//! It also caches **rows, not object graphs**. There is no relation cache and
51//! no lazy-loading proxy, because this ORM has neither.
52
53pub mod cache;
54pub mod keys;
55pub mod region;
56pub mod stats;
57
58pub use cache::ModelCache;
59pub use region::{Region, Strategy};
60pub use stats::{Counts, Stats};
61
62/// What an application file usually needs.
63pub mod prelude {
64    pub use crate::cache::ModelCache;
65    pub use crate::region::{Region, Strategy};
66}