Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Make sqlite flags configurable, with NEW defaults
✗ CI failure Yorgos Saslis committed 2 months ago
commit dc4368ccc04009f2fc0667dbabc5b28e9c029c81
parent b04f487b3ae2efcd23f52f8138abe32e73a718fd
1 failed (1 total) View logs
5 files changed +191 -15
modified crates/radicle-cli/examples/rad-config.md
@@ -52,6 +52,12 @@ $ rad config
    "workers": 8,
    "seedingPolicy": {
      "default": "block"
+
    },
+
    "database": {
+
      "sqlite": {
+
        "journalMode": "WAL",
+
        "synchronous": "NORMAL"
+
      }
    }
  }
}
modified crates/radicle-node/src/runtime.rs
@@ -177,7 +177,8 @@ impl Runtime {
        log::info!(target: "node", "Opening node database..");
        let db = home
            .database_mut()?
-
            .journal_mode(node::db::JournalMode::default())?
+
            .journal_mode(config.database.sqlite.journal_mode)?
+
            .synchronous(config.database.sqlite.synchronous)?
            .init(
                &id,
                announcement.features,
modified crates/radicle/src/node/config.rs
@@ -349,6 +349,37 @@ pub enum Relay {
    Auto,
}

+
/// Database configuration.
+
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct DatabaseConfig {
+
    /// SQLite configuration.
+
    pub sqlite: SqliteConfig,
+
}
+

+
/// SQLite-specific database configuration.
+
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase")]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub struct SqliteConfig {
+
    /// Journal mode. WAL is recommended for concurrent access.
+
    #[serde(default)]
+
    pub journal_mode: node::db::JournalMode,
+
    /// Synchronous flag. Controls how often SQLite syncs to disk.
+
    #[serde(default)]
+
    pub synchronous: node::db::Synchronous,
+
}
+

+
impl Default for SqliteConfig {
+
    fn default() -> Self {
+
        Self {
+
            journal_mode: node::db::JournalMode::WAL,
+
            synchronous: node::db::Synchronous::NORMAL,
+
        }
+
    }
+
}
+

/// Proxy configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase", tag = "mode")]
@@ -508,6 +539,9 @@ pub struct Config {
    /// Default seeding policy.
    #[serde(default)]
    pub seeding_policy: DefaultSeedingPolicy,
+
    /// Database configuration.
+
    #[serde(default)]
+
    pub database: DatabaseConfig,
    /// Extra fields that aren't supported.
    #[serde(flatten, skip_serializing)]
    pub extra: json::Map<String, json::Value>,
@@ -544,6 +578,7 @@ impl Config {
            workers: Workers::default(),
            log: LogLevel::default(),
            seeding_policy: DefaultSeedingPolicy::default(),
+
            database: DatabaseConfig::default(),
            extra: json::Map::default(),
            secret: None,
        }
@@ -828,4 +863,121 @@ mod test {
            .unwrap()
        );
    }
+

+
    #[test]
+
    fn database_config_valid_combinations() {
+
        use super::{node, Config};
+

+
        let cases = [
+
            // (journal_mode, synchronous, expected_journal, expected_sync, description)
+
            (
+
                None,
+
                None,
+
                node::db::JournalMode::WAL,
+
                node::db::Synchronous::NORMAL,
+
                "defaults",
+
            ),
+
            (
+
                Some("wal"),
+
                Some("NORMAL"),
+
                node::db::JournalMode::WAL,
+
                node::db::Synchronous::NORMAL,
+
                "WAL+NORMAL (recommended)",
+
            ),
+
            (
+
                Some("wal"),
+
                Some("FULL"),
+
                node::db::JournalMode::WAL,
+
                node::db::Synchronous::FULL,
+
                "WAL+FULL (max durability)",
+
            ),
+
            (
+
                Some("wal"),
+
                Some("OFF"),
+
                node::db::JournalMode::WAL,
+
                node::db::Synchronous::OFF,
+
                "WAL+OFF (max performance)",
+
            ),
+
            (
+
                Some("rollback"),
+
                Some("FULL"),
+
                node::db::JournalMode::DELETE,
+
                node::db::Synchronous::FULL,
+
                "DELETE+FULL",
+
            ),
+
            (
+
                Some("rollback"),
+
                Some("EXTRA"),
+
                node::db::JournalMode::DELETE,
+
                node::db::Synchronous::EXTRA,
+
                "DELETE+EXTRA (max durability)",
+
            ),
+
            (
+
                Some("WAL"),
+
                Some("NORMAL"),
+
                node::db::JournalMode::WAL,
+
                node::db::Synchronous::NORMAL,
+
                "WAL uppercase",
+
            ),
+
            (
+
                Some("DELETE"),
+
                Some("NORMAL"),
+
                node::db::JournalMode::DELETE,
+
                node::db::Synchronous::NORMAL,
+
                "DELETE uppercase",
+
            ),
+
        ];
+

+
        for (journal_mode, synchronous, expected_journal, expected_sync, description) in cases {
+
            let mut json_value = json!({"alias": "example"});
+

+
            if let (Some(jm), Some(sync)) = (journal_mode, synchronous) {
+
                json_value["database"] = json!({
+
                    "sqlite": {
+
                        "journalMode": jm,
+
                        "synchronous": sync
+
                    }
+
                });
+
            }
+

+
            let config: Config = serde_json::from_value(json_value)
+
                .unwrap_or_else(|e| panic!("Failed to parse config for {description}: {e}"));
+

+
            assert_eq!(
+
                config.database.sqlite.journal_mode, expected_journal,
+
                "journal_mode mismatch for {description}"
+
            );
+
            assert_eq!(
+
                config.database.sqlite.synchronous, expected_sync,
+
                "synchronous mismatch for {description}"
+
            );
+
        }
+
    }
+

+
    #[test]
+
    fn database_config_rejects_invalid_values() {
+
        use super::Config;
+

+
        let invalid_cases = [
+
            (Some("INVALID"), Some("NORMAL"), "invalid journal_mode"),
+
            (Some("WAL"), Some("INVALID"), "invalid synchronous"),
+
            (Some("WAL"), Some("normal"), "lowercase synchronous"),
+
            (Some("Wal"), Some("NORMAL"), "mixed case journal_mode"),
+
        ];
+

+
        for (journal_mode, synchronous, description) in invalid_cases {
+
            let mut json_value = json!({"alias": "example", "database": {"sqlite": {}}});
+
            if let Some(jm) = journal_mode {
+
                json_value["database"]["sqlite"]["journalMode"] = json!(jm);
+
            }
+
            if let Some(sync) = synchronous {
+
                json_value["database"]["sqlite"]["synchronous"] = json!(sync);
+
            }
+

+
            assert!(
+
                serde_json::from_value::<Config>(json_value).is_err(),
+
                "Should reject {description}"
+
            );
+
        }
+
    }
}
modified crates/radicle/src/node/db.rs
@@ -50,16 +50,31 @@ pub enum Error {
}

