keepsake_sqlx/repository/
cache.rs1use keepsake::{RelationDefinition, RelationId, RelationKey};
2
3#[cfg(feature = "cache")]
4use std::collections::BTreeMap;
5use std::fmt::Debug;
6#[cfg(feature = "cache")]
7use std::sync::{Arc, RwLock};
8#[cfg(feature = "cache")]
9use std::time::{Duration, Instant};
10
11#[async_trait::async_trait]
13pub trait RelationCache: Send + Sync + Debug {
14 async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition>;
16
17 async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition>;
19
20 async fn store(&self, relation: &RelationDefinition);
22
23 async fn remove_by_id(&self, relation_id: RelationId);
25}
26
27#[derive(Debug, Clone, Copy, Default)]
29pub struct NoopRelationCache;
30
31#[async_trait::async_trait]
32impl RelationCache for NoopRelationCache {
33 async fn get_by_id(&self, _relation_id: RelationId) -> Option<RelationDefinition> {
34 None
35 }
36
37 async fn get_by_key(&self, _key: &RelationKey) -> Option<RelationDefinition> {
38 None
39 }
40
41 async fn store(&self, _relation: &RelationDefinition) {}
42
43 async fn remove_by_id(&self, _relation_id: RelationId) {}
44}
45
46#[cfg(feature = "cache")]
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub struct LocalRelationCacheConfig {
50 pub ttl: Duration,
52}
53
54#[cfg(feature = "cache")]
55impl LocalRelationCacheConfig {
56 #[must_use]
58 pub const fn new(ttl: Duration) -> Self {
59 Self { ttl }
60 }
61}
62
63#[cfg(feature = "cache")]
65#[derive(Debug, Clone)]
66pub struct LocalRelationCache {
67 config: LocalRelationCacheConfig,
68 state: Arc<RwLock<LocalRelationCacheState>>,
72}
73
74#[cfg(feature = "cache")]
75impl LocalRelationCache {
76 #[must_use]
78 pub fn new(config: LocalRelationCacheConfig) -> Self {
79 Self {
80 config,
81 state: Arc::new(RwLock::new(LocalRelationCacheState::default())),
82 }
83 }
84}
85
86#[cfg(feature = "cache")]
87#[async_trait::async_trait]
88impl RelationCache for LocalRelationCache {
89 async fn get_by_id(&self, relation_id: RelationId) -> Option<RelationDefinition> {
90 self.state
91 .read()
92 .ok()
93 .and_then(|state| state.by_id.get(&relation_id).cloned())
94 .and_then(CacheEntry::fresh_relation)
95 }
96
97 async fn get_by_key(&self, key: &RelationKey) -> Option<RelationDefinition> {
98 self.state
99 .read()
100 .ok()
101 .and_then(|state| state.by_key.get(key).cloned())
102 .and_then(CacheEntry::fresh_relation)
103 }
104
105 async fn store(&self, relation: &RelationDefinition) {
106 let entry = CacheEntry {
107 relation: relation.clone(),
108 expires_at: Instant::now() + self.config.ttl,
109 };
110 if let Ok(mut state) = self.state.write() {
111 state.by_id.insert(relation.id, entry.clone());
112 state.by_key.insert(relation.key.clone(), entry);
113 }
114 }
115
116 async fn remove_by_id(&self, relation_id: RelationId) {
117 if let Ok(mut state) = self.state.write()
118 && let Some(entry) = state.by_id.remove(&relation_id)
119 {
120 state.by_key.remove(&entry.relation.key);
121 }
122 }
123}
124
125#[cfg(feature = "cache")]
126#[derive(Debug, Default)]
127struct LocalRelationCacheState {
128 by_id: BTreeMap<RelationId, CacheEntry>,
129 by_key: BTreeMap<RelationKey, CacheEntry>,
130}
131
132#[cfg(feature = "cache")]
133#[derive(Debug, Clone)]
134struct CacheEntry {
135 relation: RelationDefinition,
136 expires_at: Instant,
137}
138
139#[cfg(feature = "cache")]
140impl CacheEntry {
141 fn fresh_relation(self) -> Option<RelationDefinition> {
142 (Instant::now() <= self.expires_at).then_some(self.relation)
143 }
144}
145
146#[cfg(all(test, feature = "cache"))]
147mod tests {
148 use super::*;
149 use keepsake::ExpiryPolicy;
150 use std::thread;
151 use uuid::Uuid;
152
153 #[test]
154 fn cache_entry_expires_after_ttl() -> keepsake::Result<()> {
155 let relation = RelationDefinition::enabled(
156 Uuid::nil(),
157 RelationKey::new("tag", "trusted")?,
158 ExpiryPolicy::ManualOnly,
159 )?;
160 let entry = CacheEntry {
161 relation,
162 expires_at: Instant::now() + Duration::from_millis(1),
163 };
164
165 thread::sleep(Duration::from_millis(5));
166
167 assert_eq!(entry.fresh_relation(), None);
168 Ok(())
169 }
170}