Skip to content

Commit 906c45d

Browse files
committed
fix: cache dump restore and periodic dump trigger
ttl 0 read as expired; counter was per-clone
1 parent 8da820a commit 906c45d

1 file changed

Lines changed: 105 additions & 44 deletions

File tree

src/plugins/cache/mod.rs

Lines changed: 105 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ pub struct CachePlugin {
138138
/// Seconds between periodic dumps (default 600).
139139
dump_interval_secs: u64,
140140
/// Writes since last dump; triggers dump when it exceeds threshold.
141-
changes_since_dump: AtomicU64,
141+
/// Shared via Arc so the background task (a clone) sees live updates.
142+
changes_since_dump: Arc<AtomicU64>,
143+
/// Unix seconds of the last successful dump; 0 = never.
144+
last_dump_unix: Arc<AtomicU64>,
142145
}
143146

144147
impl Clone for CachePlugin {
@@ -161,7 +164,8 @@ impl Clone for CachePlugin {
161164
cleanup_pressure_threshold: self.cleanup_pressure_threshold,
162165
dump_file: self.dump_file.clone(),
163166
dump_interval_secs: self.dump_interval_secs,
164-
changes_since_dump: AtomicU64::new(self.changes_since_dump.load(Ordering::Relaxed)),
167+
changes_since_dump: Arc::clone(&self.changes_since_dump),
168+
last_dump_unix: Arc::clone(&self.last_dump_unix),
165169
}
166170
}
167171
}
@@ -200,7 +204,8 @@ impl CachePlugin {
200204
cleanup_pressure_threshold: 0.8,
201205
dump_file: None,
202206
dump_interval_secs: 600,
203-
changes_since_dump: AtomicU64::new(0),
207+
changes_since_dump: Arc::new(AtomicU64::new(0)),
208+
last_dump_unix: Arc::new(AtomicU64::new(0)),
204209
}
205210
}
206211

