From 6953e3acde0c3ea4eac653182706de62e19096da Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Sun, 5 Jul 2026 18:10:41 +0200 Subject: [PATCH] feat: report store durability status --- crates/geth-cli/src/lib.rs | 8 ++++ crates/geth-control/src/lib.rs | 6 +++ crates/geth-node/src/lib.rs | 43 +++++++++++++----- crates/geth-store/src/lib.rs | 66 +++++++++++++++++++++++++++- crates/geth/tests/bootstrap.rs | 4 ++ docs/architecture.md | 7 ++- docs/production-readiness-roadmap.md | 8 ++-- 7 files changed, 124 insertions(+), 18 deletions(-) diff --git a/crates/geth-cli/src/lib.rs b/crates/geth-cli/src/lib.rs index 312c31f..5a389a0 100644 --- a/crates/geth-cli/src/lib.rs +++ b/crates/geth-cli/src/lib.rs @@ -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") diff --git a/crates/geth-control/src/lib.rs b/crates/geth-control/src/lib.rs index 4eeb661..0116be3 100644 --- a/crates/geth-control/src/lib.rs +++ b/crates/geth-control/src/lib.rs @@ -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, pub iroh_relay_mode: String, diff --git a/crates/geth-node/src/lib.rs b/crates/geth-node/src/lib.rs index ef2aee5..1d4cb95 100644 --- a/crates/geth-node/src/lib.rs +++ b/crates/geth-node/src/lib.rs @@ -5868,18 +5868,37 @@ pub fn handle_request( ) -> Result { let store = Store::open(&node.paths.metadata_db())?; match request { - ControlRequest::Status => 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(), - 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::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(), diff --git a/crates/geth-store/src/lib.rs b/crates/geth-store/src/lib.rs index 660bf1e..767df97 100644 --- a/crates/geth-store/src/lib.rs +++ b/crates/geth-store/src/lib.rs @@ -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 { 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 { 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 { + pub fn schema_version(&self) -> Result { let version = self .conn .query_row( @@ -75,6 +100,26 @@ impl Store { .map_err(|_| StoreError::InvalidSchemaVersion(version)) } + pub fn journal_mode(&self) -> Result { + 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 { + 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 diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs index c40976e..a4bb33c 100644 --- a/crates/geth/tests/bootstrap.rs +++ b/crates/geth/tests/bootstrap.rs @@ -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")); diff --git a/docs/architecture.md b/docs/architecture.md index 3cc298c..79407b0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/docs/production-readiness-roadmap.md b/docs/production-readiness-roadmap.md index 60f3f7b..7aece99 100644 --- a/docs/production-readiness-roadmap.md +++ b/docs/production-readiness-roadmap.md @@ -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