@@ -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
144147impl 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+
761819impl 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