/// Database journal mode.
-
#[derive(Debug, Default, Copy, Clone, serde::Serialize, serde::Deserialize)]
-
#[serde(rename_all = "camelCase")]
+
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum JournalMode {
    /// "WAL" mode. Good for concurrent reads & writes, but keeps some extra files around.
-
    #[serde(rename = "wal")]
+
    #[serde(alias = "wal")]
    #[default]
-
    WriteAheadLog,
+
    WAL,
    /// Default "rollback" mode. Certain writes may block reads.
    #[serde(alias = "rollback")]
-
    Rollback,
+
    DELETE,
+
}
+

+
/// SQLite synchronous flag value.
+
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+
pub enum Synchronous {
+
    /// OFF - No syncing at all. Fastest but least safe.
+
    OFF,
+
    /// NORMAL - Sync at critical moments. Good balance of safety and performance.
+
    #[default]
+
    NORMAL,
+
    /// FULL - Sync at every critical moment. Safest but slowest.
+
    FULL,
+
    /// EXTRA - Like FULL with additional syncing.
+
    EXTRA,
}

/// A file-backed database storing information about the network.
@@ -117,14 +132,15 @@ impl Database {

    /// Set journal mode.
    pub fn journal_mode(self, mode: JournalMode) -> Result<Self, Error> {
-
        match mode {
-
            JournalMode::Rollback => {
-
                self.db.execute("PRAGMA journal_mode = DELETE;")?;
-
            }
-
            JournalMode::WriteAheadLog => {
-
                self.db.execute("PRAGMA journal_mode = WAL;")?;
-
            }
-
        }
+
        self.db
+
            .execute(format!("PRAGMA journal_mode = {mode:?};"))?;
+
        Ok(self)
+
    }
+

+
    /// Set synchronous flag.
+
    pub fn synchronous(self, synchronous: Synchronous) -> Result<Self, Error> {
+
        self.db
+
            .execute(format!("PRAGMA synchronous = {synchronous:?};"))?;
        Ok(self)
    }

modified crates/radicle/src/profile.rs
@@ -253,7 +253,8 @@ impl Profile {
        home.policies_mut()?;
        home.notifications_mut()?;
        home.database_mut()?
-
            .journal_mode(node::db::JournalMode::default())?
+
            .journal_mode(config.node.database.sqlite.journal_mode)?
+
            .synchronous(config.node.database.sqlite.synchronous)?
            .init(
                &public_key,
                config.node.features(),