@@ -482,6 +487,7 @@ impl CachePlugin {
482487
Ok(()) => {
483488
debug!(entries = entries.len(), path = %path.display(), "cache dumped");
484489
self.changes_since_dump.store(0, Ordering::Relaxed);
490+
self.last_dump_unix.store(unix_now(), Ordering::Relaxed);
485491
}
486492
Err(e) => {
487493
warn!(error = %e, "failed to dump cache");
@@ -490,6 +496,51 @@ impl CachePlugin {
490496
}
491497
}
492498

499+
/// Whether the configured dump interval has elapsed since the last dump.
500+
fn dump_interval_elapsed(&self) -> bool {
501+
let last = self.last_dump_unix.load(Ordering::Relaxed);
502+
last == 0 || unix_now().saturating_sub(last) >= self.dump_interval_secs
503+
}
504+
505+
/// Load a previous dump file into the cache. Entries whose original TTL
506+
/// has burned down while the server was down are skipped.
507+
fn restore_from_dump(&mut self) {
508+
let Some(ref path) = self.dump_file else {
509+
return;
510+
};
511+
match persistence::load_cache(path) {
512+
Ok(loaded) => {
513+
let now = unix_now();
514+
let mut count = 0;
515+
let mut c = self.cache.write();
516+
for entry in loaded {
517+
let elapsed = now.saturating_sub(entry.cached_at_unix);
518+
let remaining = entry.original_ttl.saturating_sub(elapsed as u32);
519+
if remaining == 0 {
520+
continue;
521+
}
522+
523+
let cache_entry = CacheEntry {
524+
response: Arc::new(entry.response),
525+
cached_at: Instant::now(),
526+
ttl: remaining,
527+
// is_cache_expired treats 0 as already expired
528+
cache_ttl: remaining,
529+
original_ttl: entry.original_ttl,
530+
last_accessed: Instant::now(),
531+
cached_at_unix: entry.cached_at_unix,
532+
};
533+
c.push(entry.key, cache_entry);
534+
count += 1;
535+
}
536+
debug!(loaded = count, "restored cache entries from dump");
537+
}
538+
Err(e) => {
539+
warn!(error = %e, "failed to load cache dump");
540+
}
541+
}
542+
}
543+
493544
/// Get minimum TTL from a DNS message
494545
fn get_min_ttl(message: &Message) -> u32 {
495546
let mut min_ttl = u32::MAX;
@@ -758,6 +809,13 @@ impl CachePlugin {
758809
}
759810
}
760811

812+
fn unix_now() -> u64 {
813+
std::time::SystemTime::now()
814+
.duration_since(std::time::UNIX_EPOCH)
815+
.map(|d| d.as_secs())
816+
.unwrap_or(0)
817+
}
818+
761819
impl fmt::Debug for CachePlugin {
762820
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
763821
f.debug_struct("CachePlugin")
@@ -790,10 +848,13 @@ impl BackgroundTask for CachePlugin {
790848
);
791849
}
792850

793-
if self.dump_file.is_some()
794-
&& self.changes_since_dump.load(Ordering::Relaxed) >= persistence::dump_threshold()
795-
{
796-
self.dump_to_file();
851+
if self.dump_file.is_some() {
852+
let changes = self.changes_since_dump.load(Ordering::Relaxed);
853+
if changes > 0
854+
&& (changes >= persistence::dump_threshold() || self.dump_interval_elapsed())
855+
{
856+
self.dump_to_file();
857+
}
797858
}
798859
}
799860

@@ -943,43 +1004,7 @@ impl Plugin for CachePlugin {
9431004
cache.dump_interval_secs = n.as_u64().unwrap_or(600);
9441005
}
9451006

946-
// Load existing dump into the cache.
947-
if let Some(ref path) = cache.dump_file {
948-
match persistence::load_cache(path) {
949-
Ok(loaded) => {
950-
let now = std::time::SystemTime::now()
951-
.duration_since(std::time::UNIX_EPOCH)
952-
.map(|d| d.as_secs())
953-
.unwrap_or(0);
954-
955-
let mut count = 0;
956-
let mut c = cache.cache.write();
957-
for entry in loaded {
958-
let elapsed = now.saturating_sub(entry.cached_at_unix);
959-
let remaining = entry.original_ttl.saturating_sub(elapsed as u32);
960-
if remaining == 0 {
961-
continue;
962-
}
963-
964-
let cache_entry = CacheEntry {
965-
response: std::sync::Arc::new(entry.response),
966-
cached_at: Instant::now(),
967-
ttl: remaining,
968-
cache_ttl: 0,
969-
original_ttl: entry.original_ttl,
970-
last_accessed: Instant::now(),
971-
cached_at_unix: entry.cached_at_unix,
972-
};
973-
c.push(entry.key, cache_entry);
974-
count += 1;
975-
}
976-
debug!(loaded = count, "restored cache entries from dump");
977-
}
978-
Err(e) => {
979-
warn!(error = %e, "failed to load cache dump");
980-
}
981-
}
982-
}
1007+
cache.restore_from_dump();
9831008
}
9841009

9851010
// Set tag from config
@@ -1489,6 +1514,42 @@ plugins:
14891514
assert_eq!(cache.stats().expirations(), 2); // Stats updated
14901515
}
14911516

1517+
#[test]
1518+
fn test_dump_counter_shared_with_clone() {
1519+
// the background task runs on a clone; increments on the original
1520+
// must be visible there or periodic dumps never fire
1521+
let cache = CachePlugin::new(100);
1522+
let bg = cache.clone();
1523+
cache.changes_since_dump.fetch_add(3, Ordering::Relaxed);
1524+
assert_eq!(bg.changes_since_dump.load(Ordering::Relaxed), 3);
1525+
}
1526+
1527+
#[test]
1528+
fn test_dump_and_restore() {
1529+
let file =
1530+
std::env::temp_dir().join(format!("lazydns_cache_test_{}.bin", std::process::id()));
1531+
1532+
let mut src = CachePlugin::new(100);
1533+
src.dump_file = Some(file.clone());
1534+
src.cache.write().push(
1535+
"example.com".to_string(),
1536+
CacheEntry::new(create_test_response(), 300, 300),
1537+
);
1538+
src.dump_to_file();
1539+
1540+
let mut restored = CachePlugin::new(100);
1541+
restored.dump_file = Some(file.clone());
1542+
restored.restore_from_dump();
1543+
1544+
let cache = restored.cache.read();
1545+
let entry = cache.peek("example.com").expect("entry not restored");
1546+
assert!(!entry.is_cache_expired(), "restored entry must be servable");
1547+
assert!(entry.remaining_ttl() > 0);
1548+
drop(cache);
1549+
1550+
let _ = std::fs::remove_file(&file);
1551+
}
1552+
14921553
#[test]
14931554
fn test_should_cleanup_pressure() {
14941555
let mut cache = CachePlugin::new(10);

0 commit comments

Comments
 (0)