@@ -64,6 +64,8 @@ pub enum SyncError {
6464 PullDb ( StatusCode , String ) ,
6565 #[ error( "server returned a lower generation than local: local={0}, remote={1}" ) ]
6666 InvalidLocalGeneration ( u32 , u32 ) ,
67+ #[ error( "invalid local state: {0}" ) ]
68+ InvalidLocalState ( String ) ,
6769}
6870
6971impl SyncError {
@@ -509,27 +511,63 @@ impl SyncContext {
509511 }
510512
511513 async fn sync_db_if_needed ( & mut self , generation : u32 ) -> Result < ( ) > {
512- // we will get the export file only if the remote generation is different from the one we have
513- if generation == self . durable_generation {
514- return Ok ( ( ) ) ;
515- }
516514 // somehow we are ahead of the remote in generations. following should not happen because
517515 // we checkpoint only if the remote server tells us to do so.
518516 if self . durable_generation > generation {
519517 tracing:: error!(
520- "server returned a lower generation than what we have: sent ={}, got ={}" ,
518+ "server returned a lower generation than what we have: local ={}, remote ={}" ,
521519 self . durable_generation,
522520 generation
523521 ) ;
524522 return Err (
525523 SyncError :: InvalidLocalGeneration ( self . durable_generation , generation) . into ( ) ,
526524 ) ;
527525 }
528- tracing:: debug!(
529- "syncing db file from remote server, generation={}" ,
530- generation
531- ) ;
532- self . sync_db ( generation) . await
526+ // we use the following heuristic to determine if we need to sync the db file
527+ // 1. if no db file or the metadata file exists, then user is starting from scratch
528+ // and we will do the sync
529+ // 2. if the db file exists, but the metadata file does not exist (or other way around),
530+ // then local db is in an incorrect state. we stop and return with an error
531+ // 3. if the db file exists and the metadata file exists, then we don't need to do the
532+ // sync
533+ let metadata_exists = check_if_file_exists ( & format ! ( "{}-info" , self . db_path) ) ?;
534+ let db_file_exists = check_if_file_exists ( & self . db_path ) ?;
535+ match ( metadata_exists, db_file_exists) {
536+ ( false , false ) => {
537+ // neither the db file nor the metadata file exists, lets bootstrap from remote
538+ tracing:: debug!(
539+ "syncing db file from remote server, generation={}" ,
540+ generation
541+ ) ;
542+ self . sync_db ( generation) . await
543+ }
544+ ( false , true ) => {
545+ // kinda inconsistent state: DB exists but metadata missing
546+ // however, this generally not an issue. For a fresh db, a user might do writes
547+ // locally and then try to do sync later. So in this case, we will not
548+ // bootstrap the db file and let the user proceed. If it is not a fresh db, the
549+ // push will fail anyways later.
550+ // if metadata file does not exist, then generation should be zero
551+ assert_eq ! ( self . durable_generation, 0 ) ;
552+ // lets initialise it to first generation
553+ self . durable_generation = 1 ;
554+ Ok ( ( ) )
555+ }
556+ ( true , false ) => {
557+ // inconsistent state: Metadata exists but DB missing
558+ tracing:: error!(
559+ "local state is incorrect, metadata file exists but db file does not"
560+ ) ;
561+ Err ( SyncError :: InvalidLocalState (
562+ "metadata file exists but db file does not" . to_string ( ) ,
563+ )
564+ . into ( ) )
565+ }
566+ ( true , true ) => {
567+ // both files exists, no need to sync
568+ Ok ( ( ) )
569+ }
570+ }
533571 }
534572
535573 /// sync_db will download the db file from the remote server and replace the local file.
@@ -646,6 +684,28 @@ async fn atomic_write<P: AsRef<Path>>(path: P, data: &[u8]) -> Result<()> {
646684 Ok ( ( ) )
647685}
648686
687+ /// bootstrap_db brings the .db file from remote, if required. If the .db file already exists, then
688+ /// it does nothing. Calling this function multiple times is safe.
689+ pub async fn bootstrap_db ( sync_ctx : & mut SyncContext ) -> Result < ( ) > {
690+ // todo: we are checking with the remote server only during initialisation. ideally,
691+ // we need to do this when we notice a large gap in generations, when bootstrapping is cheaper
692+ // than pulling each frame
693+ if !sync_ctx. initial_server_sync {
694+ // sync is being called first time. so we will call remote, get the generation information
695+ // if we are lagging behind, then we will call the export API and get to the latest
696+ // generation directly.
697+ let info = sync_ctx. get_remote_info ( ) . await ?;
698+ sync_ctx
699+ . sync_db_if_needed ( info. current_generation )
700+ . await ?;
701+ // when sync_ctx is initialised, we set durable_generation to 0. however, once
702+ // sync_db is called, it should be > 0.
703+ assert ! ( sync_ctx. durable_generation > 0 , "generation should be > 0" ) ;
704+ sync_ctx. initial_server_sync = true ;
705+ }
706+ Ok ( ( ) )
707+ }
708+
649709/// Sync WAL frames to remote.
650710pub async fn sync_offline (
651711 sync_ctx : & mut SyncContext ,
@@ -667,22 +727,6 @@ pub async fn sync_offline(
667727 Err ( e) => Err ( e) ,
668728 }
669729 } else {
670- // todo: we are checking with the remote server only during initialisation. ideally,
671- // we should check everytime we try to sync with the remote server. However, we need to close
672- // all the ongoing connections since we replace `.db` file and remove the `.db-wal` file
673- if !sync_ctx. initial_server_sync {
674- // sync is being called first time. so we will call remote, get the generation information
675- // if we are lagging behind, then we will call the export API and get to the latest
676- // generation directly.
677- let info = sync_ctx. get_remote_info ( ) . await ?;
678- sync_ctx
679- . sync_db_if_needed ( info. current_generation )
680- . await ?;
681- // when sync_ctx is initialised, we set durable_generation to 0. however, once
682- // sync_db is called, it should be > 0.
683- assert ! ( sync_ctx. durable_generation > 0 , "generation should be > 0" ) ;
684- sync_ctx. initial_server_sync = true ;
685- }
686730 try_pull ( sync_ctx, conn) . await
687731 }
688732 . or_else ( |err| {
@@ -831,3 +875,9 @@ async fn try_pull(
831875 } )
832876 }
833877}
878+
879+ fn check_if_file_exists ( path : & str ) -> core:: result:: Result < bool , SyncError > {
880+ Path :: new ( & path)
881+ . try_exists ( )
882+ . map_err ( SyncError :: io ( "metadata file exists" ) )
883+ }
0 commit comments