diff options
| -rw-r--r-- | src/audit_cleanup.rs | 149 | ||||
| -rw-r--r-- | src/lib.rs | 1 | ||||
| -rw-r--r-- | src/main.rs | 9 |
3 files changed, 159 insertions, 0 deletions
diff --git a/src/audit_cleanup.rs b/src/audit_cleanup.rs new file mode 100644 index 0000000..b976b1f --- /dev/null +++ b/src/audit_cleanup.rs | |||
| @@ -0,0 +1,149 @@ | |||
| 1 | //! Audit Event Cleanup | ||
| 2 | //! | ||
| 3 | //! Background job that periodically removes grasp-audit test events and their | ||
| 4 | //! associated git repositories from the relay. | ||
| 5 | //! | ||
| 6 | //! The grasp-audit tool tags every event it creates with: | ||
| 7 | //! `["t", "grasp-audit-test-event"]` | ||
| 8 | //! | ||
| 9 | //! This job: | ||
| 10 | //! 1. Queries kind 30617 (repo announcement) events tagged "grasp-audit-test-event" | ||
| 11 | //! that are older than `AUDIT_CLEANUP_AGE_SECS` seconds. | ||
| 12 | //! 2. Deletes the corresponding bare git repositories from disk. | ||
| 13 | //! 3. Deletes all events tagged "grasp-audit-test-event" older than the threshold | ||
| 14 | //! from the Nostr database. | ||
| 15 | //! | ||
| 16 | //! Runs every `AUDIT_CLEANUP_INTERVAL_SECS` seconds. | ||
| 17 | |||
| 18 | use std::path::PathBuf; | ||
| 19 | use std::time::Duration; | ||
| 20 | |||
| 21 | use nostr_sdk::prelude::*; | ||
| 22 | use tracing::{debug, error, info, warn}; | ||
| 23 | |||
| 24 | use crate::nostr::builder::SharedDatabase; | ||
| 25 | use crate::nostr::events::RepositoryAnnouncement; | ||
| 26 | |||
| 27 | /// How old an audit event must be before it is eligible for deletion (2 hours). | ||
| 28 | const AUDIT_CLEANUP_AGE_SECS: u64 = 2 * 3600; | ||
| 29 | |||
| 30 | /// How often the cleanup job runs (30 minutes). | ||
| 31 | const AUDIT_CLEANUP_INTERVAL_SECS: u64 = 30 * 60; | ||
| 32 | |||
| 33 | /// The hashtag used by grasp-audit to mark all test events. | ||
| 34 | const AUDIT_TEST_EVENT_TAG: &str = "grasp-audit-test-event"; | ||
| 35 | |||
| 36 | /// Run the audit cleanup loop indefinitely. | ||
| 37 | /// | ||
| 38 | /// Spawned as a background tokio task in `main.rs`. | ||
| 39 | pub async fn run_audit_cleanup_loop(database: SharedDatabase, git_data_path: PathBuf) { | ||
| 40 | // Use an interval that fires immediately on the first tick, then every 30 minutes. | ||
| 41 | let mut interval = tokio::time::interval(Duration::from_secs(AUDIT_CLEANUP_INTERVAL_SECS)); | ||
| 42 | loop { | ||
| 43 | interval.tick().await; | ||
| 44 | run_audit_cleanup_once(&database, &git_data_path).await; | ||
| 45 | } | ||
| 46 | } | ||
| 47 | |||
| 48 | /// Perform a single cleanup pass. | ||
| 49 | async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathBuf) { | ||
| 50 | let cutoff = Timestamp::from(Timestamp::now().as_secs().saturating_sub(AUDIT_CLEANUP_AGE_SECS)); | ||
| 51 | |||
| 52 | // --- Step 1: Find repo announcements to delete git repos for --- | ||
| 53 | let repo_filter = Filter::new() | ||
| 54 | .kind(Kind::GitRepoAnnouncement) | ||
| 55 | .hashtag(AUDIT_TEST_EVENT_TAG) | ||
| 56 | .until(cutoff); | ||
| 57 | |||
| 58 | let repo_events = match database.query(repo_filter).await { | ||
| 59 | Ok(events) => events, | ||
| 60 | Err(e) => { | ||
| 61 | error!("audit_cleanup: failed to query repo announcements: {}", e); | ||
| 62 | return; | ||
| 63 | } | ||
| 64 | }; | ||
| 65 | |||
| 66 | let mut repos_deleted = 0usize; | ||
| 67 | let mut repos_failed = 0usize; | ||
| 68 | |||
| 69 | for event in repo_events.iter() { | ||
| 70 | match RepositoryAnnouncement::from_event(event.clone()) { | ||
| 71 | Ok(announcement) => { | ||
| 72 | let repo_path = git_data_path.join(announcement.repo_path()); | ||
| 73 | if repo_path.exists() { | ||
| 74 | match std::fs::remove_dir_all(&repo_path) { | ||
| 75 | Ok(()) => { | ||
| 76 | debug!( | ||
| 77 | "audit_cleanup: deleted git repo {}", | ||
| 78 | repo_path.display() | ||
| 79 | ); | ||
| 80 | repos_deleted += 1; | ||
| 81 | |||
| 82 | // Remove the parent npub directory if it is now empty | ||
| 83 | if let Some(npub_dir) = repo_path.parent() { | ||
| 84 | if npub_dir.exists() { | ||
| 85 | match std::fs::read_dir(npub_dir) { | ||
| 86 | Ok(mut entries) => { | ||
| 87 | if entries.next().is_none() { | ||
| 88 | if let Err(e) = std::fs::remove_dir(npub_dir) { | ||
| 89 | warn!( | ||
| 90 | "audit_cleanup: could not remove empty npub dir {}: {}", | ||
| 91 | npub_dir.display(), | ||
| 92 | e | ||
| 93 | ); | ||
| 94 | } | ||
| 95 | } | ||
| 96 | } | ||
| 97 | Err(e) => { | ||
| 98 | warn!( | ||
| 99 | "audit_cleanup: could not read npub dir {}: {}", | ||
| 100 | npub_dir.display(), | ||
| 101 | e | ||
| 102 | ); | ||
| 103 | } | ||
| 104 | } | ||
| 105 | } | ||
| 106 | } | ||
| 107 | } | ||
| 108 | Err(e) => { | ||
| 109 | warn!( | ||
| 110 | "audit_cleanup: failed to delete git repo {}: {}", | ||
| 111 | repo_path.display(), | ||
| 112 | e | ||
| 113 | ); | ||
| 114 | repos_failed += 1; | ||
| 115 | } | ||
| 116 | } | ||
| 117 | } else { | ||
| 118 | debug!( | ||
| 119 | "audit_cleanup: git repo already absent: {}", | ||
| 120 | repo_path.display() | ||
| 121 | ); | ||
| 122 | } | ||
| 123 | } | ||
| 124 | Err(e) => { | ||
| 125 | warn!( | ||
| 126 | "audit_cleanup: could not parse repo announcement {}: {}", | ||
| 127 | event.id, e | ||
| 128 | ); | ||
| 129 | } | ||
| 130 | } | ||
| 131 | } | ||
| 132 | |||
| 133 | // --- Step 2: Delete all audit events from the database --- | ||
| 134 | let all_audit_filter = Filter::new() | ||
| 135 | .hashtag(AUDIT_TEST_EVENT_TAG) | ||
| 136 | .until(cutoff); | ||
| 137 | |||
| 138 | match database.delete(all_audit_filter).await { | ||
| 139 | Ok(()) => { | ||
| 140 | info!( | ||
| 141 | "audit_cleanup: deleted audit events older than {}s; git repos deleted={}, failed={}", | ||
| 142 | AUDIT_CLEANUP_AGE_SECS, repos_deleted, repos_failed | ||
| 143 | ); | ||
| 144 | } | ||
| 145 | Err(e) => { | ||
| 146 | error!("audit_cleanup: failed to delete audit events from database: {}", e); | ||
| 147 | } | ||
| 148 | } | ||
| 149 | } | ||
| @@ -1,3 +1,4 @@ | |||
| 1 | pub mod audit_cleanup; | ||
| 1 | pub mod config; | 2 | pub mod config; |
| 2 | pub mod git; | 3 | pub mod git; |
| 3 | pub mod http; | 4 | pub mod http; |
diff --git a/src/main.rs b/src/main.rs index bf3aefb..12a875c 100644 --- a/src/main.rs +++ b/src/main.rs | |||
| @@ -7,6 +7,7 @@ use tracing::{error, info, warn}; | |||
| 7 | use tracing_subscriber::{EnvFilter, FmtSubscriber}; | 7 | use tracing_subscriber::{EnvFilter, FmtSubscriber}; |
| 8 | 8 | ||
| 9 | use ngit_grasp::{ | 9 | use ngit_grasp::{ |
| 10 | audit_cleanup, | ||
| 10 | config::{Config, DatabaseBackend}, | 11 | config::{Config, DatabaseBackend}, |
| 11 | git, http, | 12 | git, http, |
| 12 | metrics::Metrics, | 13 | metrics::Metrics, |
| @@ -175,6 +176,14 @@ async fn main() -> Result<()> { | |||
| 175 | }); | 176 | }); |
| 176 | info!("Expired event cleanup task started (24h interval, keeps 7 days)"); | 177 | info!("Expired event cleanup task started (24h interval, keeps 7 days)"); |
| 177 | 178 | ||
| 179 | // Spawn audit event cleanup task (30m interval, removes events >2h old) | ||
| 180 | let audit_db = relay_with_db.database.clone(); | ||
| 181 | let audit_git_path = PathBuf::from(config.effective_git_data_path()); | ||
| 182 | tokio::spawn(async move { | ||
| 183 | audit_cleanup::run_audit_cleanup_loop(audit_db, audit_git_path).await; | ||
| 184 | }); | ||
| 185 | info!("Audit event cleanup task started (30m interval, removes events >2h old)"); | ||
| 186 | |||
| 178 | // Start purgatory sync loop for background git data fetching | 187 | // Start purgatory sync loop for background git data fetching |
| 179 | // Create naughty list tracker for git remote domains with persistent errors (12h expiration) | 188 | // Create naughty list tracker for git remote domains with persistent errors (12h expiration) |
| 180 | let git_naughty_list = Arc::new(NaughtyListTracker::with_defaults()); | 189 | let git_naughty_list = Arc::new(NaughtyListTracker::with_defaults()); |