refactor(nostr): move deletion orchestration into deletion subsystem

This commit is contained in:
DanConwayDev
2026-06-19 13:58:23 +00:00
parent 829d92f873
commit 3d346be5f5
16 changed files with 74 additions and 43 deletions
+1 -1
View File
@@ -21,8 +21,8 @@ use std::time::Duration;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::nostr::builder::SharedDatabase;
use crate::nostr::events::RepositoryAnnouncement; use crate::nostr::events::RepositoryAnnouncement;
use crate::nostr::SharedDatabase;
/// How old an audit event must be before it is eligible for deletion (2 hours). /// How old an audit event must be before it is eligible for deletion (2 hours).
const AUDIT_CLEANUP_AGE_SECS: u64 = 2 * 3600; const AUDIT_CLEANUP_AGE_SECS: u64 = 2 * 3600;
+1 -1
View File
@@ -35,8 +35,8 @@ use std::collections::{HashMap, HashSet};
use std::sync::Arc; use std::sync::Arc;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::nostr::builder::SharedDatabase;
use crate::nostr::events::{RepositoryAnnouncement, RepositoryState}; use crate::nostr::events::{RepositoryAnnouncement, RepositoryState};
use crate::nostr::SharedDatabase;
use crate::purgatory::Purgatory; use crate::purgatory::Purgatory;
use nostr_sdk::prelude::{Kind, PublicKey}; use nostr_sdk::prelude::{Kind, PublicKey};
+3 -1
View File
@@ -17,8 +17,9 @@ use super::subprocess::GitSubprocess;
use crate::git::authorization::{authorize_push, parse_pushed_refs}; use crate::git::authorization::{authorize_push, parse_pushed_refs};
use crate::git::sync::process_newly_available_git_data; use crate::git::sync::process_newly_available_git_data;
use crate::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::holding::LifecycleReadGuard; use crate::nostr::holding::LifecycleReadGuard;
use crate::nostr::SharedDatabase;
use crate::purgatory::Purgatory; use crate::purgatory::Purgatory;
use crate::sync::rejected_index::RejectedEventsIndex; use crate::sync::rejected_index::RejectedEventsIndex;
@@ -408,6 +409,7 @@ pub async fn handle_receive_pack(
&purgatory, &purgatory,
git_data_path_buf, git_data_path_buf,
Some(&write_policy), Some(&write_policy),
Some(write_policy.deletion()),
Some(&rejected_events_index), Some(&rejected_events_index),
) )
.await .await
+16 -12
View File
@@ -42,8 +42,10 @@ use crate::git::authorization::{
fetch_repository_data_with_purgatory, RepositoryData, fetch_repository_data_with_purgatory, RepositoryData,
}; };
use crate::git::{self, oid_exists}; use crate::git::{self, oid_exists};
use crate::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::deletion::DeletionService;
use crate::nostr::events::RepositoryState; use crate::nostr::events::RepositoryState;
use crate::nostr::SharedDatabase;
use crate::purgatory::{can_apply_state, Purgatory}; use crate::purgatory::{can_apply_state, Purgatory};
use crate::sync::rejected_index::RejectedEventsIndex; use crate::sync::rejected_index::RejectedEventsIndex;
@@ -835,6 +837,7 @@ pub async fn process_newly_available_git_data(
purgatory: &Purgatory, purgatory: &Purgatory,
git_data_path: &Path, git_data_path: &Path,
write_policy: Option<&Nip34WritePolicy>, write_policy: Option<&Nip34WritePolicy>,
deletion_service: Option<&DeletionService>,
rejected_events_index: Option<&Arc<RejectedEventsIndex>>, rejected_events_index: Option<&Arc<RejectedEventsIndex>>,
) -> anyhow::Result<ProcessResult> { ) -> anyhow::Result<ProcessResult> {
let mut result = ProcessResult::default(); let mut result = ProcessResult::default();
@@ -866,6 +869,7 @@ pub async fn process_newly_available_git_data(
purgatory, purgatory,
git_data_path, git_data_path,
write_policy, write_policy,
deletion_service,
rejected_events_index, rejected_events_index,
) )
.await; .await;
@@ -879,7 +883,7 @@ pub async fn process_newly_available_git_data(
local_relay, local_relay,
purgatory, purgatory,
git_data_path, git_data_path,
write_policy, deletion_service,
) )
.await; .await;
result.merge(state_result); result.merge(state_result);
@@ -946,7 +950,7 @@ async fn process_purgatory_state_events(
local_relay: Option<&nostr_relay_builder::LocalRelay>, local_relay: Option<&nostr_relay_builder::LocalRelay>,
purgatory: &Purgatory, purgatory: &Purgatory,
git_data_path: &Path, git_data_path: &Path,
write_policy: Option<&Nip34WritePolicy>, deletion_service: Option<&DeletionService>,
) -> ProcessResult { ) -> ProcessResult {
let mut result = ProcessResult::default(); let mut result = ProcessResult::default();
@@ -1136,11 +1140,11 @@ async fn process_purgatory_state_events(
continue; continue;
} }
let owner = &announcement.event.pubkey; let owner = &announcement.event.pubkey;
if let (Some(wp), Some(entry)) = if let (Some(ds), Some(entry)) = (
(write_policy, purgatory.find_announcement(owner, identifier)) deletion_service,
{ purgatory.find_announcement(owner, identifier),
wp.deletion() ) {
.maybe_recover_deleted_repository(&entry.event, identifier) ds.maybe_recover_deleted_repository(&entry.event, identifier)
.await; .await;
} }
@@ -1414,6 +1418,7 @@ async fn process_purgatory_announcements(
purgatory: &Purgatory, purgatory: &Purgatory,
git_data_path: &Path, git_data_path: &Path,
write_policy: Option<&Nip34WritePolicy>, write_policy: Option<&Nip34WritePolicy>,
deletion_service: Option<&DeletionService>,
rejected_events_index: Option<&Arc<RejectedEventsIndex>>, rejected_events_index: Option<&Arc<RejectedEventsIndex>>,
) -> ProcessResult { ) -> ProcessResult {
let mut result = ProcessResult::default(); let mut result = ProcessResult::default();
@@ -1447,12 +1452,11 @@ async fn process_purgatory_announcements(
} }
}; };
if let (Some(wp), Some(entry)) = ( if let (Some(ds), Some(entry)) = (
write_policy, deletion_service,
purgatory.find_announcement(&owner, identifier), purgatory.find_announcement(&owner, identifier),
) { ) {
wp.deletion() ds.maybe_recover_deleted_repository(&entry.event, identifier)
.maybe_recover_deleted_repository(&entry.event, identifier)
.await; .await;
} }
+3 -1
View File
@@ -71,7 +71,8 @@ use crate::git::sync::process_newly_available_git_data;
use crate::git::{delete_ref, list_refs}; use crate::git::{delete_ref, list_refs};
use crate::grasp06::endpoint::PrsUrl; use crate::grasp06::endpoint::PrsUrl;
use crate::grasp06::paths::prs_repo_path; use crate::grasp06::paths::prs_repo_path;
use crate::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::SharedDatabase;
use crate::purgatory::Purgatory; use crate::purgatory::Purgatory;
use crate::sync::rejected_index::RejectedEventsIndex; use crate::sync::rejected_index::RejectedEventsIndex;
@@ -356,6 +357,7 @@ pub async fn handle_prs_receive_pack(
&purgatory, &purgatory,
Path::new(git_data_path), Path::new(git_data_path),
Some(&write_policy), Some(&write_policy),
Some(write_policy.deletion()),
Some(&rejected_events_index), Some(&rejected_events_index),
) )
.await .await
+2 -1
View File
@@ -27,7 +27,8 @@ use crate::config::Config;
use crate::git; use crate::git;
use crate::grasp06::receive::RepoInitLocks; use crate::grasp06::receive::RepoInitLocks;
use crate::metrics::Metrics; use crate::metrics::Metrics;
use crate::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::SharedDatabase;
use crate::purgatory::Purgatory; use crate::purgatory::Purgatory;
use crate::sync::rejected_index::RejectedEventsIndex; use crate::sync::rejected_index::RejectedEventsIndex;
+8 -10
View File
@@ -2,8 +2,8 @@
/// ///
/// Wires nostr-relay-builder with NIP-34 admission/routing policy. /// Wires nostr-relay-builder with NIP-34 admission/routing policy.
/// Deletion behaviour is owned by the `deletion` subsystem /// Deletion behaviour is owned by the `deletion` subsystem
/// (`crate::nostr::deletion`); this module holds only thin delegation /// (`crate::nostr::deletion`); this module keeps top-level `admit_event`
/// accessors and the top-level `admit_event` dispatch. /// dispatch and non-deletion admission/router policy logic.
use std::net::SocketAddr; use std::net::SocketAddr;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::path::Path; use std::path::Path;
@@ -24,9 +24,7 @@ use crate::nostr::policy::{
AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, ReferenceResult, AnnouncementPolicy, AnnouncementResult, PolicyContext, PrEventPolicy, ReferenceResult,
RelatedEventPolicy, StatePolicy, StateResult, RelatedEventPolicy, StatePolicy, StateResult,
}; };
use crate::nostr::SharedDatabase;
/// Type alias for the shared database used by the relay
pub type SharedDatabase = Arc<dyn NostrDatabase>;
/// NIP-34 Write Policy — admission and routing for GRASP-01 events /// NIP-34 Write Policy — admission and routing for GRASP-01 events
/// ///
@@ -530,7 +528,7 @@ impl Nip34WritePolicy {
// Process state alignment asynchronously // Process state alignment asynchronously
match self match self
.state_policy .state_policy
.process_state_event(event, is_synced, Some(self)) .process_state_event(event, is_synced, Some(self.deletion()))
.await .await
{ {
Ok(policy_result) => { Ok(policy_result) => {
@@ -747,7 +745,7 @@ impl Nip34WritePolicy {
// Re-evaluate authorization with the new announcement // Re-evaluate authorization with the new announcement
match self match self
.state_policy .state_policy
.process_state_event(&entry.event, false, Some(self)) .process_state_event(&entry.event, false, Some(self.deletion()))
.await .await
{ {
Ok(WritePolicyResult::Accept) => { Ok(WritePolicyResult::Accept) => {
@@ -1044,7 +1042,7 @@ pub async fn create_relay(
// //
// NIP-09 (deletion) and NIP-62 (request to vanish) auto-processing is // NIP-09 (deletion) and NIP-62 (request to vanish) auto-processing is
// explicitly disabled on the main database: ngit-grasp owns this handling // explicitly disabled on the main database: ngit-grasp owns this handling
// (see `DeletionPolicy`, the kind-62 handler, and the `Tombstones` store). // (see the deletion subsystem facade + `Tombstones` store).
// Leaving the backend defaults (`true`) would double-process: the backend // Leaving the backend defaults (`true`) would double-process: the backend
// would silently hard-delete events and block re-submission via its own // would silently hard-delete events and block re-submission via its own
// internal tables that we cannot inspect, conflicting with our purgatory / // internal tables that we cannot inspect, conflicting with our purgatory /
@@ -1058,8 +1056,8 @@ pub async fn create_relay(
DatabaseBackend::Memory => { DatabaseBackend::Memory => {
tracing::info!("Using in-memory database (no persistence)"); tracing::info!("Using in-memory database (no persistence)");
// Disable the backend's built-in NIP-09 / NIP-62 auto-processing to // Disable the backend's built-in NIP-09 / NIP-62 auto-processing to
// match the LMDB path: ngit-grasp owns this handling (DeletionPolicy, // match the LMDB path: ngit-grasp owns this handling (deletion
// the kind-62 handler, and the Tombstones store). Leaving the memory // subsystem + Tombstones store). Leaving the memory
// backend's defaults (`true`) would silently suppress targeted events // backend's defaults (`true`) would silently suppress targeted events
// at query time the moment a kind-5 is *stored* — which in particular // at query time the moment a kind-5 is *stored* — which in particular
// defeats `deletion_request_disrespector` (archival) mode, where the // defeats `deletion_request_disrespector` (archival) mode, where the
+1 -1
View File
@@ -1,8 +1,8 @@
use crate::nostr::builder::SharedDatabase;
use crate::nostr::history::ReplaceableHistoryStore; use crate::nostr::history::ReplaceableHistoryStore;
use crate::nostr::holding::HoldingStore; use crate::nostr::holding::HoldingStore;
use crate::nostr::policy::PolicyContext; use crate::nostr::policy::PolicyContext;
use crate::nostr::tombstones::Tombstones; use crate::nostr::tombstones::Tombstones;
use crate::nostr::SharedDatabase;
use crate::{config::Config, grasp06::receive::RepoInitLocks, purgatory::Purgatory}; use crate::{config::Config, grasp06::receive::RepoInitLocks, purgatory::Purgatory};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
+1 -1
View File
@@ -830,7 +830,7 @@ impl DeletionPolicy {
/// [`Self::archive_and_delete_filter`]: every deleted orphan/coordinate /// [`Self::archive_and_delete_filter`]: every deleted orphan/coordinate
/// target is moved into holding before main-DB deletion, and announcement /// target is moved into holding before main-DB deletion, and announcement
/// deletions include git archive metadata linkage for cleanup lifecycle. /// deletions include git archive metadata linkage for cleanup lifecycle.
/// Recovery orchestration remains a future phase. /// Recovery orchestration lives in `crate::nostr::deletion::recovery`.
async fn cascade_delete_announcement( async fn cascade_delete_announcement(
&self, &self,
author: &PublicKey, author: &PublicKey,
+2 -2
View File
@@ -6,5 +6,5 @@ pub mod holding;
pub mod policy; pub mod policy;
pub mod tombstones; pub mod tombstones;
/// Re-export SharedDatabase for use by policy modules /// Shared database type used across nostr modules.
pub use builder::SharedDatabase; pub type SharedDatabase = std::sync::Arc<dyn nostr_relay_builder::prelude::NostrDatabase>;
+4 -3
View File
@@ -12,7 +12,7 @@ use nostr_relay_builder::prelude::Event;
use super::{accepted_purgatory, duplicate, reject_invalid, reject_restricted, PolicyContext}; use super::{accepted_purgatory, duplicate, reject_invalid, reject_restricted, PolicyContext};
use crate::git; use crate::git;
use crate::git::authorization::fetch_repository_data_with_purgatory; use crate::git::authorization::fetch_repository_data_with_purgatory;
use crate::nostr::builder::Nip34WritePolicy; use crate::nostr::deletion::DeletionService;
use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState}; use crate::nostr::events::{validate_state, RepositoryAnnouncement, RepositoryState};
/// Result of state policy evaluation /// Result of state policy evaluation
@@ -54,7 +54,7 @@ impl StatePolicy {
&self, &self,
event: &Event, event: &Event,
is_synced: bool, is_synced: bool,
recovery_policy: Option<&Nip34WritePolicy>, deletion_service: Option<&DeletionService>,
) -> Result<WritePolicyResult> { ) -> Result<WritePolicyResult> {
// Parse state to get HEAD and branch info // Parse state to get HEAD and branch info
let state = let state =
@@ -237,7 +237,8 @@ impl StatePolicy {
local_relay.as_ref(), local_relay.as_ref(),
&self.ctx.purgatory, &self.ctx.purgatory,
&self.ctx.git_data_path, &self.ctx.git_data_path,
recovery_policy, None,
deletion_service,
None, None,
) )
.await .await
+2 -1
View File
@@ -193,8 +193,8 @@ use std::sync::Arc;
use tracing::debug; use tracing::debug;
use crate::nostr::builder::Nip34WritePolicy; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::builder::SharedDatabase;
use crate::nostr::events::RepositoryState; use crate::nostr::events::RepositoryState;
use crate::nostr::SharedDatabase;
use crate::purgatory::Purgatory; use crate::purgatory::Purgatory;
use crate::sync::naughty_list::NaughtyListTracker; use crate::sync::naughty_list::NaughtyListTracker;
@@ -526,6 +526,7 @@ impl SyncContext for RealSyncContext {
&self.purgatory, &self.purgatory,
&self.git_data_path, &self.git_data_path,
self.write_policy.as_ref(), self.write_policy.as_ref(),
self.write_policy.as_ref().map(|wp| wp.deletion()),
None, None,
) )
.await?; .await?;
+2 -1
View File
@@ -51,7 +51,8 @@ use nostr_sdk::prelude::*;
use tokio::sync::{broadcast, Mutex, RwLock}; use tokio::sync::{broadcast, Mutex, RwLock};
use crate::config::Config; use crate::config::Config;
use crate::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use crate::nostr::builder::Nip34WritePolicy;
use crate::nostr::SharedDatabase;
use nostr_relay_builder::prelude::LocalRelay; use nostr_relay_builder::prelude::LocalRelay;
// ============================================================================= // =============================================================================
+1 -1
View File
@@ -19,7 +19,7 @@ use futures_util::StreamExt;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use crate::nostr::builder::SharedDatabase; use crate::nostr::SharedDatabase;
/// Events from a relay connection /// Events from a relay connection
#[derive(Debug)] #[derive(Debug)]
+1 -1
View File
@@ -16,7 +16,7 @@ use nostr_sdk::prelude::Timestamp;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use tokio::sync::{broadcast, mpsc}; use tokio::sync::{broadcast, mpsc};
use crate::nostr::builder::SharedDatabase; use crate::nostr::SharedDatabase;
use super::{AddFilters, RepoSyncIndex, RepoSyncNeeds, SyncLevel}; use super::{AddFilters, RepoSyncIndex, RepoSyncNeeds, SyncLevel};
+26 -5
View File
@@ -8,12 +8,13 @@ use clap::Parser;
use ngit_grasp::config::Config; use ngit_grasp::config::Config;
use ngit_grasp::grasp06::receive::new_repo_init_locks; use ngit_grasp::grasp06::receive::new_repo_init_locks;
use ngit_grasp::metrics::{self, REGISTRY}; use ngit_grasp::metrics::{self, REGISTRY};
use ngit_grasp::nostr::builder::{Nip34WritePolicy, SharedDatabase}; use ngit_grasp::nostr::builder::Nip34WritePolicy;
use ngit_grasp::nostr::history::ReplaceableHistoryStore; use ngit_grasp::nostr::history::ReplaceableHistoryStore;
use ngit_grasp::nostr::holding::{ use ngit_grasp::nostr::holding::{
DeletionSource, GitArchiveMetadata, HoldingMetadata, HoldingStore, HOLDING_ARCHIVE_PATH_TAG, DeletionSource, GitArchiveMetadata, HoldingMetadata, HoldingStore, HOLDING_ARCHIVE_PATH_TAG,
}; };
use ngit_grasp::nostr::tombstones::Tombstones; use ngit_grasp::nostr::tombstones::Tombstones;
use ngit_grasp::nostr::SharedDatabase;
use ngit_grasp::purgatory::Purgatory; use ngit_grasp::purgatory::Purgatory;
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use prometheus::{Encoder, TextEncoder}; use prometheus::{Encoder, TextEncoder};
@@ -480,7 +481,12 @@ async fn startup_whitelist_restore_recovers_now_whitelisted_scope_and_is_idempot
"ngit_whitelist_startup_restore_total", "ngit_whitelist_startup_restore_total",
&[("result", "attempted"), ("reason", "none")], &[("result", "attempted"), ("reason", "none")],
); );
assert_eq!(attempted_after, attempted_before + 1.0); assert!(
attempted_after >= attempted_before + 1.0,
"expected attempted restore metric to increase by at least 1 (before={}, after={})",
attempted_before,
attempted_after
);
let success_before = metric_value( let success_before = metric_value(
&before, &before,
@@ -492,7 +498,12 @@ async fn startup_whitelist_restore_recovers_now_whitelisted_scope_and_is_idempot
"ngit_whitelist_startup_restore_total", "ngit_whitelist_startup_restore_total",
&[("result", "succeeded"), ("reason", "none")], &[("result", "succeeded"), ("reason", "none")],
); );
assert_eq!(success_after, success_before + 1.0); assert!(
success_after >= success_before + 1.0,
"expected successful restore metric to increase by at least 1 (before={}, after={})",
success_before,
success_after
);
let second_stats = restore_policy let second_stats = restore_policy
.deletion() .deletion()
@@ -702,7 +713,12 @@ async fn startup_blacklist_restore_recovers_unblacklisted_scope_and_is_idempoten
"ngit_blacklist_startup_restore_total", "ngit_blacklist_startup_restore_total",
&[("result", "attempted"), ("reason", "none")], &[("result", "attempted"), ("reason", "none")],
); );
assert_eq!(attempted_after, attempted_before + 1.0); assert!(
attempted_after >= attempted_before + 1.0,
"expected attempted restore metric to increase by at least 1 (before={}, after={})",
attempted_before,
attempted_after
);
let success_before = metric_value( let success_before = metric_value(
&before, &before,
@@ -714,7 +730,12 @@ async fn startup_blacklist_restore_recovers_unblacklisted_scope_and_is_idempoten
"ngit_blacklist_startup_restore_total", "ngit_blacklist_startup_restore_total",
&[("result", "succeeded"), ("reason", "none")], &[("result", "succeeded"), ("reason", "none")],
); );
assert_eq!(success_after, success_before + 1.0); assert!(
success_after >= success_before + 1.0,
"expected successful restore metric to increase by at least 1 (before={}, after={})",
success_before,
success_after
);
let second_stats = restore_policy let second_stats = restore_policy
.deletion() .deletion()