@@ -688,13 +688,28 @@ impl KvStore {
688688 /// operator, which for a single-node deployment holding the only copy of
689689 /// the ACME account and DNS credentials is the difference between a restart
690690 /// and a rebuild.
691+ ///
692+ /// `wal_sync_interval` is how long a write may sit in the page cache before
693+ /// the log is forced to disk; `None` forces every write before it returns.
694+ /// It applies only to the persistent store — the ephemeral one keeps no log
695+ /// and never touches the disk.
691696 pub fn new (
692697 my_node_id : NodeId ,
693698 peer_ids : Vec < NodeId > ,
694699 data_dir : impl AsRef < Path > ,
700+ wal_sync_interval : Option < Duration > ,
695701 ) -> Result < Self > {
696702 let data_dir = data_dir. as_ref ( ) ;
697- let persistent = match Node :: new_with_persistence ( my_node_id, peer_ids. clone ( ) , data_dir) {
703+ let node_config = wavekv:: NodeConfig {
704+ wal_sync_interval,
705+ ..Default :: default ( )
706+ } ;
707+ let persistent = match Node :: with_persistence_and_config (
708+ my_node_id,
709+ peer_ids. clone ( ) ,
710+ data_dir,
711+ node_config. clone ( ) ,
712+ ) {
698713 Ok ( node) => node,
699714 Err ( err) if is_storage_failure ( & err) => {
700715 return Err ( err) . with_context ( || {
@@ -721,8 +736,13 @@ impl KvStore {
721736 data_dir. display( ) ,
722737 quarantined. display( ) ,
723738 ) ;
724- Node :: new_with_persistence ( my_node_id, peer_ids. clone ( ) , data_dir)
725- . context ( "failed to create persistent wavekv node on a fresh data dir" ) ?
739+ Node :: with_persistence_and_config (
740+ my_node_id,
741+ peer_ids. clone ( ) ,
742+ data_dir,
743+ node_config,
744+ )
745+ . context ( "failed to create persistent wavekv node on a fresh data dir" ) ?
726746 }
727747 } ;
728748
@@ -1033,6 +1053,15 @@ impl KvStore {
10331053 self . persistent . persist_if_dirty ( )
10341054 }
10351055
1056+ /// Force the write-ahead log to disk if the configured window has elapsed.
1057+ ///
1058+ /// Returns whether an fsync happened. A no-op when no window is configured
1059+ /// — every write was already forced — or when nothing has been written
1060+ /// since the last one, so an idle gateway costs a lock acquisition.
1061+ pub fn sync_wal_if_due ( & self ) -> Result < bool > {
1062+ self . persistent . sync_wal_if_due ( )
1063+ }
1064+
10361065 // ==================== Peer Management ====================
10371066
10381067 pub fn add_peer ( & self , peer_id : NodeId ) -> Result < ( ) > {
@@ -1625,7 +1654,7 @@ mod acme_credentials_tests {
16251654 use super :: * ;
16261655
16271656 fn test_kv ( data_dir : & std:: path:: Path ) -> KvStore {
1628- KvStore :: new ( 1 , vec ! [ ] , data_dir) . expect ( "failed to create kv store" )
1657+ KvStore :: new ( 1 , vec ! [ ] , data_dir, None ) . expect ( "failed to create kv store" )
16291658 }
16301659
16311660 #[ test]
@@ -1772,7 +1801,7 @@ mod value_encoding_tests {
17721801 #[ test]
17731802 fn legacy_positional_records_are_still_readable ( ) {
17741803 let dir = tempfile:: tempdir ( ) . expect ( "failed to create temp dir" ) ;
1775- let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) ) . expect ( "failed to create kv store" ) ;
1804+ let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) , None ) . expect ( "failed to create kv store" ) ;
17761805
17771806 let legacy = rmp_serde:: encode:: to_vec ( & CertRenewLock {
17781807 started_at : 1_700_000_000 ,
@@ -1836,6 +1865,51 @@ mod value_encoding_tests {
18361865 }
18371866 }
18381867
1868+ /// The configured window has to reach the store, not just the config file.
1869+ ///
1870+ /// An fsync per write runs under the store lock, so it bounds how fast this
1871+ /// gateway accepts registrations; the window is what buys that back, and it
1872+ /// buys nothing if the interval stops at `SyncConfig`.
1873+ #[ test]
1874+ fn a_configured_window_holds_writes_out_of_the_disk_until_it_elapses ( ) {
1875+ let dir = tempfile:: tempdir ( ) . expect ( "failed to create temp dir" ) ;
1876+ let window = Duration :: from_millis ( 20 ) ;
1877+ let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) , Some ( window) ) . expect ( "kv store" ) ;
1878+
1879+ kv. set_node_status ( 1 , NodeStatus :: Up ) . expect ( "write" ) ;
1880+ assert_eq ! (
1881+ kv. persistent( ) . read( ) . wal_sync_count( ) ,
1882+ 0 ,
1883+ "a write inside the window must not reach the disk"
1884+ ) ;
1885+ assert ! (
1886+ !kv. sync_wal_if_due( ) . expect( "sync check" ) ,
1887+ "nothing is due before the window elapses"
1888+ ) ;
1889+
1890+ std:: thread:: sleep ( window * 3 ) ;
1891+ assert ! ( kv. sync_wal_if_due( ) . expect( "sync" ) , "the window elapsed" ) ;
1892+ assert_eq ! ( kv. persistent( ) . read( ) . wal_sync_count( ) , 1 ) ;
1893+ }
1894+
1895+ /// Without a window every write is on the disk before it returns, which is
1896+ /// what every release before this one did and what a single-node gateway
1897+ /// holding the only copy of its ACME account still wants.
1898+ #[ test]
1899+ fn without_a_window_every_write_reaches_the_disk_before_it_returns ( ) {
1900+ let dir = tempfile:: tempdir ( ) . expect ( "failed to create temp dir" ) ;
1901+ let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) , None ) . expect ( "kv store" ) ;
1902+
1903+ kv. set_node_status ( 1 , NodeStatus :: Up ) . expect ( "write" ) ;
1904+ kv. set_node_status ( 2 , NodeStatus :: Down ) . expect ( "write" ) ;
1905+
1906+ assert_eq ! ( kv. persistent( ) . read( ) . wal_sync_count( ) , 2 ) ;
1907+ assert ! (
1908+ !kv. sync_wal_if_due( ) . expect( "sync check" ) ,
1909+ "with no window there is never anything owing"
1910+ ) ;
1911+ }
1912+
18391913 /// An instance record as some later release will declare it.
18401914 #[ derive( Debug , Serialize , Deserialize ) ]
18411915 struct FutureInstanceData {
@@ -1854,7 +1928,7 @@ mod value_encoding_tests {
18541928 #[ test]
18551929 fn a_record_keeps_the_fields_its_writer_does_not_know ( ) {
18561930 let dir = tempfile:: tempdir ( ) . expect ( "failed to create temp dir" ) ;
1857- let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) ) . expect ( "failed to create kv store" ) ;
1931+ let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) , None ) . expect ( "failed to create kv store" ) ;
18581932
18591933 let written_by_a_newer_node = encode ( & FutureInstanceData {
18601934 app_id : "app" . to_string ( ) ,
@@ -1913,7 +1987,7 @@ mod value_encoding_tests {
19131987 #[ test]
19141988 fn a_snapshot_does_not_inherit_the_previous_writes_fields ( ) {
19151989 let dir = tempfile:: tempdir ( ) . expect ( "failed to create temp dir" ) ;
1916- let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) ) . expect ( "failed to create kv store" ) ;
1990+ let kv = KvStore :: new ( 1 , vec ! [ ] , dir. path ( ) , None ) . expect ( "failed to create kv store" ) ;
19171991
19181992 #[ derive( Debug , Serialize , Deserialize ) ]
19191993 struct FutureCertData {
@@ -1973,7 +2047,7 @@ mod sync_wire_tests {
19732047 use wavekv:: sync:: SyncEnvelope ;
19742048
19752049 fn store ( dir : & std:: path:: Path , id : NodeId , peers : Vec < NodeId > ) -> KvStore {
1976- KvStore :: new ( id, peers, dir) . expect ( "failed to create kv store" )
2050+ KvStore :: new ( id, peers, dir, None ) . expect ( "failed to create kv store" )
19772051 }
19782052
19792053 #[ test]
@@ -2048,7 +2122,8 @@ mod wavekv_v1_migration_tests {
20482122 . expect ( "write trailing v1 WAL entry" ) ;
20492123 }
20502124
2051- let upgraded = KvStore :: new ( 1 , Vec :: new ( ) , dir. path ( ) ) . expect ( "open v1 data after upgrade" ) ;
2125+ let upgraded =
2126+ KvStore :: new ( 1 , Vec :: new ( ) , dir. path ( ) , None ) . expect ( "open v1 data after upgrade" ) ;
20522127 assert_eq ! (
20532128 upgraded
20542129 . persistent( )
@@ -2078,7 +2153,8 @@ mod wavekv_v1_migration_tests {
20782153 upgraded. persist_if_dirty ( ) . expect ( "persist upgraded data" ) ;
20792154 drop ( upgraded) ;
20802155
2081- let restarted = KvStore :: new ( 1 , Vec :: new ( ) , dir. path ( ) ) . expect ( "restart upgraded store" ) ;
2156+ let restarted =
2157+ KvStore :: new ( 1 , Vec :: new ( ) , dir. path ( ) , None ) . expect ( "restart upgraded store" ) ;
20822158 for ( key, expected) in [ ( key, value) , ( wal_key, wal_value) , ( new_key, new_value) ] {
20832159 assert_eq ! (
20842160 restarted
@@ -2131,7 +2207,7 @@ mod corruption_tests {
21312207 use super :: * ;
21322208
21332209 fn test_kv ( data_dir : & std:: path:: Path ) -> KvStore {
2134- KvStore :: new ( 1 , vec ! [ ] , data_dir) . expect ( "failed to create kv store" )
2210+ KvStore :: new ( 1 , vec ! [ ] , data_dir, None ) . expect ( "failed to create kv store" )
21352211 }
21362212
21372213 fn put_raw ( kv : & KvStore , key : & str , value : & [ u8 ] ) {
@@ -2245,7 +2321,7 @@ mod corruption_tests {
22452321 let data_dir = dir. path ( ) . join ( "kv" ) ;
22462322 std:: fs:: write ( & data_dir, b"not a directory" ) . expect ( "failed to create blocker" ) ;
22472323
2248- let Err ( err) = KvStore :: new ( 1 , vec ! [ ] , & data_dir) else {
2324+ let Err ( err) = KvStore :: new ( 1 , vec ! [ ] , & data_dir, None ) else {
22492325 panic ! ( "startup must fail when the storage is unusable" ) ;
22502326 } ;
22512327 assert ! (
@@ -2285,7 +2361,8 @@ mod corruption_tests {
22852361 // replicated, so it must not keep the gateway from booting.
22862362 std:: fs:: write ( data_dir. join ( "node_1.wal" ) , b"garbage" ) . expect ( "failed to corrupt wal" ) ;
22872363
2288- let kv = KvStore :: new ( 1 , vec ! [ ] , & data_dir) . expect ( "startup must survive a corrupt wal" ) ;
2364+ let kv =
2365+ KvStore :: new ( 1 , vec ! [ ] , & data_dir, None ) . expect ( "startup must survive a corrupt wal" ) ;
22892366 let loaded = kv. load_all_instances ( ) ;
22902367 assert ! ( loaded. decoded. is_empty( ) ) ;
22912368 assert ! ( loaded. undecodable. is_empty( ) ) ;
0 commit comments