fix(prop-grid-rs): evict expired IdxCache entries on get
The in-memory idx cache kept entries around forever: get() returned None on expiry but left the HashMap entry in place. Each hourly HRRR cycle adds ~38 new idx URLs (19 forecast hours × surface + pressure), so the map grew ~4 MiB/day indefinitely on each Rust replica. Not the main cause of the OOM loop (glibc fragmentation was; jemalloc landed last commit) but worth sealing so a long- lived pod's idx cache can't drift toward the 3 GiB ceiling over weeks. Evicts on-touch via `get()` — simple, amortized, no background sweep thread needed.
This commit is contained in:
parent
641789449a
commit
3ec976ccc4
1 changed files with 37 additions and 1 deletions
|
|
@ -294,11 +294,14 @@ impl IdxCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&self, url: &str) -> Option<String> {
|
pub fn get(&self, url: &str) -> Option<String> {
|
||||||
if let Ok(map) = self.inner.lock() {
|
if let Ok(mut map) = self.inner.lock() {
|
||||||
if let Some(entry) = map.get(url) {
|
if let Some(entry) = map.get(url) {
|
||||||
if entry.inserted.elapsed() < IDX_TTL {
|
if entry.inserted.elapsed() < IDX_TTL {
|
||||||
return Some(entry.body.clone());
|
return Some(entry.body.clone());
|
||||||
}
|
}
|
||||||
|
// Expired entry — evict so unique idx URLs don't
|
||||||
|
// accumulate across hourly HRRR cycles.
|
||||||
|
map.remove(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(disk) = &self.disk {
|
if let Some(disk) = &self.disk {
|
||||||
|
|
@ -332,6 +335,24 @@ impl IdxCache {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.inner.lock().map(|m| m.len()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn put_with_age(&self, url: String, body: String, age: Duration) {
|
||||||
|
if let Ok(mut map) = self.inner.lock() {
|
||||||
|
map.insert(
|
||||||
|
url,
|
||||||
|
IdxCacheEntry {
|
||||||
|
body,
|
||||||
|
inserted: Instant::now() - age,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── HTTP client ──────────────────────────────────────────────────────
|
// ── HTTP client ──────────────────────────────────────────────────────
|
||||||
|
|
@ -722,6 +743,21 @@ mod tests {
|
||||||
assert!(cache.get("missing").is_none());
|
assert!(cache.get("missing").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idx_cache_removes_expired_entries_on_get() {
|
||||||
|
// Without eviction the HashMap grows forever as unique idx URLs
|
||||||
|
// accumulate each hour. Calling get() on an expired entry must
|
||||||
|
// remove it so the in-memory map bounds itself to the hot set.
|
||||||
|
let cache = IdxCache::new();
|
||||||
|
cache.put_with_age("stale".into(), "old".into(), IDX_TTL + Duration::from_secs(1));
|
||||||
|
cache.put("fresh".into(), "new".into());
|
||||||
|
|
||||||
|
assert_eq!(cache.len(), 2);
|
||||||
|
assert!(cache.get("stale").is_none());
|
||||||
|
assert_eq!(cache.len(), 1, "expired entry should have been evicted");
|
||||||
|
assert_eq!(cache.get("fresh").as_deref(), Some("new"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disk_idx_cache_round_trips() {
|
fn disk_idx_cache_round_trips() {
|
||||||
let tmp = tempfile::TempDir::new().unwrap();
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue