@@ -28,7 +28,6 @@ use crate::logger::{LdkLogger, Logger};
2828use crate :: types:: { DynStore , DynStoreWrapper } ;
2929
3030const INDEX_DATABASE_ID_LEN : usize = 16 ;
31- const PAGE_TOKEN_FORMAT_VERSION : u8 = 1 ;
3231const INDEX_ENTRIES_PRIMARY_NAMESPACE : & str = "_tier_store_entries" ;
3332const INDEX_JOURNAL_PRIMARY_NAMESPACE : & str = "_tier_store_journal" ;
3433const INDEX_METADATA_PRIMARY_NAMESPACE : & str = "_tier_store_metadata" ;
@@ -37,6 +36,20 @@ const INDEX_NAMESPACE_READY_KEY_PREFIX: &str = "ready_";
3736const INDEX_CACHE_READY_KEY_PREFIX : & str = "cache_ready_" ;
3837const INDEX_ENTRY_VALUE : & [ u8 ] = & [ 1 ] ;
3938
39+ const PAGE_TOKEN_FORMAT_VERSION : u8 = 1 ;
40+
41+ const BACKUP_SYNC_GENERATION_LEN : usize = 16 ;
42+ const BACKUP_SYNC_PRIMARY_NAMESPACE : & str = "_tier_store_backup_sync" ;
43+ const PRIMARY_SYNC_GENERATION_KEY : & str = "primary_generation" ;
44+ const BACKUP_SYNC_COMPLETION_KEY : & str = "completed_generation" ;
45+
46+ #[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
47+ pub ( crate ) enum BackupSyncStatus {
48+ NotConfigured ,
49+ Synchronized ,
50+ Required ,
51+ }
52+
4053#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
4154enum ValueTier {
4255 Primary ,
@@ -601,6 +614,26 @@ impl TierStore {
601614
602615 inner. index = Some ( index) ;
603616 }
617+
618+ /// Initializes the durable metadata used to determine whether the backup matches the primary.
619+ ///
620+ /// This must be called once after the optional backup store has been configured and before the
621+ /// `TierStore` is used. If no backup is configured but a primary generation already exists, it
622+ /// writes a new generation so any backup completed against the earlier generation will be
623+ /// recognized as stale if it returns. If no generation exists, no backup has yet been tracked and
624+ /// no metadata is created. If a backup is configured, it reads or creates the primary generation
625+ /// and compares it with the backup's completion record without modifying the completion record.
626+ ///
627+ /// Returns [`BackupSyncStatus::NotConfigured`] when no backup is present, whether or not an
628+ /// existing primary generation was rotated, [`BackupSyncStatus::Synchronized`] when both records match, or
629+ /// [`BackupSyncStatus::Required`] when the backup has no completion record or records another
630+ /// generation. A `Required` result only classifies the backup; it does not perform synchronization.
631+ ///
632+ /// Returns an error if generation metadata cannot be read, generated, or persisted, or if a
633+ /// stored generation has an invalid length.
634+ pub ( crate ) async fn initialize_backup_synchronization ( & self ) -> io:: Result < BackupSyncStatus > {
635+ self . inner . initialize_backup_synchronization ( ) . await
636+ }
604637}
605638
606639pub ( crate ) async fn setup_index_store ( data_dir : PathBuf ) -> io:: Result < TierStoreIndex > {
@@ -731,6 +764,149 @@ impl TierStoreInner {
731764 }
732765 }
733766
767+ /// Rotates or compares the stores' backup-synchronization generations.
768+ ///
769+ /// Without a configured backup, this rotates an existing primary generation so primary-only
770+ /// operation invalidates any earlier backup completion. It leaves primary metadata absent when no
771+ /// backup has ever established a generation. With a configured backup, this preserves the primary
772+ /// generation (creating it if absent) and compares it with the completion generation stored in the
773+ /// backup. A missing or different backup completion is classified as requiring synchronization;
774+ /// this method does not perform that synchronization.
775+ async fn initialize_backup_synchronization ( & self ) -> io:: Result < BackupSyncStatus > {
776+ if self . backup_store . is_none ( ) {
777+ match Self :: read_backup_sync_generation (
778+ self . primary_store . as_ref ( ) ,
779+ PRIMARY_SYNC_GENERATION_KEY ,
780+ )
781+ . await
782+ {
783+ Ok ( _) => {
784+ let generation = Self :: generate_backup_sync_generation ( ) ?;
785+ self . write_primary_backup_sync_generation ( generation) . await ?;
786+ } ,
787+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => { } ,
788+ Err ( e) => return Err ( e) ,
789+ }
790+ return Ok ( BackupSyncStatus :: NotConfigured ) ;
791+ }
792+
793+ let primary_generation = self . read_or_create_primary_backup_sync_generation ( ) . await ?;
794+ match self . read_backup_sync_completion ( ) . await {
795+ Ok ( backup_generation) if backup_generation == primary_generation => {
796+ Ok ( BackupSyncStatus :: Synchronized )
797+ } ,
798+ Ok ( _) => Ok ( BackupSyncStatus :: Required ) ,
799+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => Ok ( BackupSyncStatus :: Required ) ,
800+ Err ( e) => Err ( e) ,
801+ }
802+ }
803+
804+ /// Generates an opaque random identity for one synchronization generation.
805+ fn generate_backup_sync_generation ( ) -> io:: Result < [ u8 ; BACKUP_SYNC_GENERATION_LEN ] > {
806+ let mut generation = [ 0 ; BACKUP_SYNC_GENERATION_LEN ] ;
807+ getrandom:: fill ( & mut generation) . map_err ( |e| {
808+ io:: Error :: new (
809+ io:: ErrorKind :: Other ,
810+ format ! ( "Failed to generate tier-store backup synchronization generation: {e}" ) ,
811+ )
812+ } ) ?;
813+ Ok ( generation)
814+ }
815+
816+ /// Reads the primary's current synchronization generation, creating and persisting one if absent.
817+ ///
818+ /// An existing generation is preserved so a matching backup completion remains valid across
819+ /// restarts where the backup stays configured.
820+ async fn read_or_create_primary_backup_sync_generation (
821+ & self ,
822+ ) -> io:: Result < [ u8 ; BACKUP_SYNC_GENERATION_LEN ] > {
823+ match Self :: read_backup_sync_generation (
824+ self . primary_store . as_ref ( ) ,
825+ PRIMARY_SYNC_GENERATION_KEY ,
826+ )
827+ . await
828+ {
829+ Ok ( generation) => Ok ( generation) ,
830+ Err ( e) if e. kind ( ) == io:: ErrorKind :: NotFound => {
831+ let generation = Self :: generate_backup_sync_generation ( ) ?;
832+ self . write_primary_backup_sync_generation ( generation) . await ?;
833+ Ok ( generation)
834+ } ,
835+ Err ( e) => Err ( e) ,
836+ }
837+ }
838+
839+ /// Persists the current synchronization generation directly in the authoritative primary store.
840+ ///
841+ /// The write intentionally bypasses backup replication: changing this record invalidates an old
842+ /// backup, whose completion record must remain unchanged until synchronization actually finishes.
843+ async fn write_primary_backup_sync_generation (
844+ & self , generation : [ u8 ; BACKUP_SYNC_GENERATION_LEN ] ,
845+ ) -> io:: Result < ( ) > {
846+ KVStore :: write (
847+ self . primary_store . as_ref ( ) ,
848+ BACKUP_SYNC_PRIMARY_NAMESPACE ,
849+ "" ,
850+ PRIMARY_SYNC_GENERATION_KEY ,
851+ generation. to_vec ( ) ,
852+ )
853+ . await
854+ }
855+
856+ /// Reads the generation for which the configured backup last completed synchronization.
857+ ///
858+ /// The completion record is proof that all durable primary data was copied and stale backup data
859+ /// was removed for that generation. Its absence therefore means synchronization is required; it
860+ /// must not be inferred from individual values already present in the backup.
861+ ///
862+ /// Returns [`io::ErrorKind::NotFound`] when no backup is configured or the configured backup has
863+ /// no completion record. Other storage errors and malformed completion records are propagated so
864+ /// callers cannot mistake an unreadable record for proof that the backup is current.
865+ async fn read_backup_sync_completion ( & self ) -> io:: Result < [ u8 ; BACKUP_SYNC_GENERATION_LEN ] > {
866+ let backup_store = self . backup_store . as_ref ( ) . ok_or_else ( || {
867+ io:: Error :: new ( io:: ErrorKind :: NotFound , "Backup store is not configured" )
868+ } ) ?;
869+ Self :: read_backup_sync_generation ( backup_store. as_ref ( ) , BACKUP_SYNC_COMPLETION_KEY ) . await
870+ }
871+
872+ /// Reads and validates a synchronization generation from the given store metadata key.
873+ ///
874+ /// Primary generations and backup completion generations use the same opaque, fixed-width value
875+ /// representation but live under different keys. This helper centralizes the shared storage
876+ /// location and length validation without assigning ordering semantics to the random bytes.
877+ ///
878+ /// Returns [`io::ErrorKind::NotFound`] when the key is absent and
879+ /// [`io::ErrorKind::InvalidData`] when its value is not exactly
880+ /// [`BACKUP_SYNC_GENERATION_LEN`] bytes. All other underlying storage errors are propagated.
881+ async fn read_backup_sync_generation (
882+ store : & DynStore , key : & str ,
883+ ) -> io:: Result < [ u8 ; BACKUP_SYNC_GENERATION_LEN ] > {
884+ let generation = KVStore :: read ( store, BACKUP_SYNC_PRIMARY_NAMESPACE , "" , key) . await ?;
885+ generation. try_into ( ) . map_err ( |_| {
886+ io:: Error :: new (
887+ io:: ErrorKind :: InvalidData ,
888+ "Invalid tier-store backup synchronization generation" ,
889+ )
890+ } )
891+ }
892+
893+ #[ cfg( test) ]
894+ async fn write_backup_sync_completion (
895+ & self , generation : [ u8 ; BACKUP_SYNC_GENERATION_LEN ] ,
896+ ) -> io:: Result < ( ) > {
897+ let backup_store = self . backup_store . as_ref ( ) . ok_or_else ( || {
898+ io:: Error :: new ( io:: ErrorKind :: NotFound , "Backup store is not configured" )
899+ } ) ?;
900+ KVStore :: write (
901+ backup_store. as_ref ( ) ,
902+ BACKUP_SYNC_PRIMARY_NAMESPACE ,
903+ "" ,
904+ BACKUP_SYNC_COMPLETION_KEY ,
905+ generation. to_vec ( ) ,
906+ )
907+ . await
908+ }
909+
734910 fn get_new_version_and_lock_ref ( & self , locking_key : String ) -> ( Arc < TokioMutex < u64 > > , u64 ) {
735911 let version = self . next_write_version . fetch_add ( 1 , Ordering :: Relaxed ) ;
736912 if version == u64:: MAX {
@@ -1616,6 +1792,121 @@ mod tests {
16161792 tier. set_index_store ( TierStoreIndex :: from_store ( store) ) ;
16171793 }
16181794
1795+ #[ tokio:: test]
1796+ async fn backup_sync_metadata_remains_absent_when_a_backup_has_never_been_configured ( ) {
1797+ let base_dir = random_storage_path ( ) ;
1798+ let log_path = base_dir. join ( "tier_store_test.log" ) . to_string_lossy ( ) . into_owned ( ) ;
1799+ let logger = Arc :: new ( Logger :: new_fs_writer ( log_path, Level :: Trace ) . unwrap ( ) ) ;
1800+ let _cleanup = CleanupDir ( base_dir) ;
1801+ let primary_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1802+
1803+ let tier = setup_tier_store ( Arc :: clone ( & primary_store) , Arc :: clone ( & logger) ) ;
1804+ assert_eq ! (
1805+ tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1806+ BackupSyncStatus :: NotConfigured
1807+ ) ;
1808+ drop ( tier) ;
1809+ let restarted_tier = setup_tier_store ( Arc :: clone ( & primary_store) , logger) ;
1810+ assert_eq ! (
1811+ restarted_tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1812+ BackupSyncStatus :: NotConfigured
1813+ ) ;
1814+ let error = TierStoreInner :: read_backup_sync_generation (
1815+ primary_store. as_ref ( ) ,
1816+ PRIMARY_SYNC_GENERATION_KEY ,
1817+ )
1818+ . await
1819+ . unwrap_err ( ) ;
1820+ assert_eq ! ( error. kind( ) , io:: ErrorKind :: NotFound ) ;
1821+ }
1822+
1823+ #[ tokio:: test]
1824+ async fn backup_sync_generation_rotates_after_backup_is_removed ( ) {
1825+ let base_dir = random_storage_path ( ) ;
1826+ let log_path = base_dir. join ( "tier_store_test.log" ) . to_string_lossy ( ) . into_owned ( ) ;
1827+ let logger = Arc :: new ( Logger :: new_fs_writer ( log_path, Level :: Trace ) . unwrap ( ) ) ;
1828+ let _cleanup = CleanupDir ( base_dir) ;
1829+ let primary_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1830+ let backup_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1831+
1832+ let mut tier = setup_tier_store ( Arc :: clone ( & primary_store) , Arc :: clone ( & logger) ) ;
1833+ tier. set_backup_store ( backup_store) ;
1834+ assert_eq ! (
1835+ tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1836+ BackupSyncStatus :: Required
1837+ ) ;
1838+ let configured_generation = TierStoreInner :: read_backup_sync_generation (
1839+ primary_store. as_ref ( ) ,
1840+ PRIMARY_SYNC_GENERATION_KEY ,
1841+ )
1842+ . await
1843+ . unwrap ( ) ;
1844+ drop ( tier) ;
1845+
1846+ let restarted_tier = setup_tier_store ( Arc :: clone ( & primary_store) , logger) ;
1847+ assert_eq ! (
1848+ restarted_tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1849+ BackupSyncStatus :: NotConfigured
1850+ ) ;
1851+ let rotated_generation = TierStoreInner :: read_backup_sync_generation (
1852+ primary_store. as_ref ( ) ,
1853+ PRIMARY_SYNC_GENERATION_KEY ,
1854+ )
1855+ . await
1856+ . unwrap ( ) ;
1857+
1858+ assert_ne ! ( rotated_generation, configured_generation) ;
1859+ }
1860+
1861+ #[ tokio:: test]
1862+ async fn backup_sync_status_tracks_persisted_completion ( ) {
1863+ let base_dir = random_storage_path ( ) ;
1864+ let log_path = base_dir. join ( "tier_store_test.log" ) . to_string_lossy ( ) . into_owned ( ) ;
1865+ let logger = Arc :: new ( Logger :: new_fs_writer ( log_path, Level :: Trace ) . unwrap ( ) ) ;
1866+ let _cleanup = CleanupDir ( base_dir) ;
1867+ let primary_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1868+ let backup_store: Arc < DynStore > = Arc :: new ( DynStoreWrapper ( InMemoryStore :: new ( ) ) ) ;
1869+
1870+ let mut tier = setup_tier_store ( Arc :: clone ( & primary_store) , Arc :: clone ( & logger) ) ;
1871+ tier. set_backup_store ( Arc :: clone ( & backup_store) ) ;
1872+ assert_eq ! (
1873+ tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1874+ BackupSyncStatus :: Required
1875+ ) ;
1876+ let generation = TierStoreInner :: read_backup_sync_generation (
1877+ primary_store. as_ref ( ) ,
1878+ PRIMARY_SYNC_GENERATION_KEY ,
1879+ )
1880+ . await
1881+ . unwrap ( ) ;
1882+
1883+ let mut stale_generation = generation;
1884+ stale_generation[ 0 ] ^= 1 ;
1885+ tier. inner . write_backup_sync_completion ( stale_generation) . await . unwrap ( ) ;
1886+ assert_eq ! (
1887+ tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1888+ BackupSyncStatus :: Required
1889+ ) ;
1890+
1891+ tier. inner . write_backup_sync_completion ( generation) . await . unwrap ( ) ;
1892+ drop ( tier) ;
1893+ let mut restarted_tier = setup_tier_store ( Arc :: clone ( & primary_store) , logger) ;
1894+ restarted_tier. set_backup_store ( backup_store) ;
1895+ assert_eq ! (
1896+ restarted_tier. initialize_backup_synchronization( ) . await . unwrap( ) ,
1897+ BackupSyncStatus :: Synchronized
1898+ ) ;
1899+ assert_eq ! (
1900+ TierStoreInner :: read_backup_sync_generation(
1901+ primary_store. as_ref( ) ,
1902+ PRIMARY_SYNC_GENERATION_KEY ,
1903+ )
1904+ . await
1905+ . unwrap( ) ,
1906+ generation
1907+ ) ;
1908+ }
1909+
16191910 #[ test]
16201911 fn journal_entry_roundtrips ( ) {
16211912 for operation in [
0 commit comments