1use std::collections::HashMap;
43use std::future::Future;
44use std::sync::atomic::{AtomicI64, Ordering};
45use std::sync::{Arc, Mutex, Weak};
46use std::time::Duration;
47
48use serde::Serialize;
49use serde::de::DeserializeOwned;
50use serde_json::Value;
51
52use crate::Result;
53use crate::db::{Db, Migration};
54use crate::queue::unix_now;
55
56pub(crate) const MIGRATION: Migration =
57 crate::db::framework_migration!("cache", "00010101000200_create_cache_table");
58
59const SWEEP_AT: usize = 10_000;
61
62type Entries = HashMap<String, (Value, Option<i64>)>;
64
65#[derive(Clone)]
66enum Store {
67 Memory(Arc<Mutex<Entries>>),
68 Database(Db),
69}
70
71#[derive(Clone)]
73pub struct Cache {
74 store: Store,
75 computing: Arc<Mutex<HashMap<String, Weak<tokio::sync::Mutex<()>>>>>,
77}
78
79fn expiry(ttl: Option<Duration>) -> Option<i64> {
81 ttl.map(|ttl| unix_now() + ttl.as_secs() as i64 + i64::from(ttl.subsec_nanos() > 0))
82}
83
84const FRAMEWORK_PREFIX: &str = "renox:";
86const LOCK_PREFIX: &str = "renox:lock:";
88const PRUNE_EVERY: i64 = 60 * 60;
90
91fn live(expires: Option<i64>, now: i64) -> bool {
92 expires.is_none_or(|at| at > now)
93}
94
95impl Cache {
96 pub(crate) fn new(store: crate::CacheStore, db: Db) -> anyhow::Result<Self> {
97 let store = match store {
98 crate::CacheStore::Memory => Store::Memory(Arc::default()),
99 crate::CacheStore::Database => Store::Database(db),
100 };
101 Ok(Self {
102 store,
103 computing: Arc::default(),
104 })
105 }
106
107 async fn raw(&self, key: &str) -> Result<Option<Value>> {
108 let now = unix_now();
109 match &self.store {
110 Store::Memory(map) => {
111 let map = map.lock().unwrap_or_else(|e| e.into_inner());
112 Ok(map
113 .get(key)
114 .filter(|(_, expires)| expires.is_none_or(|at| at > now))
115 .map(|(value, _)| value.clone()))
116 }
117 Store::Database(db) => {
118 let text: Option<String> = crate::db::sql(
119 "SELECT value FROM cache WHERE key = ? AND (expires_at IS NULL OR expires_at > ?)",
120 )
121 .bind(key)
122 .bind(now)
123 .scalar_optional(db)
124 .await?;
125 Ok(text.and_then(|t| serde_json::from_str(&t).ok()))
126 }
127 }
128 }
129
130 pub async fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
133 match self.raw(key).await? {
134 None => Ok(None),
135 Some(value) => serde_json::from_value(value).map(Some).map_err(|err| {
136 anyhow::Error::new(err)
137 .context(format!(
138 "the cached `{key}` is not a {}",
139 std::any::type_name::<T>()
140 ))
141 .into()
142 }),
143 }
144 }
145
146 pub async fn has(&self, key: &str) -> Result<bool> {
148 Ok(self.raw(key).await?.is_some())
149 }
150
151 pub async fn put(&self, key: &str, value: &impl Serialize, ttl: Option<Duration>) -> Result {
154 if ttl == Some(Duration::ZERO) {
155 return self.forget(key).await;
156 }
157 let value = serde_json::to_value(value)?;
158 let expires = expiry(ttl);
159 match &self.store {
160 Store::Memory(map) => {
161 let mut map = map.lock().unwrap_or_else(|e| e.into_inner());
162 if map.len() >= SWEEP_AT {
163 let now = unix_now();
164 map.retain(|_, (_, at)| at.is_none_or(|at| at > now));
165 }
166 map.insert(key.to_owned(), (value, expires));
167 }
168 Store::Database(db) => {
169 self.prune_now_and_then(db).await;
170 crate::db::sql(
171 "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) \
172 ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at",
173 )
174 .bind(key)
175 .bind(value.to_string())
176 .bind(expires)
177 .execute(db)
178 .await?;
179 }
180 }
181 Ok(())
182 }
183
184 pub async fn add(
187 &self,
188 key: &str,
189 value: &impl Serialize,
190 ttl: Option<Duration>,
191 ) -> Result<bool> {
192 let value = serde_json::to_value(value)?;
193 let expires = expiry(ttl);
194 let now = unix_now();
195 match &self.store {
196 Store::Memory(map) => {
197 let mut map = map.lock().unwrap_or_else(|e| e.into_inner());
198 if map.get(key).is_some_and(|(_, at)| live(*at, now)) {
199 return Ok(false);
200 }
201 map.insert(key.to_owned(), (value, expires));
202 Ok(true)
203 }
204 Store::Database(db) => {
205 self.prune_now_and_then(db).await;
206 let added = crate::db::sql(
207 "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, ?) \
208 ON CONFLICT (key) DO UPDATE SET value = excluded.value, \
209 expires_at = excluded.expires_at \
210 WHERE cache.expires_at IS NOT NULL AND cache.expires_at <= ?",
211 )
212 .bind(key)
213 .bind(value.to_string())
214 .bind(expires)
215 .bind(now)
216 .execute(db)
217 .await?;
218 Ok(added == 1)
219 }
220 }
221 }
222
223 pub async fn pull<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
226 let now = unix_now();
227 let value = match &self.store {
228 Store::Memory(map) => map
229 .lock()
230 .unwrap_or_else(|e| e.into_inner())
231 .remove(key)
232 .filter(|(_, at)| live(*at, now))
233 .map(|(value, _)| value),
234 Store::Database(db) => {
235 let row: Option<(String, Option<i64>)> =
236 crate::db::sql("DELETE FROM cache WHERE key = ? RETURNING value, expires_at")
237 .bind(key)
238 .fetch_as(db)
239 .await?
240 .into_iter()
241 .next();
242 row.filter(|(_, at)| live(*at, now))
243 .and_then(|(text, _)| serde_json::from_str(&text).ok())
244 }
245 };
246 match value {
247 None => Ok(None),
248 Some(value) => Ok(Some(serde_json::from_value(value).map_err(|err| {
249 anyhow::Error::new(err).context(format!("the cached `{key}` is not that type"))
250 })?)),
251 }
252 }
253
254 pub async fn increment(&self, key: &str, by: i64) -> Result<i64> {
259 let now = unix_now();
260 match &self.store {
261 Store::Memory(map) => {
262 let mut map = map.lock().unwrap_or_else(|e| e.into_inner());
263 let (current, expires) = match map.get(key) {
264 Some((value, at)) if live(*at, now) => {
265 let Some(n) = value.as_i64() else {
266 return Err(anyhow::anyhow!(
267 "the cached `{key}` is not a whole number"
268 )
269 .into());
270 };
271 (n, *at)
272 }
273 _ => (0, None),
274 };
275 let next = current
276 .checked_add(by)
277 .ok_or_else(|| anyhow::anyhow!("the cached `{key}` would overflow"))?;
278 map.insert(key.to_owned(), (Value::from(next), expires));
279 Ok(next)
280 }
281 Store::Database(db) => {
282 if let Some(value) = self.raw(key).await?
283 && !value.is_i64()
284 {
285 return Err(anyhow::anyhow!("the cached `{key}` is not a whole number").into());
286 }
287 let next: String = crate::db::sql(
288 "INSERT INTO cache (key, value, expires_at) VALUES (?, ?, NULL) \
289 ON CONFLICT (key) DO UPDATE SET \
290 value = CASE WHEN cache.expires_at IS NOT NULL AND cache.expires_at <= ? \
291 THEN excluded.value \
292 ELSE CAST(CAST(cache.value AS BIGINT) + ? AS TEXT) END, \
293 expires_at = CASE WHEN cache.expires_at IS NOT NULL AND cache.expires_at <= ? \
294 THEN NULL ELSE cache.expires_at END \
295 RETURNING value",
296 )
297 .bind(key)
298 .bind(by.to_string())
299 .bind(now)
300 .bind(by)
301 .bind(now)
302 .scalar(db)
303 .await?;
304 Ok(next
305 .parse()
306 .map_err(|_| anyhow::anyhow!("the cached `{key}` is not a whole number"))?)
307 }
308 }
309 }
310
311 pub async fn decrement(&self, key: &str, by: i64) -> Result<i64> {
313 self.increment(key, -by).await
314 }
315
316 pub fn lock(&self, name: &str, ttl: Duration) -> Lock {
322 Lock {
323 cache: self.clone(),
324 key: format!("{LOCK_PREFIX}{name}"),
325 ttl: ttl.max(Duration::from_secs(1)),
326 }
327 }
328
329 pub async fn prune(&self) -> Result<u64> {
332 let now = unix_now();
333 match &self.store {
334 Store::Memory(map) => {
335 map.lock()
336 .unwrap_or_else(|e| e.into_inner())
337 .retain(|_, (_, at)| live(*at, now));
338 Ok(0)
339 }
340 Store::Database(db) => Ok(crate::db::sql(
341 "DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at <= ?",
342 )
343 .bind(now)
344 .execute(db)
345 .await?),
346 }
347 }
348
349 async fn prune_now_and_then(&self, db: &Db) {
351 static LAST: AtomicI64 = AtomicI64::new(0);
352 let now = unix_now();
353 let last = LAST.load(Ordering::Relaxed);
354 if now - last < PRUNE_EVERY
355 || LAST
356 .compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
357 .is_err()
358 {
359 return;
360 }
361 delete_expired(db, now).await;
362 }
363
364 async fn forget_if(&self, key: &str, value: &Value) -> Result<bool> {
366 match &self.store {
367 Store::Memory(map) => {
368 let mut map = map.lock().unwrap_or_else(|e| e.into_inner());
369 if map.get(key).is_some_and(|(held, _)| held == value) {
370 map.remove(key);
371 return Ok(true);
372 }
373 Ok(false)
374 }
375 Store::Database(db) => Ok(crate::db::sql(
376 "DELETE FROM cache WHERE key = ? AND value = ?",
377 )
378 .bind(key)
379 .bind(value.to_string())
380 .execute(db)
381 .await?
382 == 1),
383 }
384 }
385
386 pub async fn remember<T, F, Fut>(&self, key: &str, ttl: Duration, compute: F) -> Result<T>
391 where
392 T: Serialize + DeserializeOwned,
393 F: FnOnce() -> Fut,
394 Fut: Future<Output = Result<T>>,
395 {
396 if let Some(value) = self.cached(key).await? {
397 return Ok(value);
398 }
399 let lock = self.computing_lock(key);
400 let _computing = lock.lock().await;
401 if let Some(value) = self.cached(key).await? {
402 return Ok(value);
403 }
404 let value = compute().await?;
405 self.put(key, &value, Some(ttl)).await?;
406 Ok(value)
407 }
408
409 async fn cached<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>> {
411 Ok(self
412 .raw(key)
413 .await?
414 .and_then(|v| serde_json::from_value(v).ok()))
415 }
416
417 fn computing_lock(&self, key: &str) -> Arc<tokio::sync::Mutex<()>> {
418 let mut locks = self.computing.lock().unwrap_or_else(|e| e.into_inner());
419 if let Some(lock) = locks.get(key).and_then(Weak::upgrade) {
420 return lock;
421 }
422 locks.retain(|_, lock| lock.strong_count() > 0);
423 let lock = Arc::new(tokio::sync::Mutex::new(()));
424 locks.insert(key.to_owned(), Arc::downgrade(&lock));
425 lock
426 }
427
428 pub async fn forget(&self, key: &str) -> Result {
430 match &self.store {
431 Store::Memory(map) => {
432 map.lock().unwrap_or_else(|e| e.into_inner()).remove(key);
433 }
434 Store::Database(db) => {
435 crate::db::sql("DELETE FROM cache WHERE key = ?")
436 .bind(key)
437 .execute(db)
438 .await?;
439 }
440 }
441 Ok(())
442 }
443
444 pub async fn flush(&self) -> Result {
446 match &self.store {
447 Store::Memory(map) => map
448 .lock()
449 .unwrap_or_else(|e| e.into_inner())
450 .retain(|key, _| key.starts_with(FRAMEWORK_PREFIX)),
451 Store::Database(db) => {
452 crate::db::sql("DELETE FROM cache WHERE key NOT LIKE ?")
453 .bind(format!("{FRAMEWORK_PREFIX}%"))
454 .execute(db)
455 .await?;
456 }
457 }
458 Ok(())
459 }
460}
461
462#[derive(Clone)]
464pub struct Lock {
465 cache: Cache,
466 key: String,
467 ttl: Duration,
468}
469
470#[must_use = "the lock is released when the guard is dropped"]
473pub struct LockGuard {
474 cache: Cache,
475 key: String,
476 owner: Value,
477 released: bool,
478}
479
480impl Lock {
481 pub async fn try_acquire(&self) -> Result<Option<LockGuard>> {
483 let owner = Value::String(crate::crypto::random_token());
484 let ttl = self.ttl + Duration::from_secs(1);
486 if self.cache.add(&self.key, &owner, Some(ttl)).await? {
487 return Ok(Some(LockGuard {
488 cache: self.cache.clone(),
489 key: self.key.clone(),
490 owner,
491 released: false,
492 }));
493 }
494 Ok(None)
495 }
496
497 pub async fn block(&self, wait: Duration) -> Result<LockGuard> {
500 let deadline = tokio::time::Instant::now() + wait;
501 let mut pause = Duration::from_millis(25);
502 loop {
503 if let Some(guard) = self.try_acquire().await? {
504 return Ok(guard);
505 }
506 let now = tokio::time::Instant::now();
507 if now >= deadline {
508 let name = self.key.trim_start_matches(LOCK_PREFIX);
509 return Err(crate::abort(
510 axum::http::StatusCode::LOCKED,
511 format!("the lock `{name}` is held elsewhere"),
512 ));
513 }
514 tokio::time::sleep(pause.min(deadline - now)).await;
515 pause = (pause * 2).min(Duration::from_millis(250));
516 }
517 }
518
519 pub async fn is_held(&self) -> Result<bool> {
521 self.cache.has(&self.key).await
522 }
523
524 pub async fn force_release(&self) -> Result {
526 self.cache.forget(&self.key).await
527 }
528}
529
530impl LockGuard {
531 pub async fn release(mut self) -> Result<bool> {
534 self.released = true;
535 self.cache.forget_if(&self.key, &self.owner).await
536 }
537}
538
539impl Drop for LockGuard {
540 fn drop(&mut self) {
541 if self.released {
542 return;
543 }
544 let (cache, key, owner) = (
545 self.cache.clone(),
546 std::mem::take(&mut self.key),
547 self.owner.take(),
548 );
549 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
550 runtime.spawn(async move {
551 if let Err(err) = cache.forget_if(&key, &owner).await {
552 tracing::warn!(lock = %key, error = ?err, "could not release a lock");
553 }
554 });
555 }
556 }
557}
558
559async fn delete_expired(db: &Db, now: i64) {
562 let pruned =
563 crate::db::sql("DELETE FROM cache WHERE expires_at IS NOT NULL AND expires_at <= ?")
564 .bind(now)
565 .execute(db)
566 .await;
567 if let Err(err) = pruned {
568 tracing::warn!(error = %err, "could not prune expired cache rows");
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 async fn db() -> Db {
577 crate::db::connect(&crate::Config::default()).await.unwrap()
578 }
579
580 #[tokio::test]
582 async fn the_memory_store_sweeps_expired_values_once_it_is_large() {
583 let cache = Cache::new(crate::CacheStore::Memory, db().await).unwrap();
584 for i in 0..SWEEP_AT {
585 cache
586 .put(&format!("k{i}"), &i, Some(Duration::from_secs(1)))
587 .await
588 .unwrap();
589 }
590 crate::clock::with_offset(10, cache.put("late", &1, None))
591 .await
592 .unwrap();
593 let Store::Memory(map) = &cache.store else {
594 unreachable!()
595 };
596 assert_eq!(map.lock().unwrap().len(), 1);
597 }
598
599 #[tokio::test]
603 async fn the_database_store_without_its_table() {
604 let (logs, _logged) = crate::test_logs::capture();
605 let db = db().await;
606 let cache = Cache::new(crate::CacheStore::Database, db.clone()).unwrap();
607 assert!(cache.put("k", &1, None).await.is_err());
608 delete_expired(&db, 0).await;
609 assert!(
610 logs.has(&["could not prune expired cache rows"]),
611 "{}",
612 logs.text()
613 );
614
615 crate::db::sql("CREATE TABLE cache (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL, expires_at BIGINT)")
616 .execute(&db)
617 .await
618 .unwrap();
619 let lock = cache.lock("report", Duration::from_secs(60));
620 let guard = lock.try_acquire().await.unwrap().expect("free");
621 crate::db::sql("ALTER TABLE cache RENAME TO cache_away")
622 .execute(&db)
623 .await
624 .unwrap();
625 drop(guard);
626 tokio::time::sleep(Duration::from_millis(100)).await;
627 crate::db::sql("ALTER TABLE cache_away RENAME TO cache")
628 .execute(&db)
629 .await
630 .unwrap();
631 assert!(lock.is_held().await.unwrap());
632 assert!(logs.has(&["could not release a lock"]), "{}", logs.text());
633 }
634}