cloudillo_action/
key_cache.rs1use lru::LruCache;
7use std::num::NonZeroUsize;
8use std::sync::Arc;
9
10use crate::prelude::*;
11
12const DEFAULT_CACHE_CAPACITY: usize = 100;
14
15const TTL_NETWORK_ERROR_SECS: i64 = 5 * 60; const TTL_PERSISTENT_ERROR_SECS: i64 = 60 * 60; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum FailureType {
24 NetworkError,
26 NotFound,
28 Unauthorized,
30 ParseError,
32}
33
34impl FailureType {
35 pub fn ttl_secs(&self) -> i64 {
37 match self {
38 FailureType::NetworkError => TTL_NETWORK_ERROR_SECS,
39 FailureType::NotFound => TTL_PERSISTENT_ERROR_SECS,
40 FailureType::Unauthorized => TTL_PERSISTENT_ERROR_SECS,
41 FailureType::ParseError => TTL_PERSISTENT_ERROR_SECS,
42 }
43 }
44
45 pub fn from_error(error: &Error) -> Self {
47 match error {
48 Error::NetworkError(_) => FailureType::NetworkError,
49 Error::NotFound => FailureType::NotFound,
50 Error::PermissionDenied | Error::Unauthorized => FailureType::Unauthorized,
51 Error::Parse => FailureType::ParseError,
52 _ => FailureType::NetworkError,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct FailureEntry {
61 pub failed_at: Timestamp,
63 pub failure_type: FailureType,
65 pub retry_after: Timestamp,
67}
68
69impl FailureEntry {
70 pub fn new(failure_type: FailureType) -> Self {
72 let now = Timestamp::now();
73 let ttl = failure_type.ttl_secs();
74 Self { failed_at: now, failure_type, retry_after: now.add_seconds(ttl) }
75 }
76
77 pub fn is_expired(&self) -> bool {
79 Timestamp::now() >= self.retry_after
80 }
81
82 pub fn seconds_until_retry(&self) -> i64 {
84 let now = Timestamp::now();
85 if now >= self.retry_after {
86 0
87 } else {
88 self.retry_after.0 - now.0
89 }
90 }
91}
92
93pub struct KeyFetchCache {
98 failures: Arc<parking_lot::RwLock<LruCache<String, FailureEntry>>>,
100}
101
102impl KeyFetchCache {
103 pub fn new(max_entries: usize) -> Self {
105 let capacity = NonZeroUsize::new(max_entries.max(1)).unwrap_or(NonZeroUsize::MIN);
106
107 Self { failures: Arc::new(parking_lot::RwLock::new(LruCache::new(capacity))) }
108 }
109
110 fn make_key(issuer: &str, key_id: &str) -> String {
112 format!("{}:{}", issuer, key_id)
113 }
114
115 pub fn check_failure(&self, issuer: &str, key_id: &str) -> Option<FailureEntry> {
120 let key = Self::make_key(issuer, key_id);
121 let mut cache = self.failures.write();
122
123 if let Some(entry) = cache.get(&key) {
124 if entry.is_expired() {
125 cache.pop(&key);
127 None
128 } else {
129 Some(entry.clone())
131 }
132 } else {
133 None
134 }
135 }
136
137 pub fn record_failure(&self, issuer: &str, key_id: &str, error: &Error) {
139 let key = Self::make_key(issuer, key_id);
140 let failure_type = FailureType::from_error(error);
141 let entry = FailureEntry::new(failure_type);
142
143 debug!(
144 "Caching key fetch failure for {} (type: {:?}, retry in {} secs)",
145 key,
146 failure_type,
147 entry.seconds_until_retry()
148 );
149
150 let mut cache = self.failures.write();
151 cache.put(key, entry);
152 }
153
154 pub fn clear_failure(&self, issuer: &str, key_id: &str) {
156 let key = Self::make_key(issuer, key_id);
157 let mut cache = self.failures.write();
158 cache.pop(&key);
159 }
160
161 pub fn len(&self) -> usize {
163 self.failures.read().len()
164 }
165
166 pub fn is_empty(&self) -> bool {
168 self.failures.read().is_empty()
169 }
170
171 pub fn clear(&self) {
173 self.failures.write().clear();
174 }
175}
176
177impl Default for KeyFetchCache {
178 fn default() -> Self {
179 Self::new(DEFAULT_CACHE_CAPACITY)
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn test_failure_type_ttl() {
189 assert_eq!(FailureType::NetworkError.ttl_secs(), 5 * 60);
190 assert_eq!(FailureType::NotFound.ttl_secs(), 60 * 60);
191 assert_eq!(FailureType::Unauthorized.ttl_secs(), 60 * 60);
192 assert_eq!(FailureType::ParseError.ttl_secs(), 60 * 60);
193 }
194
195 #[test]
196 fn test_failure_entry_expiration() {
197 let entry = FailureEntry::new(FailureType::NetworkError);
198 assert!(!entry.is_expired());
200 assert!(entry.seconds_until_retry() > 0);
201 }
202
203 #[test]
204 fn test_cache_operations() {
205 let cache = KeyFetchCache::new(10);
206
207 assert!(cache.is_empty());
209 assert!(cache.check_failure("alice.example.com", "key-1").is_none());
210
211 cache.record_failure("alice.example.com", "key-1", &Error::NotFound);
213
214 assert!(!cache.is_empty());
216 assert_eq!(cache.len(), 1);
217
218 let failure = cache.check_failure("alice.example.com", "key-1");
219 assert!(failure.is_some());
220 assert_eq!(failure.unwrap().failure_type, FailureType::NotFound);
221
222 cache.clear_failure("alice.example.com", "key-1");
224 assert!(cache.is_empty());
225 }
226
227 #[test]
228 fn test_cache_lru_eviction() {
229 let cache = KeyFetchCache::new(2);
230
231 cache.record_failure("a.com", "k1", &Error::NotFound);
232 cache.record_failure("b.com", "k2", &Error::NotFound);
233 assert_eq!(cache.len(), 2);
234
235 cache.record_failure("c.com", "k3", &Error::NotFound);
237 assert_eq!(cache.len(), 2);
238
239 assert!(cache.check_failure("a.com", "k1").is_none());
241 assert!(cache.check_failure("b.com", "k2").is_some());
242 assert!(cache.check_failure("c.com", "k3").is_some());
243 }
244
245 #[test]
246 fn test_failure_type_from_error() {
247 assert_eq!(
248 FailureType::from_error(&Error::NetworkError("test".into())),
249 FailureType::NetworkError
250 );
251 assert_eq!(FailureType::from_error(&Error::NotFound), FailureType::NotFound);
252 assert_eq!(FailureType::from_error(&Error::PermissionDenied), FailureType::Unauthorized);
253 assert_eq!(FailureType::from_error(&Error::Unauthorized), FailureType::Unauthorized);
254 assert_eq!(FailureType::from_error(&Error::Parse), FailureType::ParseError);
255 }
256}
257
258