Skip to main content

sl_glw/
cache.rs

1//! Three-tier persistent cache for GLW events.
2//!
3//! Layered identically to `sl_map_apis::region::RegionNameToGridCoordinatesCache`:
4//!
5//! 1. In-memory `lru::LruCache` keyed by [`EventId`] or [`GlwEventKey`].
6//! 2. On-disk `redb` database holding the JSON-serialised event plus
7//!    its `http_cache_semantics::CachePolicy`.
8//! 3. Live HTTP fetch via [`crate::client`], with cache-policy
9//!    revalidation against the disk-cached policy.
10
11use std::path::PathBuf;
12
13use redb::ReadableDatabase as _;
14
15use crate::client::{default_base_url, fetch_event_by_id, fetch_event_by_key};
16use crate::error::GlwEventCacheError;
17use crate::types::{EventId, GlwEvent, GlwEventKey};
18
19/// Cached value alongside its `http-cache-semantics` policy.
20type CachedEvent = (Option<GlwEvent>, http_cache_semantics::CachePolicy);
21
22/// Redb table mapping numeric event ids to JSON-serialised
23/// `Option<GlwEvent>` strings.
24const GLW_EVENT_BY_ID_TABLE: redb::TableDefinition<u32, String> =
25    redb::TableDefinition::new("glw_event_by_id");
26
27/// Redb table mapping numeric event ids to JSON-serialised
28/// `CachePolicy` strings.
29const GLW_EVENT_BY_ID_POLICY_TABLE: redb::TableDefinition<u32, String> =
30    redb::TableDefinition::new("glw_event_by_id_policy");
31
32/// Redb table mapping string event keys to JSON-serialised
33/// `Option<GlwEvent>` strings.
34const GLW_EVENT_BY_KEY_TABLE: redb::TableDefinition<String, String> =
35    redb::TableDefinition::new("glw_event_by_key");
36
37/// Redb table mapping string event keys to JSON-serialised
38/// `CachePolicy` strings.
39const GLW_EVENT_BY_KEY_POLICY_TABLE: redb::TableDefinition<String, String> =
40    redb::TableDefinition::new("glw_event_by_key_policy");
41
42/// Three-tier cache for GLW events.
43///
44/// `&mut self` is required on the read methods because the in-memory
45/// LRU promotes entries on access and the redb writes happen on the
46/// write path.
47#[expect(
48    clippy::module_name_repetitions,
49    reason = "GlwEventCache is the primary public type of this module"
50)]
51#[derive(Debug)]
52pub struct GlwEventCache {
53    /// reqwest HTTP client for upstream fetches.
54    client: reqwest::Client,
55    /// Base URL the `glwDataReq.php` path is joined onto.
56    base_url: url::Url,
57    /// Persistent on-disk cache (`redb`).
58    db: redb::Database,
59    /// In-memory cache keyed by numeric event id.
60    memory_by_id: lru::LruCache<EventId, CachedEvent>,
61    /// In-memory cache keyed by string event key.
62    memory_by_key: lru::LruCache<GlwEventKey, CachedEvent>,
63}
64
65impl GlwEventCache {
66    /// Create a new cache backed by `<cache_directory>/glw_event.redb`.
67    ///
68    /// `base_url` is the GLW base URL (must include the version segment
69    /// as a path, e.g. `http://example.com/glw127/`). Pass `None` to use
70    /// the workspace default.
71    ///
72    /// # Errors
73    ///
74    /// Returns [`GlwEventCacheError`] if the database file cannot be
75    /// created or if the default URL fails to parse (the latter being
76    /// unreachable for the hard-coded defaults).
77    pub fn new(
78        cache_directory: PathBuf,
79        base_url: Option<url::Url>,
80    ) -> Result<Self, GlwEventCacheError> {
81        let client = crate::client::build_http_client();
82        let base_url = match base_url {
83            Some(u) => u,
84            None => default_base_url().map_err(GlwEventCacheError::FetchError)?,
85        };
86        let db = redb::Database::create(cache_directory.join("glw_event.redb"))?;
87        Ok(Self {
88            client,
89            base_url,
90            db,
91            memory_by_id: lru::LruCache::unbounded(),
92            memory_by_key: lru::LruCache::unbounded(),
93        })
94    }
95
96    /// Get a GLW event by numeric id.
97    ///
98    /// Returns `Ok(None)` if the server reports no such event.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`GlwEventCacheError`] if a database or upstream HTTP
103    /// operation fails.
104    #[tracing::instrument(skip(self))]
105    pub async fn get_event_by_id(
106        &mut self,
107        id: EventId,
108    ) -> Result<Option<GlwEvent>, GlwEventCacheError> {
109        tracing::debug!("Retrieving GLW event by id {id}");
110        let cached = self.load_by_id_from_caches(id)?;
111        let (event, cache_policy) =
112            fetch_event_by_id(&self.client, &self.base_url, id, cached).await?;
113        self.store_by_id(id, event.as_ref(), &cache_policy)?;
114        Ok(event)
115    }
116
117    /// Get a GLW event by string event key.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`GlwEventCacheError`] if a database or upstream HTTP
122    /// operation fails.
123    #[tracing::instrument(skip(self))]
124    pub async fn get_event_by_key(
125        &mut self,
126        key: &GlwEventKey,
127    ) -> Result<Option<GlwEvent>, GlwEventCacheError> {
128        tracing::debug!("Retrieving GLW event by key {key}");
129        let cached = self.load_by_key_from_caches(key)?;
130        let (event, cache_policy) =
131            fetch_event_by_key(&self.client, &self.base_url, key, cached).await?;
132        self.store_by_key(key, event.as_ref(), &cache_policy)?;
133        Ok(event)
134    }
135
136    /// Force a fresh fetch of an event by id, bypassing all cache tiers.
137    ///
138    /// # Errors
139    ///
140    /// Returns [`GlwEventCacheError`] if a database or upstream HTTP
141    /// operation fails.
142    pub async fn refresh_event_by_id(
143        &mut self,
144        id: EventId,
145    ) -> Result<Option<GlwEvent>, GlwEventCacheError> {
146        let (event, cache_policy) =
147            fetch_event_by_id(&self.client, &self.base_url, id, None).await?;
148        self.store_by_id(id, event.as_ref(), &cache_policy)?;
149        Ok(event)
150    }
151
152    /// Force a fresh fetch of an event by key, bypassing all cache tiers.
153    ///
154    /// # Errors
155    ///
156    /// Returns [`GlwEventCacheError`] if a database or upstream HTTP
157    /// operation fails.
158    pub async fn refresh_event_by_key(
159        &mut self,
160        key: &GlwEventKey,
161    ) -> Result<Option<GlwEvent>, GlwEventCacheError> {
162        let (event, cache_policy) =
163            fetch_event_by_key(&self.client, &self.base_url, key, None).await?;
164        self.store_by_key(key, event.as_ref(), &cache_policy)?;
165        Ok(event)
166    }
167
168    /// Look up a cached id-keyed value+policy across the memory and
169    /// disk tiers.
170    fn load_by_id_from_caches(
171        &mut self,
172        id: EventId,
173    ) -> Result<Option<CachedEvent>, GlwEventCacheError> {
174        if let Some(hit) = self.memory_by_id.get(&id) {
175            return Ok(Some(hit.to_owned()));
176        }
177        let read_txn = self.db.begin_read()?;
178        let policy = match read_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE) {
179            Ok(table) => match table.get(id.get())? {
180                Some(value) => {
181                    let policy: http_cache_semantics::CachePolicy =
182                        serde_json::from_str(&value.value())?;
183                    Some(policy)
184                }
185                None => None,
186            },
187            Err(_) => None,
188        };
189        let Some(policy) = policy else {
190            return Ok(None);
191        };
192        let value = match read_txn.open_table(GLW_EVENT_BY_ID_TABLE) {
193            Ok(table) => match table.get(id.get())? {
194                Some(value) => {
195                    let event: Option<GlwEvent> = serde_json::from_str(&value.value())?;
196                    event
197                }
198                None => None,
199            },
200            Err(_) => None,
201        };
202        Ok(Some((value, policy)))
203    }
204
205    /// Look up a cached key-keyed value+policy across the memory and
206    /// disk tiers.
207    fn load_by_key_from_caches(
208        &mut self,
209        key: &GlwEventKey,
210    ) -> Result<Option<CachedEvent>, GlwEventCacheError> {
211        if let Some(hit) = self.memory_by_key.get(key) {
212            return Ok(Some(hit.to_owned()));
213        }
214        let read_txn = self.db.begin_read()?;
215        let policy = match read_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE) {
216            Ok(table) => match table.get(key.as_str().to_owned())? {
217                Some(value) => {
218                    let policy: http_cache_semantics::CachePolicy =
219                        serde_json::from_str(&value.value())?;
220                    Some(policy)
221                }
222                None => None,
223            },
224            Err(_) => None,
225        };
226        let Some(policy) = policy else {
227            return Ok(None);
228        };
229        let value = match read_txn.open_table(GLW_EVENT_BY_KEY_TABLE) {
230            Ok(table) => match table.get(key.as_str().to_owned())? {
231                Some(value) => {
232                    let event: Option<GlwEvent> = serde_json::from_str(&value.value())?;
233                    event
234                }
235                None => None,
236            },
237            Err(_) => None,
238        };
239        Ok(Some((value, policy)))
240    }
241
242    /// Persist (or evict) the result of an id-keyed lookup. Stores both
243    /// positive and negative responses, but only when the policy is
244    /// `is_storable` per `http-cache-semantics`.
245    fn store_by_id(
246        &mut self,
247        id: EventId,
248        event: Option<&GlwEvent>,
249        cache_policy: &http_cache_semantics::CachePolicy,
250    ) -> Result<(), GlwEventCacheError> {
251        let write_txn = self.db.begin_write()?;
252        if cache_policy.is_storable() {
253            {
254                let mut policy_table = write_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE)?;
255                policy_table.insert(id.get(), serde_json::to_string(cache_policy)?)?;
256            }
257            {
258                let mut value_table = write_txn.open_table(GLW_EVENT_BY_ID_TABLE)?;
259                value_table.insert(id.get(), serde_json::to_string(&event)?)?;
260            }
261            write_txn.commit()?;
262            self.memory_by_id
263                .put(id, (event.cloned(), cache_policy.clone()));
264        } else {
265            tracing::debug!("GLW event by id is not storable; evicting any stale entry");
266            {
267                let mut policy_table = write_txn.open_table(GLW_EVENT_BY_ID_POLICY_TABLE)?;
268                policy_table.remove(id.get())?;
269            }
270            {
271                let mut value_table = write_txn.open_table(GLW_EVENT_BY_ID_TABLE)?;
272                value_table.remove(id.get())?;
273            }
274            write_txn.commit()?;
275            self.memory_by_id.pop(&id);
276        }
277        Ok(())
278    }
279
280    /// Persist (or evict) the result of a key-keyed lookup.
281    fn store_by_key(
282        &mut self,
283        key: &GlwEventKey,
284        event: Option<&GlwEvent>,
285        cache_policy: &http_cache_semantics::CachePolicy,
286    ) -> Result<(), GlwEventCacheError> {
287        let write_txn = self.db.begin_write()?;
288        if cache_policy.is_storable() {
289            {
290                let mut policy_table = write_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE)?;
291                policy_table.insert(
292                    key.as_str().to_owned(),
293                    serde_json::to_string(cache_policy)?,
294                )?;
295            }
296            {
297                let mut value_table = write_txn.open_table(GLW_EVENT_BY_KEY_TABLE)?;
298                value_table.insert(key.as_str().to_owned(), serde_json::to_string(&event)?)?;
299            }
300            write_txn.commit()?;
301            self.memory_by_key
302                .put(key.to_owned(), (event.cloned(), cache_policy.clone()));
303        } else {
304            tracing::debug!("GLW event by key is not storable; evicting any stale entry");
305            {
306                let mut policy_table = write_txn.open_table(GLW_EVENT_BY_KEY_POLICY_TABLE)?;
307                policy_table.remove(key.as_str().to_owned())?;
308            }
309            {
310                let mut value_table = write_txn.open_table(GLW_EVENT_BY_KEY_TABLE)?;
311                value_table.remove(key.as_str().to_owned())?;
312            }
313            write_txn.commit()?;
314            self.memory_by_key.pop(key);
315        }
316        Ok(())
317    }
318}