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