add background job to clean up grasp-audit test events and git repos

Spawns a tokio task that runs every 30 minutes and removes all events
tagged 'grasp-audit-test-event' older than 2 hours from the LMDB
database, along with their associated bare git repositories on disk.
This commit is contained in:
DanConwayDev
2026-02-24 12:45:26 +00:00
parent 62f829e274
commit 3f4dc8e1bc
3 changed files with 159 additions and 0 deletions
+149
View File
@@ -0,0 +1,149 @@
//! Audit Event Cleanup
//!
//! Background job that periodically removes grasp-audit test events and their
//! associated git repositories from the relay.
//!
//! The grasp-audit tool tags every event it creates with:
//! `["t", "grasp-audit-test-event"]`
//!
//! This job:
//! 1. Queries kind 30617 (repo announcement) events tagged "grasp-audit-test-event"
//! that are older than `AUDIT_CLEANUP_AGE_SECS` seconds.
//! 2. Deletes the corresponding bare git repositories from disk.
//! 3. Deletes all events tagged "grasp-audit-test-event" older than the threshold
//! from the Nostr database.
//!
//! Runs every `AUDIT_CLEANUP_INTERVAL_SECS` seconds.
use std::path::PathBuf;
use std::time::Duration;
use nostr_sdk::prelude::*;
use tracing::{debug, error, info, warn};
use crate::nostr::builder::SharedDatabase;
use crate::nostr::events::RepositoryAnnouncement;
/// How old an audit event must be before it is eligible for deletion (2 hours).
const AUDIT_CLEANUP_AGE_SECS: u64 = 2 * 3600;
/// How often the cleanup job runs (30 minutes).
const AUDIT_CLEANUP_INTERVAL_SECS: u64 = 30 * 60;
/// The hashtag used by grasp-audit to mark all test events.
const AUDIT_TEST_EVENT_TAG: &str = "grasp-audit-test-event";
/// Run the audit cleanup loop indefinitely.
///
/// Spawned as a background tokio task in `main.rs`.
pub async fn run_audit_cleanup_loop(database: SharedDatabase, git_data_path: PathBuf) {
// Use an interval that fires immediately on the first tick, then every 30 minutes.
let mut interval = tokio::time::interval(Duration::from_secs(AUDIT_CLEANUP_INTERVAL_SECS));
loop {
interval.tick().await;
run_audit_cleanup_once(&database, &git_data_path).await;
}
}
/// Perform a single cleanup pass.
async fn run_audit_cleanup_once(database: &SharedDatabase, git_data_path: &PathBuf) {
let cutoff = Timestamp::from(Timestamp::now().as_secs().saturating_sub(AUDIT_CLEANUP_AGE_SECS));
// --- Step 1: Find repo announcements to delete git repos for ---
let repo_filter = Filter::new()
.kind(Kind::GitRepoAnnouncement)
.hashtag(AUDIT_TEST_EVENT_TAG)
.until(cutoff);
let repo_events = match database.query(repo_filter).await {
Ok(events) => events,
Err(e) => {
error!("audit_cleanup: failed to query repo announcements: {}", e);
return;
}
};
let mut repos_deleted = 0usize;
let mut repos_failed = 0usize;
for event in repo_events.iter() {
match RepositoryAnnouncement::from_event(event.clone()) {
Ok(announcement) => {
let repo_path = git_data_path.join(announcement.repo_path());
if repo_path.exists() {
match std::fs::remove_dir_all(&repo_path) {
Ok(()) => {
debug!(
"audit_cleanup: deleted git repo {}",
repo_path.display()
);
repos_deleted += 1;
// Remove the parent npub directory if it is now empty
if let Some(npub_dir) = repo_path.parent() {
if npub_dir.exists() {
match std::fs::read_dir(npub_dir) {
Ok(mut entries) => {
if entries.next().is_none() {
if let Err(e) = std::fs::remove_dir(npub_dir) {
warn!(
"audit_cleanup: could not remove empty npub dir {}: {}",
npub_dir.display(),
e
);
}
}
}
Err(e) => {
warn!(
"audit_cleanup: could not read npub dir {}: {}",
npub_dir.display(),
e
);
}
}
}
}
}
Err(e) => {
warn!(
"audit_cleanup: failed to delete git repo {}: {}",
repo_path.display(),
e
);
repos_failed += 1;
}
}
} else {
debug!(
"audit_cleanup: git repo already absent: {}",
repo_path.display()
);
}
}
Err(e) => {
warn!(
"audit_cleanup: could not parse repo announcement {}: {}",
event.id, e
);
}
}
}
// --- Step 2: Delete all audit events from the database ---
let all_audit_filter = Filter::new()
.hashtag(AUDIT_TEST_EVENT_TAG)
.until(cutoff);
match database.delete(all_audit_filter).await {
Ok(()) => {
info!(
"audit_cleanup: deleted audit events older than {}s; git repos deleted={}, failed={}",
AUDIT_CLEANUP_AGE_SECS, repos_deleted, repos_failed
);
}
Err(e) => {
error!("audit_cleanup: failed to delete audit events from database: {}", e);
}
}
}
+1
View File
@@ -1,3 +1,4 @@
pub mod audit_cleanup;
pub mod config;
pub mod git;
pub mod http;
+9
View File
@@ -7,6 +7,7 @@ use tracing::{error, info, warn};
use tracing_subscriber::{EnvFilter, FmtSubscriber};
use ngit_grasp::{
audit_cleanup,
config::{Config, DatabaseBackend},
git, http,
metrics::Metrics,
@@ -175,6 +176,14 @@ async fn main() -> Result<()> {
});
info!("Expired event cleanup task started (24h interval, keeps 7 days)");
// Spawn audit event cleanup task (30m interval, removes events >2h old)
let audit_db = relay_with_db.database.clone();
let audit_git_path = PathBuf::from(config.effective_git_data_path());
tokio::spawn(async move {
audit_cleanup::run_audit_cleanup_loop(audit_db, audit_git_path).await;
});
info!("Audit event cleanup task started (30m interval, removes events >2h old)");
// Start purgatory sync loop for background git data fetching
// Create naughty list tracker for git remote domains with persistent errors (12h expiration)
let git_naughty_list = Arc::new(NaughtyListTracker::with_defaults());