http_cache/managers/redb.rs
1//! HTTP cache manager backed by an embedded [`redb`](https://github.com/cberner/redb)
2//! key/value database.
3//!
4//! Unlike [`CACacheManager`](crate::CACacheManager), this manager performs all
5//! of its I/O **synchronously** and depends on **no async runtime** — `redb`
6//! itself only depends on `libc`. That makes it a good fit for smol-based
7//! clients (such as `http-cache-ureq`) and for embedding runtimes like Bevy,
8//! where there is no tokio reactor available: it pulls in no `tokio`
9//! dependency and requires no reactor at runtime.
10//!
11//! The cache is stored in a single database file. Only one instance may have
12//! the file open at a time — `redb` takes an exclusive file lock — so wrap the
13//! manager in `Arc`/a `static` and share it rather than constructing a second
14//! instance for the same path.
15//!
16//! Stores are batched for durability: entries are committed without an
17//! fsync and flushed to disk every 64 writes (configurable via
18//! [`RedbManager::from_database_with_flush_interval`]) and on drop, so a
19//! crash can lose approximately the most recent 64 stores. Deletes commit
20//! durably, so an invalidated entry cannot come back after a crash. When
21//! sharing a [`Database`] via [`RedbManager::from_database`], do not drop
22//! the last manager clone while holding an open `WriteTransaction` on that
23//! database — the drop-time flush must acquire the writer lock.
24
25use std::path::Path;
26use std::sync::atomic::{AtomicU64, Ordering};
27use std::sync::Arc;
28
29use crate::{CacheManager, HttpResponse, Result};
30
31use http_cache_semantics::CachePolicy;
32use redb::{Database, Durability, ReadableDatabase, TableDefinition};
33use serde::{Deserialize, Serialize};
34
35pub(crate) const TABLE: TableDefinition<&str, &[u8]> =
36 TableDefinition::new("http_cache_v1");
37
38/// Implements [`CacheManager`] with [`redb`](https://github.com/cberner/redb)
39/// as the backend — a pure-Rust, embedded, persistent key/value store.
40///
41/// All operations are synchronous and require no async runtime, so this
42/// manager works under any executor (tokio, smol, Bevy, …) and adds no
43/// `tokio` dependency.
44#[cfg_attr(docsrs, doc(cfg(feature = "manager-redb")))]
45#[derive(Clone)]
46pub struct RedbManager {
47 flush: Arc<FlushState>,
48}
49
50impl std::fmt::Debug for RedbManager {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 f.debug_struct("RedbManager").finish_non_exhaustive()
53 }
54}
55
56// Store format (postcard). `manager-redb` always enables `postcard`, and this
57// is a new backend with no legacy on-disk data, so only postcard is supported.
58#[derive(Debug, Deserialize, Serialize)]
59struct Store {
60 response: HttpResponse,
61 policy: CachePolicy,
62}
63
64/// Stores are committed with `Durability::None` and made durable by an empty
65/// `Immediate` commit every `interval` writes and when the last manager
66/// clone drops.
67const DEFAULT_FLUSH_INTERVAL: u64 = 64;
68
69struct FlushState {
70 db: Arc<Database>,
71 unflushed: AtomicU64,
72 interval: u64,
73}
74
75impl FlushState {
76 fn flush(&self) -> Result<()> {
77 // An empty Immediate commit persists all prior None-durability
78 // commits.
79 let write_txn = self.db.begin_write()?;
80 write_txn.commit()?;
81 Ok(())
82 }
83
84 fn record_write(&self) {
85 if self.unflushed.fetch_add(1, Ordering::Relaxed) + 1 >= self.interval {
86 // Claim the whole batch so writers queued behind a flush in
87 // flight don't each fsync. The write's own commit already
88 // succeeded; on failure the count is restored and the drop
89 // guard retries — though redb latches I/O errors, so retries
90 // fail until the database is reopened.
91 let n = self.unflushed.swap(0, Ordering::Relaxed);
92 if n == 0 {
93 return;
94 }
95 if let Err(e) = self.flush() {
96 self.unflushed.fetch_add(n, Ordering::Relaxed);
97 log::warn!("redb interval flush failed: {e}");
98 }
99 }
100 }
101}
102
103impl Drop for FlushState {
104 fn drop(&mut self) {
105 if self.unflushed.load(Ordering::Relaxed) > 0 {
106 if let Err(e) = self.flush() {
107 log::warn!("redb flush on drop failed: {e}");
108 }
109 }
110 }
111}
112
113impl RedbManager {
114 /// Creates a new [`RedbManager`], creating or opening the redb database
115 /// file at `path`.
116 ///
117 /// Returns an error if the database cannot be opened (for example if
118 /// another instance already holds the exclusive file lock).
119 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
120 let db = Database::create(path)?;
121 Self::from_database(Arc::new(db))
122 }
123
124 /// Wraps an already-open redb [`Database`], allowing callers to configure
125 /// the database (cache size, etc.) before handing it to the manager.
126 pub fn from_database(db: Arc<Database>) -> Result<Self> {
127 Self::from_database_with_flush_interval(db, DEFAULT_FLUSH_INTERVAL)
128 }
129
130 /// Like [`from_database`](Self::from_database), but flushes stores to
131 /// disk every `flush_interval` writes instead of the default 64. An
132 /// interval of 1 flushes after every store.
133 pub fn from_database_with_flush_interval(
134 db: Arc<Database>,
135 flush_interval: u64,
136 ) -> Result<Self> {
137 // Ensure the table exists up front so reads never fail with
138 // "table does not exist".
139 let write_txn = db.begin_write()?;
140 {
141 let _table = write_txn.open_table(TABLE)?;
142 }
143 write_txn.commit()?;
144 Ok(Self {
145 flush: Arc::new(FlushState {
146 db,
147 unflushed: AtomicU64::new(0),
148 interval: flush_interval.max(1),
149 }),
150 })
151 }
152
153 /// Clears out the entire cache.
154 pub async fn clear(&self) -> Result<()> {
155 let write_txn = self.flush.db.begin_write()?;
156 write_txn.delete_table(TABLE)?;
157 // Recreate the (now empty) table so subsequent reads still succeed.
158 {
159 let _table = write_txn.open_table(TABLE)?;
160 }
161 write_txn.commit()?;
162 Ok(())
163 }
164
165 /// Reads and decodes the stored entry for `cache_key`, if present.
166 /// Deserializes from the borrowed value while the read transaction is
167 /// alive to avoid copying the entry out of redb first.
168 fn read_entry(&self, cache_key: &str) -> Result<Option<Store>> {
169 let read_txn = self.flush.db.begin_read()?;
170 let table = read_txn.open_table(TABLE)?;
171 match table.get(cache_key)? {
172 Some(guard) => {
173 match postcard::from_bytes::<Store>(guard.value()) {
174 Ok(store) => Ok(Some(store)),
175 Err(e) => {
176 // Treat undecodable entries as a miss rather than an
177 // error, matching the other managers.
178 log::debug!(
179 "Failed to deserialize cache entry for key \
180 '{cache_key}': {e}"
181 );
182 Ok(None)
183 }
184 }
185 }
186 None => Ok(None),
187 }
188 }
189}
190
191impl CacheManager for RedbManager {
192 async fn get(
193 &self,
194 cache_key: &str,
195 ) -> Result<Option<(HttpResponse, CachePolicy)>> {
196 // A storage-level read error is treated as a cache miss (degrade to a
197 // fresh fetch) rather than a hard error, matching `CACacheManager`.
198 match self.read_entry(cache_key) {
199 Ok(Some(store)) => Ok(Some((store.response, store.policy))),
200 Ok(None) => Ok(None),
201 Err(e) => {
202 log::debug!("redb read failed for key '{cache_key}': {e}");
203 Ok(None)
204 }
205 }
206 }
207
208 async fn put(
209 &self,
210 cache_key: String,
211 response: HttpResponse,
212 policy: CachePolicy,
213 ) -> Result<HttpResponse> {
214 let data = Store { response, policy };
215 let bytes = postcard::to_allocvec(&data)?;
216 let mut write_txn = self.flush.db.begin_write()?;
217 write_txn.set_durability(Durability::None)?;
218 {
219 let mut table = write_txn.open_table(TABLE)?;
220 table.insert(cache_key.as_str(), bytes.as_slice())?;
221 }
222 write_txn.commit()?;
223 self.flush.record_write();
224 Ok(data.response)
225 }
226
227 async fn delete(&self, cache_key: &str) -> Result<()> {
228 // Deletes commit durably: an invalidation rolled back by a crash
229 // would resurrect an entry the origin already replaced.
230 let write_txn = self.flush.db.begin_write()?;
231 {
232 let mut table = write_txn.open_table(TABLE)?;
233 table.remove(cache_key)?;
234 }
235 write_txn.commit()?;
236 Ok(())
237 }
238}