Skip to content

Commit bc05e5a

Browse files
committed
Floor the runtime workers and make bottomless restore never destroy local state
1 parent 8b1bb47 commit bc05e5a

2 files changed

Lines changed: 63 additions & 21 deletions

File tree

bottomless/src/replicator.rs

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1306,17 +1306,14 @@ impl Replicator {
13061306
}
13071307

13081308
// Returns the number of pages stored in the local WAL file, or 0, if there aren't any.
1309-
async fn get_local_wal_page_count(&mut self) -> u32 {
1310-
match WalFileReader::open(&format!("{}-wal", &self.db_path)).await {
1311-
Ok(None) => 0,
1312-
Ok(Some(wal)) => {
1309+
async fn get_local_wal_page_count(&mut self) -> Result<u32> {
1310+
match WalFileReader::open(&format!("{}-wal", &self.db_path)).await? {
1311+
None => Ok(0),
1312+
Some(wal) => {
13131313
let page_size = wal.page_size();
1314-
if self.set_page_size(page_size as usize).is_err() {
1315-
return 0;
1316-
}
1317-
wal.frame_count().await
1314+
self.set_page_size(page_size as usize)?;
1315+
Ok(wal.frame_count().await)
13181316
}
1319-
Err(_) => 0,
13201317
}
13211318
}
13221319

@@ -1381,8 +1378,10 @@ impl Replicator {
13811378
.await
13821379
{
13831380
Ok(result) => {
1381+
// Move any existing local state aside instead of overwriting or
1382+
// deleting it, so a wrong restore decision stays recoverable.
1383+
self.preserve_local_state_aside().await;
13841384
tokio::fs::rename(&restore_path, &self.db_path).await?;
1385-
let _ = self.remove_wal_files().await; // best effort, WAL files may not exists
13861385

13871386
let elapsed = Instant::now() - start_ts;
13881387
tracing::info!("Finished database restoration in {:?}", elapsed);
@@ -1496,18 +1495,29 @@ impl Replicator {
14961495
last_consistent_frame: u32,
14971496
) -> Result<Option<RestoreAction>> {
14981497
// Check if the database needs to be restored by inspecting the database
1499-
// change counter and the WAL size.
1500-
let local_counter = self.read_change_counter().unwrap_or([0u8; 4]);
1498+
// change counter and the WAL size. A read error must never be taken as
1499+
// an empty database: restoring over unreadable-but-present local state
1500+
// destroys acknowledged writes.
1501+
let local_counter = tokio::task::block_in_place(|| self.read_change_counter())?;
15011502
if local_counter != [0u8; 4] && local_counter != [0, 0, 0, 1] {
15021503
// if a non-empty database file exists always treat it as new and more up to date,
15031504
// skipping the restoration process and calling for a new generation to be made
15041505
return Ok(Some(RestoreAction::SnapshotMainDbFile));
15051506
}
15061507

1508+
let wal_pages = self.get_local_wal_page_count().await?;
1509+
if wal_pages > 0 {
1510+
// A fresh-looking main file with committed WAL frames is local data
1511+
// (the change counter only moves on checkpoint), not an empty db.
1512+
tracing::info!(
1513+
"Local WAL holds {} committed pages; treating local state as authoritative",
1514+
wal_pages
1515+
);
1516+
return Ok(Some(RestoreAction::SnapshotMainDbFile));
1517+
}
1518+
15071519
let remote_counter = self.get_remote_change_counter(&generation).await?;
15081520
tracing::debug!("Counters: l={:?}, r={:?}", local_counter, remote_counter);
1509-
1510-
let wal_pages = self.get_local_wal_page_count().await;
15111521
// We impersonate as a given generation, since we're comparing against local backup at that
15121522
// generation. This is used later in [Self::new_generation] to create a dependency between
15131523
// this generation and a new one.
@@ -1745,11 +1755,25 @@ impl Replicator {
17451755
Ok(applied_wal_frame)
17461756
}
17471757

1748-
async fn remove_wal_files(&self) -> Result<()> {
1749-
tracing::debug!("Overwriting any existing WAL file: {}-wal", &self.db_path);
1750-
tokio::fs::remove_file(&format!("{}-wal", &self.db_path)).await?;
1751-
tokio::fs::remove_file(&format!("{}-shm", &self.db_path)).await?;
1752-
Ok(())
1758+
/// Move the local database, WAL, and shm files to `.pre-restore` siblings
1759+
/// (replacing any previous set) before a restore overwrites them. Best
1760+
/// effort: a rename failure is logged, never fatal, and never leaves the
1761+
/// restore blocked.
1762+
async fn preserve_local_state_aside(&self) {
1763+
for suffix in ["", "-wal", "-shm"] {
1764+
let src = format!("{}{}", &self.db_path, suffix);
1765+
if matches!(tokio::fs::try_exists(&src).await, Ok(true)) {
1766+
let dst = format!("{}.pre-restore{}", &self.db_path, suffix);
1767+
match tokio::fs::rename(&src, &dst).await {
1768+
Ok(()) => {
1769+
tracing::warn!("preserved local {} as {} before restore", src, dst)
1770+
}
1771+
Err(e) => {
1772+
tracing::error!("could not preserve {} before restore: {}", src, e)
1773+
}
1774+
}
1775+
}
1776+
}
17531777
}
17541778

17551779
pub async fn copy(&mut self, generation: Option<Uuid>, to_dir: String) -> Result<()> {

libsql-server/src/main.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -727,8 +727,26 @@ async fn build_server(
727727
})
728728
}
729729

730-
#[tokio::main]
731-
async fn main() -> Result<()> {
730+
fn main() -> Result<()> {
731+
// A low cpu cgroup limit yields as few as one runtime worker, and a single
732+
// blocking call then stalls every task, health responses included.
733+
let worker_threads = std::env::var("SQLD_RUNTIME_WORKER_THREADS")
734+
.ok()
735+
.and_then(|v| v.parse::<usize>().ok())
736+
.unwrap_or_else(|| {
737+
std::thread::available_parallelism()
738+
.map(|n| n.get())
739+
.unwrap_or(1)
740+
.max(4)
741+
});
742+
tokio::runtime::Builder::new_multi_thread()
743+
.worker_threads(worker_threads)
744+
.enable_all()
745+
.build()?
746+
.block_on(async_main())
747+
}
748+
749+
async fn async_main() -> Result<()> {
732750
let args = Cli::parse();
733751

734752
if std::env::var("RUST_LOG").is_err() {

0 commit comments

Comments
 (0)