Initialize SQLite databases with DELETE journaling on NFS (#56552)

## What changed

Choose `DELETE` journaling for new SQLite databases on NFS and `WAL` elsewhere, using the database's own directory to detect the filesystem. Probe a separately configured logs directory independently of the state directory.

Preserve journal modes for databases with an existing schema, and apply `synchronous = EXTRA` when initialization results in `DELETE` mode.

## Testing

Extend SQLite tests to cover NFS and non-NFS initialization, interrupted initialization, lazy pool connections, separate logs directories, and preservation of existing settings under a write lock.

GitOrigin-RevId: bde35861e517881b9ce23b28b199475cee70bd76
This commit is contained in:
Dmitriy Kovalenko
2026-10-23 18:30:44 +00:00
committed by copyberry
parent f970f4be67
commit 0d7f3ea03b
2 changed files with 136 additions and 26 deletions
+43 -7
View File
@@ -1,4 +1,5 @@
//! Shared SQLite connection configuration.
//! New databases use DELETE on NFS and WAL elsewhere.
//! Writable pools preserve the journal and vacuum modes of initialized databases.
//! Startup retries SQLITE_BUSY and reports other errors to callers.
//! The configured logs directory is created and locked only when opening its database.
@@ -416,6 +417,22 @@ impl SqliteConfig {
telemetry_override: Option<&dyn DbTelemetry>,
) -> anyhow::Result<SqlitePool> {
self.prepare_logs_directory(path).await?;
let directory = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let is_nfs = if self.local_logs_dir.is_some() && directory == self.logs_home() {
self.logs_are_on_nfs().await?
} else if directory == self.home() {
self.is_nfs().await?
} else {
probe_nfs(directory, &OnceCell::new()).await?
};
let journal_mode_policy = if is_nfs {
JournalModePolicy::InitializeDelete
} else {
JournalModePolicy::InitializeWal
};
let recovery = recover_spec.map_or(RecoveryMode::Unavailable, |spec| spec.recovery);
let options = SqliteConnectOptions::new()
.filename(path)
@@ -424,7 +441,11 @@ impl SqliteConfig {
.busy_timeout(DEFAULT_BUSY_TIMEOUT)
.log_statements(LevelFilter::Off);
// Each failed attempt closes its pool before initialization is retried.
let connect = || async { connect_once(&options).await.map_err(anyhow::Error::from) };
let connect = || async {
connect_once(&options, journal_mode_policy)
.await
.map_err(anyhow::Error::from)
};
let pool = retry_on_busy(connect).await?;
let validation_result = self
.quick_check_manager
@@ -518,7 +539,16 @@ impl SqliteConfig {
}
}
async fn connect_once(options: &SqliteConnectOptions) -> Result<SqlitePool, Error> {
#[derive(Clone, Copy, Eq, PartialEq)]
enum JournalModePolicy {
InitializeWal,
InitializeDelete,
}
async fn connect_once(
options: &SqliteConnectOptions,
journal_mode_policy: JournalModePolicy,
) -> Result<SqlitePool, Error> {
// SQLx retries after_connect errors, eventually replacing them with PoolTimedOut.
// Return the first initialization error directly while opening this pool.
let (init_error_tx, mut init_error_rx) = tokio::sync::mpsc::channel(/*buffer*/ 1);
@@ -575,12 +605,14 @@ async fn connect_once(options: &SqliteConnectOptions) -> Result<SqlitePool, Erro
.execute(&mut *connection)
.await?;
}
sqlx::query("PRAGMA journal_mode = WAL")
.execute(&mut *connection)
.await?;
let pragma = match journal_mode_policy {
JournalModePolicy::InitializeWal => "PRAGMA journal_mode = WAL",
JournalModePolicy::InitializeDelete => "PRAGMA journal_mode = DELETE",
};
sqlx::query(pragma).execute(&mut *connection).await?;
}
// Another process can initialize WAL after this connection selects EXTRA.
// Apply NORMAL to WAL even if that process already created the schema.
// Initialization or another process can change the journal mode after after_connect.
// Match this connection's synchronization setting to the resulting journal mode.
let journal: String = sqlx::query_scalar("PRAGMA journal_mode")
.fetch_one(&mut *connection)
.await?;
@@ -588,6 +620,10 @@ async fn connect_once(options: &SqliteConnectOptions) -> Result<SqlitePool, Erro
sqlx::query("PRAGMA synchronous = NORMAL")
.execute(&mut *connection)
.await?;
} else if journal == "delete" {
sqlx::query("PRAGMA synchronous = EXTRA")
.execute(&mut *connection)
.await?;
}
// Wait for the connection to return to the pool before startup requests it again.
// An asynchronous drop can otherwise cause the pool to open a second connection.