Skip to main content

hydracache_db/
invalidation.rs

1use std::collections::BTreeSet;
2
3use hydracache::HydraCache;
4use hydracache_core::CacheCodec;
5
6use crate::CacheEntity;
7
8/// A database-neutral list of cache invalidations staged by repository code.
9///
10/// `InvalidationPlan` deliberately does not know about SQLx, Diesel, SeaORM, or
11/// any transaction type. Build it while preparing a write, execute the database
12/// transaction in the ORM/client you already use, and call [`execute`] only
13/// after commit succeeds. Dropping the plan on rollback leaves cached values
14/// untouched.
15///
16/// [`execute`]: InvalidationPlan::execute
17///
18/// # Example
19///
20/// ```rust
21/// use hydracache::HydraCache;
22/// use hydracache_db::{HydraCacheEntity, InvalidationPlan};
23///
24/// #[derive(HydraCacheEntity)]
25/// #[hydracache(entity = "user", collection = "users")]
26/// struct User {
27///     #[hydracache(id)]
28///     id: i64,
29/// }
30///
31/// # async fn example(cache: HydraCache) -> hydracache::CacheResult<()> {
32/// let pending = InvalidationPlan::new().cache_entity::<User>(42);
33///
34/// // tx.update_user(42).await?;
35/// // tx.commit().await?;
36///
37/// let report = pending.execute(&cache).await?;
38/// assert_eq!(report.tag_count, 2);
39/// # Ok(())
40/// # }
41/// ```
42#[derive(Debug, Clone, Default, PartialEq, Eq)]
43pub struct InvalidationPlan {
44    keys: BTreeSet<String>,
45    tags: BTreeSet<String>,
46}
47
48impl InvalidationPlan {
49    /// Create an empty staged invalidation plan.
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Stage one physical cache key for removal.
55    pub fn key(mut self, key: impl Into<String>) -> Self {
56        self.keys.insert(key.into());
57        self
58    }
59
60    /// Stage several physical cache keys for removal.
61    pub fn keys<I, S>(mut self, keys: I) -> Self
62    where
63        I: IntoIterator<Item = S>,
64        S: Into<String>,
65    {
66        self.keys.extend(keys.into_iter().map(Into::into));
67        self
68    }
69
70    /// Stage one invalidation tag.
71    pub fn tag(mut self, tag: impl Into<String>) -> Self {
72        self.tags.insert(tag.into());
73        self
74    }
75
76    /// Stage several invalidation tags.
77    pub fn tags<I, S>(mut self, tags: I) -> Self
78    where
79        I: IntoIterator<Item = S>,
80        S: Into<String>,
81    {
82        self.tags.extend(tags.into_iter().map(Into::into));
83        self
84    }
85
86    /// Stage the entity tag for a [`CacheEntity`] id.
87    pub fn entity<E>(mut self, id: E::Id) -> Self
88    where
89        E: CacheEntity,
90    {
91        self.tags.insert(E::entity_tag_for(&id));
92        self
93    }
94
95    /// Stage the collection tag for a [`CacheEntity`], if it has one.
96    pub fn collection<E>(mut self) -> Self
97    where
98        E: CacheEntity,
99    {
100        if let Some(tag) = E::collection_tag() {
101            self.tags.insert(tag);
102        }
103        self
104    }
105
106    /// Stage both entity and collection tags for a [`CacheEntity`] id.
107    pub fn cache_entity<E>(self, id: E::Id) -> Self
108    where
109        E: CacheEntity,
110    {
111        self.entity::<E>(id).collection::<E>()
112    }
113
114    /// Return true when no key or tag invalidations have been staged.
115    pub fn is_empty(&self) -> bool {
116        self.keys.is_empty() && self.tags.is_empty()
117    }
118
119    /// Number of staged key invalidations after de-duplication.
120    pub fn key_count(&self) -> usize {
121        self.keys.len()
122    }
123
124    /// Number of staged tag invalidations after de-duplication.
125    pub fn tag_count(&self) -> usize {
126        self.tags.len()
127    }
128
129    /// Staged keys in deterministic order.
130    pub fn key_values(&self) -> impl Iterator<Item = &str> {
131        self.keys.iter().map(String::as_str)
132    }
133
134    /// Staged tags in deterministic order.
135    pub fn tag_values(&self) -> impl Iterator<Item = &str> {
136        self.tags.iter().map(String::as_str)
137    }
138
139    /// Execute all staged invalidations against the local cache after commit.
140    pub async fn execute<C>(
141        self,
142        cache: &HydraCache<C>,
143    ) -> hydracache::CacheResult<InvalidationReport>
144    where
145        C: CacheCodec,
146    {
147        let key_count = self.keys.len();
148        let tag_count = self.tags.len();
149        let mut keys_removed = 0;
150        let mut tags_removed = 0;
151
152        for key in self.keys {
153            if cache.remove(&key).await? {
154                keys_removed += 1;
155            }
156        }
157
158        for tag in self.tags {
159            tags_removed += cache.invalidate_tag(&tag).await?;
160        }
161
162        Ok(InvalidationReport {
163            key_count,
164            tag_count,
165            keys_removed,
166            tags_removed,
167        })
168    }
169}
170
171/// Result of executing a staged [`InvalidationPlan`].
172#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
173pub struct InvalidationReport {
174    /// Number of distinct staged keys.
175    pub key_count: usize,
176    /// Number of distinct staged tags.
177    pub tag_count: usize,
178    /// Number of key removals that found an entry.
179    pub keys_removed: u64,
180    /// Number of entries removed by tag invalidations.
181    pub tags_removed: u64,
182}
183
184impl InvalidationReport {
185    /// Total entries removed by key and tag invalidation.
186    pub fn removed_entries(self) -> u64 {
187        self.keys_removed + self.tags_removed
188    }
189}