1use std::future::Future;
19use std::time::{Duration, Instant};
20use tokio::sync::Mutex;
21
22#[derive(Debug, Clone)]
24pub struct TemporaryToken<T> {
25 pub token: T,
27 pub expiry: Option<Instant>,
30}
31
32#[derive(Debug)]
53pub struct TokenCache<T> {
54 cache: Mutex<Option<(TemporaryToken<T>, Instant)>>,
55 min_ttl: Duration,
56 fetch_backoff: Duration,
57}
58
59impl<T> Default for TokenCache<T> {
60 fn default() -> Self {
61 Self {
62 cache: Default::default(),
63 min_ttl: Duration::from_secs(300),
64 fetch_backoff: Duration::from_millis(100),
67 }
68 }
69}
70
71impl<T: Clone + Send> TokenCache<T> {
72 pub(crate) fn with_min_ttl(self, min_ttl: Duration) -> Self {
74 Self { min_ttl, ..self }
75 }
76
77 pub async fn get_or_insert_with<F, Fut, E>(&self, f: F) -> Result<T, E>
78 where
79 F: FnOnce() -> Fut + Send,
80 Fut: Future<Output = Result<TemporaryToken<T>, E>> + Send,
81 {
82 let now = Instant::now();
83 let mut locked = self.cache.lock().await;
84
85 if let Some((cached, fetched_at)) = locked.as_ref() {
86 match cached.expiry {
87 Some(ttl) => {
88 if ttl.checked_duration_since(now).unwrap_or_default() > self.min_ttl ||
89 (fetched_at.elapsed() < self.fetch_backoff && ttl.checked_duration_since(now).is_some())
92 {
93 return Ok(cached.token.clone());
94 }
95 }
96 None => return Ok(cached.token.clone()),
97 }
98 }
99
100 let cached = f().await?;
101 let token = cached.token.clone();
102 *locked = Some((cached, Instant::now()));
103
104 Ok(token)
105 }
106}
107
108#[cfg(test)]
109mod test {
110 use super::*;
111 use std::sync::Arc;
112 use std::sync::atomic::{AtomicU32, Ordering};
113 use std::time::{Duration, Instant};
114
115 fn create_token(expiry_duration: Option<Duration>) -> TemporaryToken<String> {
117 TemporaryToken {
118 token: "test_token".to_string(),
119 expiry: expiry_duration.map(|d| Instant::now() + d),
120 }
121 }
122
123 #[tokio::test]
124 async fn test_expired_token_is_refreshed() {
125 let cache = TokenCache::default();
126 static COUNTER: AtomicU32 = AtomicU32::new(0);
127
128 async fn get_token() -> Result<TemporaryToken<String>, String> {
129 COUNTER.fetch_add(1, Ordering::SeqCst);
130 Ok::<_, String>(create_token(Some(Duration::from_secs(0))))
131 }
132
133 let _ = cache.get_or_insert_with(get_token).await.unwrap();
135 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
136
137 tokio::time::sleep(Duration::from_millis(2)).await;
138
139 let _ = cache.get_or_insert_with(get_token).await.unwrap();
141 assert_eq!(COUNTER.load(Ordering::SeqCst), 2);
142 }
143
144 #[tokio::test]
145 async fn test_min_ttl_causes_refresh() {
146 let cache = TokenCache {
147 cache: Default::default(),
148 min_ttl: Duration::from_secs(1),
149 fetch_backoff: Duration::from_millis(1),
150 };
151
152 static COUNTER: AtomicU32 = AtomicU32::new(0);
153
154 async fn get_token() -> Result<TemporaryToken<String>, String> {
155 COUNTER.fetch_add(1, Ordering::SeqCst);
156 Ok::<_, String>(create_token(Some(Duration::from_millis(100))))
157 }
158
159 let _ = cache.get_or_insert_with(get_token).await.unwrap();
161 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
162
163 let _ = cache.get_or_insert_with(get_token).await.unwrap();
165 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
166
167 tokio::time::sleep(Duration::from_millis(2)).await;
168
169 let _ = cache.get_or_insert_with(get_token).await.unwrap();
171 assert_eq!(COUNTER.load(Ordering::SeqCst), 2);
172 }
173
174 #[tokio::test]
175 async fn test_valid_token_within_ttl_is_reused() {
176 let cache = TokenCache::default();
179 static COUNTER: AtomicU32 = AtomicU32::new(0);
180
181 async fn get_token() -> Result<TemporaryToken<String>, String> {
182 COUNTER.fetch_add(1, Ordering::SeqCst);
183 Ok::<_, String>(create_token(Some(Duration::from_secs(3600))))
184 }
185
186 let _ = cache.get_or_insert_with(get_token).await.unwrap();
188 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
189
190 for _ in 0..5 {
192 let _ = cache.get_or_insert_with(get_token).await.unwrap();
193 }
194 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
195 }
196
197 #[tokio::test]
198 async fn test_non_expiring_token_is_reused() {
199 let cache = TokenCache::default();
202 static COUNTER: AtomicU32 = AtomicU32::new(0);
203
204 async fn get_token() -> Result<TemporaryToken<String>, String> {
205 COUNTER.fetch_add(1, Ordering::SeqCst);
206 Ok::<_, String>(create_token(None))
207 }
208
209 let _ = cache.get_or_insert_with(get_token).await.unwrap();
210 let _ = cache.get_or_insert_with(get_token).await.unwrap();
211 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
212 }
213
214 #[tokio::test]
215 async fn test_concurrent_callers_single_flight() {
216 let cache = Arc::new(TokenCache::default());
221 static COUNTER: AtomicU32 = AtomicU32::new(0);
222
223 async fn get_token() -> Result<TemporaryToken<String>, String> {
224 COUNTER.fetch_add(1, Ordering::SeqCst);
225 tokio::time::sleep(Duration::from_millis(20)).await;
227 Ok::<_, String>(create_token(Some(Duration::from_secs(3600))))
228 }
229
230 let mut handles = Vec::new();
231 for _ in 0..10 {
232 let cache = Arc::clone(&cache);
233 handles.push(tokio::spawn(async move {
234 cache.get_or_insert_with(get_token).await.unwrap()
235 }));
236 }
237 for h in handles {
238 h.await.unwrap();
239 }
240
241 assert_eq!(COUNTER.load(Ordering::SeqCst), 1);
243 }
244}