Skip to content

Commit 361d082

Browse files
authored
Bootstrap database before opening connection (#2021)
This patch is a follow up on #2017 It changes the heuristics for bootstrapping the DB. One more change is, it also does bootstrap before any connection to the local SQLite is created Earlier, we bootstrapped whenever we noticed a change in the generation. However, this patch changes this behaviour and does bootstrap only if local files don't exist: 1. if no db file or the metadata file exists, then user is starting from scratch and we will do the sync 2. if the db file exists, but the metadata file does not exist (or other way around), then local db is in an incorrect state. we stop and return with an error 3. if the db file exists and the metadata file exists, then we don't need to do the sync
2 parents 483a150 + c55935a commit 361d082

2 files changed

Lines changed: 77 additions & 27 deletions

File tree

libsql/src/local/database.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,8 +465,8 @@ impl Database {
465465
/// Sync WAL frames to remote.
466466
pub async fn sync_offline(&self) -> Result<crate::database::Replicated> {
467467
let mut sync_ctx = self.sync_ctx.as_ref().unwrap().lock().await;
468+
crate::sync::bootstrap_db(&mut sync_ctx).await?;
468469
let conn = self.connect()?;
469-
470470
crate::sync::sync_offline(&mut sync_ctx, &conn).await
471471
}
472472

libsql/src/sync.rs

Lines changed: 76 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6971
impl 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.
650710
pub 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

Comments
 (0)