Skip the blob activation disk preflight when no mount matches the database

available_space_for_path returning None aborted startup with a fatal
UnknownFilesystem error, even on a fresh install with zero legacy rows.
Hosts with tmpfs or squashfs roots, network-filesystem data paths, or an
unreadable mount table would fail every boot with no operator override,
while the resource sampler already treats the identical condition as
benign (supported: false) and keeps running.

The preflight now logs a warning and is skipped when free space cannot
be determined; the import, verification, and integrity checks still run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Scott Werner 2026-08-23 13:11:27 -04:00
parent bccc5750a4
commit e1abecc9f4
2 changed files with 40 additions and 31 deletions

View file

@ -59,8 +59,10 @@ On startup, Fabro activates SQLite as the only live content-addressed blob
store before it opens routes, schedulers, workers, webhooks, reapers, or the
ready callback. The activation inventories the exact legacy SlateDB blob
prefix, checks disk headroom sized to the rows not yet imported (a warm
restart with nothing left to import only needs a small fixed headroom),
imports in bounded transactions, compares every legacy blob byte-for-byte
restart with nothing left to import only needs a small fixed headroom; on
filesystems whose free space cannot be determined the check is skipped with
a warning), imports in bounded transactions, compares every legacy blob
byte-for-byte
with SQLite, runs a live SQLite integrity check, and completes a final WAL
checkpoint. Boots that import new rows additionally re-verify every legacy
blob against SQLite and validate every SQLite blob row independently. Any

View file

@ -16,7 +16,7 @@ use sqlx::Connection as _;
use sqlx::sqlite::{SqliteConnectOptions, SqliteConnection};
use tokio::fs;
use tokio::task::{JoinError, spawn_blocking};
use tracing::{debug, info};
use tracing::{debug, info, warn};
use crate::server::resource_sampler;
@ -52,8 +52,6 @@ pub(crate) enum BlobActivationError {
},
#[error("activation backup integrity check did not return exactly one ok result at {path}")]
BackupIntegrityFailed { path: PathBuf },
#[error("no filesystem mount matched the SQLite database path {path}")]
UnknownFilesystem { path: PathBuf },
#[error("reading SQLite file metadata at {path}")]
SqliteMetadata {
path: PathBuf,
@ -150,33 +148,42 @@ pub(crate) async fn activate_blob_storage(
validate_backup(&backup_path).await?;
}
let backup_required = inventory.rows > 0 && !backup_exists;
let available_free_bytes = resource_sampler::available_space_for_path(&canonical_path)
.ok_or_else(|| BlobActivationError::UnknownFilesystem {
path: canonical_path.clone(),
})?;
let backup_reserve = if backup_required {
sqlite_file_set_bytes(&canonical_path).await?
// The resource sampler treats a path with no matching mount as an
// unsupported-but-benign condition (tmpfs or squashfs roots, network
// filesystems, an unreadable mount table), so the preflight does too:
// skipping the capacity check must not block a boot the import itself
// could complete.
if let Some(available_free_bytes) = resource_sampler::available_space_for_path(&canonical_path)
{
let backup_reserve = if backup_required {
sqlite_file_set_bytes(&canonical_path).await?
} else {
0
};
// Only the rows the import still has to copy need new space; rows
// already present in SQLite cost nothing on a warm restart.
let required_free_bytes = compute_disk_preflight(
inventory.pending_bytes,
backup_reserve,
available_free_bytes,
)?;
debug!(
legacy_rows = inventory.rows,
legacy_bytes = inventory.bytes,
pending_rows = inventory.pending_rows,
pending_bytes = inventory.pending_bytes,
backup_required,
backup_reserve,
required_free_bytes,
available_free_bytes,
"Checked SQLite blob activation disk capacity"
);
} else {
0
};
// Only the rows the import still has to copy need new space; rows already
// present in SQLite cost nothing on a warm restart.
let required_free_bytes = compute_disk_preflight(
inventory.pending_bytes,
backup_reserve,
available_free_bytes,
)?;
debug!(
legacy_rows = inventory.rows,
legacy_bytes = inventory.bytes,
pending_rows = inventory.pending_rows,
pending_bytes = inventory.pending_bytes,
backup_required,
backup_reserve,
required_free_bytes,
available_free_bytes,
"Checked SQLite blob activation disk capacity"
);
warn!(
database_path = %canonical_path.display(),
"No filesystem mount matched the SQLite database path; skipping the blob activation disk preflight"
);
}
let retained_backup = if backup_exists {
Some(backup_path)