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}