feat: report store durability status

This commit is contained in:
Eric Wendland 2026-07-05 18:10:41 +02:00
commit 6953e3acde
7 changed files with 124 additions and 18 deletions

View file

@ -2210,6 +2210,14 @@ fn print_response(response: ControlResponse, json: bool) -> Result<()> {
println!("socket: {}", status.socket.display());
println!("agent: {}", status.agent_id);
println!("node: {}", status.node_id);
println!(
"store schema: {}/{}",
status.store_schema_version, status.store_current_schema_version
);
println!("store journal: {}", status.store_journal_mode);
println!("store synchronous: {}", status.store_synchronous);
println!("store status: {}", status.store_status);
println!("store note: {}", status.store_note);
println!(
"endpoint: {}",
status.endpoint_id.as_deref().unwrap_or("not started")

View file

@ -1156,6 +1156,12 @@ pub struct StatusResponse {
pub socket: PathBuf,
pub agent_id: String,
pub node_id: String,
pub store_schema_version: i64,
pub store_current_schema_version: i64,
pub store_journal_mode: String,
pub store_synchronous: String,
pub store_status: String,
pub store_note: String,
pub iroh_enabled: bool,
pub endpoint_id: Option<String>,
pub iroh_relay_mode: String,

View file

@ -5868,18 +5868,37 @@ pub fn handle_request(
) -> Result<ControlResponse, NodeError> {
let store = Store::open(&node.paths.metadata_db())?;
match request {
ControlRequest::Status => Ok(ControlResponse::Status(StatusResponse {
ControlRequest::Status => {
let store_schema_version = store.schema_version()?;
let store_journal_mode = store.journal_mode()?;
let store_synchronous = store.synchronous_mode()?;
let store_ok = store_schema_version == geth_store::CURRENT_SCHEMA_VERSION
&& store_journal_mode == geth_store::FILE_JOURNAL_MODE
&& store_synchronous == geth_store::SYNCHRONOUS_MODE;
Ok(ControlResponse::Status(StatusResponse {
home: node.paths.home().to_path_buf(),
socket: node.paths.socket_path(),
agent_id: node.agent_id.clone(),
node_id: node.node_id.clone(),
store_schema_version,
store_current_schema_version: geth_store::CURRENT_SCHEMA_VERSION,
store_journal_mode,
store_synchronous,
store_status: if store_ok { "ok" } else { "check" }.to_owned(),
store_note: format!(
"expected schema {}, journal {}, synchronous {}",
geth_store::CURRENT_SCHEMA_VERSION,
geth_store::FILE_JOURNAL_MODE,
geth_store::SYNCHRONOUS_MODE
),
iroh_enabled: node.iroh_status.enabled,
endpoint_id: node.iroh_status.endpoint_id.clone(),
iroh_relay_mode: node.iroh_status.relay_mode.clone(),
iroh_local_discovery: node.iroh_status.local_discovery,
iroh: node.iroh_status.note.clone(),
native_backends: native_backend_statuses(),
})),
}))
}
ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse {
agent_id: node.agent_id.clone(),
node_id: node.node_id.clone(),

View file

@ -2,6 +2,8 @@ use rusqlite::{Connection, OptionalExtension, Transaction, params};
use std::path::Path;
pub const CURRENT_SCHEMA_VERSION: i64 = 2;
pub const FILE_JOURNAL_MODE: &str = "wal";
pub const SYNCHRONOUS_MODE: &str = "normal";
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
@ -17,9 +19,31 @@ pub struct Store {
conn: Connection,
}
fn configure_file_connection(conn: &Connection) -> Result<(), StoreError> {
conn.execute_batch(
r#"
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
"#,
)?;
Ok(())
}
fn configure_memory_connection(conn: &Connection) -> Result<(), StoreError> {
conn.execute_batch(
r#"
PRAGMA synchronous = NORMAL;
PRAGMA foreign_keys = ON;
"#,
)?;
Ok(())
}
impl Store {
pub fn open(path: &Path) -> Result<Self, StoreError> {
let conn = Connection::open(path)?;
configure_file_connection(&conn)?;
let store = Self { conn };
store.migrate()?;
Ok(store)
@ -27,6 +51,7 @@ impl Store {
pub fn open_memory() -> Result<Self, StoreError> {
let conn = Connection::open_in_memory()?;
configure_memory_connection(&conn)?;
let store = Self { conn };
store.migrate()?;
Ok(store)
@ -58,7 +83,7 @@ impl Store {
Ok(())
}
fn schema_version(&self) -> Result<i64, StoreError> {
pub fn schema_version(&self) -> Result<i64, StoreError> {
let version = self
.conn
.query_row(
@ -75,6 +100,26 @@ impl Store {
.map_err(|_| StoreError::InvalidSchemaVersion(version))
}
pub fn journal_mode(&self) -> Result<String, StoreError> {
self.conn
.query_row("PRAGMA journal_mode", [], |row| row.get::<_, String>(0))
.map(|mode| mode.to_ascii_lowercase())
.map_err(StoreError::from)
}
pub fn synchronous_mode(&self) -> Result<String, StoreError> {
let mode = self
.conn
.query_row("PRAGMA synchronous", [], |row| row.get::<_, i64>(0))?;
Ok(match mode {
0 => "off".to_owned(),
1 => "normal".to_owned(),
2 => "full".to_owned(),
3 => "extra".to_owned(),
other => format!("unknown-{other}"),
})
}
fn apply_schema_v1(&self) -> Result<(), StoreError> {
let tx = self.conn.unchecked_transaction()?;
tx.execute_batch(
@ -1817,6 +1862,25 @@ mod tests {
let _ = std::fs::remove_file(path);
}
#[test]
fn file_store_uses_documented_durability_pragmas() {
let path = std::env::temp_dir().join(format!("geth-store-durability-{}.sqlite", now_ms()));
let store = Store::open(&path).expect("open file store");
assert_eq!(
store.journal_mode().expect("journal mode"),
FILE_JOURNAL_MODE
);
assert_eq!(
store.synchronous_mode().expect("synchronous mode"),
SYNCHRONOUS_MODE
);
drop(store);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(path.with_extension("sqlite-wal"));
let _ = std::fs::remove_file(path.with_extension("sqlite-shm"));
}
fn column_exists(store: &Store, table: &str, column: &str) -> bool {
let mut stmt = store
.conn

View file

@ -268,6 +268,10 @@ fn geth_status_against_running_daemon() {
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("geth daemon: running"));
assert!(stdout.contains("agent:"));
assert!(stdout.contains("store schema:"));
assert!(stdout.contains("store journal: wal"));
assert!(stdout.contains("store synchronous: normal"));
assert!(stdout.contains("store status: ok"));
assert!(stdout.contains("endpoint:"));
assert!(stdout.contains("iroh relay: disabled"));
assert!(stdout.contains("iroh discovery: local-network disabled"));

View file

@ -10,7 +10,12 @@ The local metadata store is SQLite product state. `geth-store` tracks a numeric
`schema_version` in the `meta` table and applies ordered migrations up to the
crate's current schema version when the store opens. Fresh database creation and
repeated opens are idempotent; migrations that change existing schemas run in a
SQLite transaction where SQLite supports it.
SQLite transaction where SQLite supports it. File-backed stores deliberately use
SQLite WAL mode with `synchronous=NORMAL`: committed transactions remain
consistent after process crashes, while the most recent transaction can be lost
on an OS crash or power loss before the WAL is durable. `geth status` reports
the observed schema version, journal mode, synchronous mode, and whether those
values match the expected store policy.
Service management is also exposed through the single binary. `geth daemon
service ...` installs and controls a user-level service definition for the local

View file

@ -142,11 +142,11 @@ it.
- `[ ]` Backup output avoids copying private SSH admin keys.
- `[ ]` Docs explain what is and is not included.
- `[ ]` Document database durability settings.
- `[x]` Document database durability settings.
Acceptance criteria:
- `[ ]` WAL and synchronous settings are chosen deliberately.
- `[ ]` Crash-recovery expectations are documented.
- `[ ]` `geth doctor` or status output reports obvious store issues.
- `[x]` WAL and synchronous settings are chosen deliberately.
- `[x]` Crash-recovery expectations are documented.
- `[x]` `geth doctor` or status output reports obvious store issues.
## Phase 4: Security Boundary Closure