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!("socket: {}", status.socket.display());
println!("agent: {}", status.agent_id); println!("agent: {}", status.agent_id);
println!("node: {}", status.node_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!( println!(
"endpoint: {}", "endpoint: {}",
status.endpoint_id.as_deref().unwrap_or("not started") status.endpoint_id.as_deref().unwrap_or("not started")

View file

@ -1156,6 +1156,12 @@ pub struct StatusResponse {
pub socket: PathBuf, pub socket: PathBuf,
pub agent_id: String, pub agent_id: String,
pub node_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 iroh_enabled: bool,
pub endpoint_id: Option<String>, pub endpoint_id: Option<String>,
pub iroh_relay_mode: String, pub iroh_relay_mode: String,

View file

@ -5868,18 +5868,37 @@ pub fn handle_request(
) -> Result<ControlResponse, NodeError> { ) -> Result<ControlResponse, NodeError> {
let store = Store::open(&node.paths.metadata_db())?; let store = Store::open(&node.paths.metadata_db())?;
match request { 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(), home: node.paths.home().to_path_buf(),
socket: node.paths.socket_path(), socket: node.paths.socket_path(),
agent_id: node.agent_id.clone(), agent_id: node.agent_id.clone(),
node_id: node.node_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, iroh_enabled: node.iroh_status.enabled,
endpoint_id: node.iroh_status.endpoint_id.clone(), endpoint_id: node.iroh_status.endpoint_id.clone(),
iroh_relay_mode: node.iroh_status.relay_mode.clone(), iroh_relay_mode: node.iroh_status.relay_mode.clone(),
iroh_local_discovery: node.iroh_status.local_discovery, iroh_local_discovery: node.iroh_status.local_discovery,
iroh: node.iroh_status.note.clone(), iroh: node.iroh_status.note.clone(),
native_backends: native_backend_statuses(), native_backends: native_backend_statuses(),
})), }))
}
ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse { ControlRequest::NodeId => Ok(ControlResponse::NodeId(NodeIdResponse {
agent_id: node.agent_id.clone(), agent_id: node.agent_id.clone(),
node_id: node.node_id.clone(), node_id: node.node_id.clone(),

View file

@ -2,6 +2,8 @@ use rusqlite::{Connection, OptionalExtension, Transaction, params};
use std::path::Path; use std::path::Path;
pub const CURRENT_SCHEMA_VERSION: i64 = 2; pub const CURRENT_SCHEMA_VERSION: i64 = 2;
pub const FILE_JOURNAL_MODE: &str = "wal";
pub const SYNCHRONOUS_MODE: &str = "normal";
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum StoreError { pub enum StoreError {
@ -17,9 +19,31 @@ pub struct Store {
conn: Connection, 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 { impl Store {
pub fn open(path: &Path) -> Result<Self, StoreError> { pub fn open(path: &Path) -> Result<Self, StoreError> {
let conn = Connection::open(path)?; let conn = Connection::open(path)?;
configure_file_connection(&conn)?;
let store = Self { conn }; let store = Self { conn };
store.migrate()?; store.migrate()?;
Ok(store) Ok(store)
@ -27,6 +51,7 @@ impl Store {
pub fn open_memory() -> Result<Self, StoreError> { pub fn open_memory() -> Result<Self, StoreError> {
let conn = Connection::open_in_memory()?; let conn = Connection::open_in_memory()?;
configure_memory_connection(&conn)?;
let store = Self { conn }; let store = Self { conn };
store.migrate()?; store.migrate()?;
Ok(store) Ok(store)
@ -58,7 +83,7 @@ impl Store {
Ok(()) Ok(())
} }
fn schema_version(&self) -> Result<i64, StoreError> { pub fn schema_version(&self) -> Result<i64, StoreError> {
let version = self let version = self
.conn .conn
.query_row( .query_row(
@ -75,6 +100,26 @@ impl Store {
.map_err(|_| StoreError::InvalidSchemaVersion(version)) .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> { fn apply_schema_v1(&self) -> Result<(), StoreError> {
let tx = self.conn.unchecked_transaction()?; let tx = self.conn.unchecked_transaction()?;
tx.execute_batch( tx.execute_batch(
@ -1817,6 +1862,25 @@ mod tests {
let _ = std::fs::remove_file(path); 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 { fn column_exists(store: &Store, table: &str, column: &str) -> bool {
let mut stmt = store let mut stmt = store
.conn .conn

View file

@ -268,6 +268,10 @@ fn geth_status_against_running_daemon() {
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("geth daemon: running")); assert!(stdout.contains("geth daemon: running"));
assert!(stdout.contains("agent:")); 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("endpoint:"));
assert!(stdout.contains("iroh relay: disabled")); assert!(stdout.contains("iroh relay: disabled"));
assert!(stdout.contains("iroh discovery: local-network 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 `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 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 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 management is also exposed through the single binary. `geth daemon
service ...` installs and controls a user-level service definition for the local 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. - `[ ]` Backup output avoids copying private SSH admin keys.
- `[ ]` Docs explain what is and is not included. - `[ ]` Docs explain what is and is not included.
- `[ ]` Document database durability settings. - `[x]` Document database durability settings.
Acceptance criteria: Acceptance criteria:
- `[ ]` WAL and synchronous settings are chosen deliberately. - `[x]` WAL and synchronous settings are chosen deliberately.
- `[ ]` Crash-recovery expectations are documented. - `[x]` Crash-recovery expectations are documented.
- `[ ]` `geth doctor` or status output reports obvious store issues. - `[x]` `geth doctor` or status output reports obvious store issues.
## Phase 4: Security Boundary Closure ## Phase 4: Security Boundary Closure