diff --git a/.env.example b/.env.example index 5d29af8..5692263 100644 --- a/.env.example +++ b/.env.example @@ -169,6 +169,23 @@ # Default: 12 # NGIT_NAUGHTY_LIST_EXPIRATION_HOURS=12 +# ============================================================================ +# HOLDING DB CLEANUP +# ============================================================================ + +# Retention window in seconds for deleted events kept in holding DB +# Used by startup catch-up and periodic expiration cleanup +# Must be greater than 0 +# CLI: --holding-retention-secs +# Default: 7776000 (90 days) +# NGIT_HOLDING_RETENTION_SECS=7776000 + +# Interval in seconds between holding DB background cleanup passes +# Must be greater than 0 +# CLI: --holding-cleanup-interval-secs +# Default: 86400 (24 hours) +# NGIT_HOLDING_CLEANUP_INTERVAL_SECS=86400 + # ============================================================================ # GRASP-05 ARCHIVE MODE # ============================================================================ @@ -345,4 +362,4 @@ # When unset (default), connections are unlimited (defers to OS fd limits and infrastructure controls) # CLI: --max-connections # Default: unlimited -# NGIT_MAX_CONNECTIONS=4096 \ No newline at end of file +# NGIT_MAX_CONNECTIONS=4096 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index b4c0e49..45bdc38 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -492,6 +492,58 @@ NGIT_REJECTED_COLD_INDEX_EXPIRY_SECS=1209600 --- +### Holding DB Cleanup Configuration + +These options control retention and cleanup cadence for deleted events archived in the holding database. + +#### `NGIT_HOLDING_RETENTION_SECS` + +**Description:** How long deleted events are retained in the holding DB before expiration cleanup removes them +**Type:** Integer (seconds) +**Default:** `7776000` (90 days) +**Required:** No + +**Examples:** + +```bash +# Default: 90 days +NGIT_HOLDING_RETENTION_SECS=7776000 + +# Short retention (30 days) +NGIT_HOLDING_RETENTION_SECS=2592000 +``` + +**Notes:** + +- Must be greater than 0 +- Used by both startup catch-up cleanup and periodic cleanup passes + +--- + +#### `NGIT_HOLDING_CLEANUP_INTERVAL_SECS` + +**Description:** Interval between periodic holding DB expiration cleanup passes +**Type:** Integer (seconds) +**Default:** `86400` (24 hours) +**Required:** No + +**Examples:** + +```bash +# Default: daily cleanup +NGIT_HOLDING_CLEANUP_INTERVAL_SECS=86400 + +# Every 6 hours +NGIT_HOLDING_CLEANUP_INTERVAL_SECS=21600 +``` + +**Notes:** + +- Must be greater than 0 +- Smaller values clean up expired records sooner at the cost of more background work + +--- + ### GRASP-05 Archive Configuration These options enable archive/mirror/backup mode per the GRASP-05 specification. diff --git a/nix/module.nix b/nix/module.nix index c8f3f92..ad9a0b7 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -178,6 +178,20 @@ let description = "Hours before removing relay from naughty list"; }; + holdingRetentionSecs = mkOption { + type = types.int; + default = 7776000; + description = + "Retention window in seconds for deleted events in holding DB (default: 90 days)"; + }; + + holdingCleanupIntervalSecs = mkOption { + type = types.int; + default = 86400; + description = + "Interval in seconds between holding DB cleanup passes (default: 24 hours)"; + }; + archiveAll = mkOption { type = types.bool; default = false; @@ -369,6 +383,9 @@ let toString cfg.rejectedColdIndexExpirySecs; NGIT_NAUGHTY_LIST_EXPIRATION_HOURS = toString cfg.naughtyListExpirationHours; + NGIT_HOLDING_RETENTION_SECS = toString cfg.holdingRetentionSecs; + NGIT_HOLDING_CLEANUP_INTERVAL_SECS = + toString cfg.holdingCleanupIntervalSecs; NGIT_ARCHIVE_ALL = if cfg.archiveAll then "true" else "false"; NGIT_ARCHIVE_WHITELIST = concatStringsSep "," cfg.archiveWhitelist; NGIT_ARCHIVE_GRASP_SERVICES = diff --git a/src/config.rs b/src/config.rs index 058b969..d7d10b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,6 +4,7 @@ use nostr_sdk::prelude::*; use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +use std::time::Duration; /// Whitelist entry for repository/archive filtering #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -428,6 +429,22 @@ pub struct Config { #[arg(long, env = "NGIT_NAUGHTY_LIST_EXPIRATION_HOURS", default_value_t = 12)] pub naughty_list_expiration_hours: u64, + /// Retention window in seconds for deleted events kept in holding DB. + #[arg( + long, + env = "NGIT_HOLDING_RETENTION_SECS", + default_value_t = crate::nostr::holding::DEFAULT_RETENTION.as_secs() + )] + pub holding_retention_secs: u64, + + /// Interval in seconds between background holding cleanup passes. + #[arg( + long, + env = "NGIT_HOLDING_CLEANUP_INTERVAL_SECS", + default_value_t = crate::nostr::holding::DEFAULT_CLEANUP_INTERVAL.as_secs() + )] + pub holding_cleanup_interval_secs: u64, + /// Enable GRASP-05 archive mode: accept all announcements regardless of listing (WARNING: storage risk) #[arg(long, env = "NGIT_ARCHIVE_ALL", default_value_t = false)] pub archive_all: bool, @@ -661,6 +678,18 @@ impl Config { // Validate repository whitelist configuration let repository_whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist); + if self.holding_retention_secs == 0 { + return Err(anyhow!( + "NGIT_HOLDING_RETENTION_SECS must be greater than 0" + )); + } + + if self.holding_cleanup_interval_secs == 0 { + return Err(anyhow!( + "NGIT_HOLDING_CLEANUP_INTERVAL_SECS must be greater than 0" + )); + } + // Fatal error: repository_whitelist with archive_read_only=true (incompatible) if !repository_whitelist.is_empty() { let read_only = self.archive_read_only.unwrap_or(archive_enabled); @@ -751,6 +780,16 @@ impl Config { EventBlacklistConfig { blacklisted_npubs } } + /// Holding DB retention as a duration. + pub fn holding_retention(&self) -> Duration { + Duration::from_secs(self.holding_retention_secs) + } + + /// Holding DB periodic cleanup interval as a duration. + pub fn holding_cleanup_interval(&self) -> Duration { + Duration::from_secs(self.holding_cleanup_interval_secs) + } + /// Create config for testing #[cfg(test)] pub fn for_testing() -> Self { @@ -781,6 +820,9 @@ impl Config { rejected_hot_cache_duration_secs: 120, rejected_cold_index_expiry_secs: 604800, naughty_list_expiration_hours: 12, + holding_retention_secs: crate::nostr::holding::DEFAULT_RETENTION.as_secs(), + holding_cleanup_interval_secs: crate::nostr::holding::DEFAULT_CLEANUP_INTERVAL + .as_secs(), archive_all: false, archive_whitelist: String::new(), archive_grasp_services: String::new(), diff --git a/src/main.rs b/src/main.rs index 9145818..0e2e915 100644 --- a/src/main.rs +++ b/src/main.rs @@ -399,11 +399,10 @@ async fn run_relay(config: Config) -> Result<()> { // 1) startup catch-up pass (runs once) // 2) periodic background cleanup pass let holding_cleanup_store = relay_with_db.holding.clone(); + let holding_retention = config.holding_retention(); + let holding_cleanup_interval = config.holding_cleanup_interval(); match holding_cleanup_store - .cleanup_expired( - nostr_sdk::prelude::Timestamp::now(), - nostr::holding::DEFAULT_RETENTION, - ) + .cleanup_expired(nostr_sdk::prelude::Timestamp::now(), holding_retention) .await { Ok(stats) => { @@ -423,7 +422,6 @@ async fn run_relay(config: Config) -> Result<()> { } let (holding_cleanup_shutdown_tx, mut holding_cleanup_shutdown_rx) = watch::channel(false); - let holding_cleanup_interval = nostr::holding::DEFAULT_CLEANUP_INTERVAL; let holding_cleanup_task = tokio::spawn(async move { let mut interval = tokio::time::interval_at( tokio::time::Instant::now() + holding_cleanup_interval, @@ -434,10 +432,7 @@ async fn run_relay(config: Config) -> Result<()> { tokio::select! { _ = interval.tick() => { match holding_cleanup_store - .cleanup_expired( - nostr_sdk::prelude::Timestamp::now(), - nostr::holding::DEFAULT_RETENTION, - ) + .cleanup_expired(nostr_sdk::prelude::Timestamp::now(), holding_retention) .await { Ok(stats) => { @@ -466,8 +461,8 @@ async fn run_relay(config: Config) -> Result<()> { } }); info!( - retention_secs = nostr::holding::DEFAULT_RETENTION.as_secs(), - interval_secs = nostr::holding::DEFAULT_CLEANUP_INTERVAL.as_secs(), + retention_secs = holding_retention.as_secs(), + interval_secs = holding_cleanup_interval.as_secs(), "Holding cleanup task started" );