upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/purgatory/mod.rs139
1 files changed, 123 insertions, 16 deletions
diff --git a/src/purgatory/mod.rs b/src/purgatory/mod.rs
index 1894738..3c6bc1b 100644
--- a/src/purgatory/mod.rs
+++ b/src/purgatory/mod.rs
@@ -33,6 +33,13 @@ pub use sync::SyncQueueEntry;
33/// Default expiry duration for purgatory entries (30 minutes) 33/// Default expiry duration for purgatory entries (30 minutes)
34const DEFAULT_EXPIRY: Duration = Duration::from_secs(1800); 34const DEFAULT_EXPIRY: Duration = Duration::from_secs(1800);
35 35
36/// Extended expiry for soft-expired announcements (24 hours).
37///
38/// After the initial 30-minute expiry, the bare repo is deleted but the event is
39/// retained for this additional period. This allows revival if a state event arrives
40/// late (e.g. slow sync), without permanently blocking the repository.
41const SOFT_EXPIRY_EXTENDED: Duration = Duration::from_secs(86400);
42
36/// Default delay before syncing user-submitted events (3 minutes). 43/// Default delay before syncing user-submitted events (3 minutes).
37/// This gives time for the git push to arrive after the nostr event. 44/// This gives time for the git push to arrive after the nostr event.
38const DEFAULT_SYNC_DELAY: Duration = Duration::from_secs(180); 45const DEFAULT_SYNC_DELAY: Duration = Duration::from_secs(180);
@@ -657,20 +664,77 @@ impl Purgatory {
657 /// * `duration` - Minimum duration to guarantee from now 664 /// * `duration` - Minimum duration to guarantee from now
658 pub fn extend_announcement_expiry(&self, owner: &PublicKey, identifier: &str, duration: Duration) { 665 pub fn extend_announcement_expiry(&self, owner: &PublicKey, identifier: &str, duration: Duration) {
659 let key = (*owner, identifier.to_string()); 666 let key = (*owner, identifier.to_string());
667
668 // Collect revival info before taking a mutable borrow
669 let revival_info: Option<(PathBuf, bool)> = self
670 .announcement_purgatory
671 .get(&key)
672 .map(|entry| (entry.repo_path.clone(), entry.soft_expired));
673
660 if let Some(mut entry) = self.announcement_purgatory.get_mut(&key) { 674 if let Some(mut entry) = self.announcement_purgatory.get_mut(&key) {
661 let now = Instant::now(); 675 let now = Instant::now();
662 let new_expiry = now + duration; 676 let new_expiry = now + duration;
663 if entry.expires_at < new_expiry { 677 if entry.expires_at < new_expiry {
664 entry.expires_at = new_expiry; 678 entry.expires_at = new_expiry;
665 // If soft-expired, revive it 679 }
666 if entry.soft_expired { 680 // Always reset soft_expired when expiry is extended — the caller
667 entry.soft_expired = false; 681 // (state event or git auth) signals the repo is still active.
668 tracing::debug!( 682 if entry.soft_expired {
669 owner = %owner, 683 entry.soft_expired = false;
670 identifier = %identifier, 684 }
671 "Revived soft-expired announcement" 685 }
672 ); 686
687 // If the entry was soft-expired, recreate the bare repo outside the
688 // mutable borrow so we don't hold the DashMap lock during I/O.
689 if let Some((repo_path, was_soft_expired)) = revival_info {
690 if was_soft_expired {
691 if !repo_path.exists() {
692 match std::fs::create_dir_all(&repo_path) {
693 Ok(()) => {
694 // Initialise as a bare git repository
695 let status = std::process::Command::new("git")
696 .args(["init", "--bare"])
697 .arg(&repo_path)
698 .status();
699 match status {
700 Ok(s) if s.success() => {
701 tracing::info!(
702 path = %repo_path.display(),
703 owner = %owner,
704 identifier = %identifier,
705 "Recreated bare repository for revived soft-expired announcement"
706 );
707 }
708 Ok(s) => {
709 tracing::warn!(
710 path = %repo_path.display(),
711 exit_code = ?s.code(),
712 "git init --bare failed when reviving soft-expired announcement"
713 );
714 }
715 Err(e) => {
716 tracing::warn!(
717 path = %repo_path.display(),
718 error = %e,
719 "Failed to run git init --bare when reviving soft-expired announcement"
720 );
721 }
722 }
723 }
724 Err(e) => {
725 tracing::warn!(
726 path = %repo_path.display(),
727 error = %e,
728 "Failed to create directory when reviving soft-expired announcement"
729 );
730 }
731 }
673 } 732 }
733 tracing::info!(
734 owner = %owner,
735 identifier = %identifier,
736 "Revived soft-expired announcement (bare repo recreated, expiry extended)"
737 );
674 } 738 }
675 } 739 }
676 } 740 }
@@ -803,22 +867,65 @@ impl Purgatory {
803 pub fn cleanup(&self) -> (usize, usize, usize) { 867 pub fn cleanup(&self) -> (usize, usize, usize) {
804 let now = Instant::now(); 868 let now = Instant::now();
805 869
806 // Remove expired announcements and mark them as expired 870 // Process expired announcements with two-phase soft expiry:
807 let expired_announcements: Vec<(PublicKey, String, EventId)> = self 871 //
872 // Phase 1 (initial expiry, !soft_expired): Delete bare repo, set soft_expired=true,
873 // extend expiry by SOFT_EXPIRY_EXTENDED so the event is retained for revival.
874 // Phase 2 (extended expiry, soft_expired): Fully remove from purgatory.
875 //
876 // Collect entries that have passed their expires_at deadline.
877 let expired_announcements: Vec<(PublicKey, String, PathBuf, EventId, bool)> = self
808 .announcement_purgatory 878 .announcement_purgatory
809 .iter() 879 .iter()
810 .filter(|entry| entry.value().expires_at <= now) 880 .filter(|entry| entry.value().expires_at <= now)
811 .map(|entry| { 881 .map(|entry| {
812 let key = entry.key(); 882 let key = entry.key();
813 let event_id = entry.value().event.id; 883 let v = entry.value();
814 (key.0.clone(), key.1.clone(), event_id) 884 (key.0.clone(), key.1.clone(), v.repo_path.clone(), v.event.id, v.soft_expired)
815 }) 885 })
816 .collect(); 886 .collect();
817 887
818 let announcement_removed = expired_announcements.len(); 888 let mut announcement_removed = 0;
819 for (owner, identifier, event_id) in expired_announcements { 889 for (owner, identifier, repo_path, event_id, already_soft_expired) in expired_announcements {
820 self.mark_expired(event_id); 890 if already_soft_expired {
821 self.announcement_purgatory.remove(&(owner, identifier)); 891 // Phase 2: fully remove
892 self.mark_expired(event_id);
893 self.announcement_purgatory.remove(&(owner.clone(), identifier.clone()));
894 announcement_removed += 1;
895 tracing::info!(
896 owner = %owner,
897 identifier = %identifier,
898 "Announcement fully expired from purgatory (soft expiry period elapsed)"
899 );
900 } else {
901 // Phase 1: soft expiry — delete bare repo, retain event
902 if repo_path.exists() {
903 if let Err(e) = std::fs::remove_dir_all(&repo_path) {
904 tracing::warn!(
905 path = %repo_path.display(),
906 error = %e,
907 "Failed to delete bare repository during soft expiry"
908 );
909 } else {
910 tracing::info!(
911 path = %repo_path.display(),
912 owner = %owner,
913 identifier = %identifier,
914 "Deleted bare repository during soft expiry (event retained for revival)"
915 );
916 }
917 }
918 // Mark soft_expired and extend expiry
919 if let Some(mut entry) = self.announcement_purgatory.get_mut(&(owner.clone(), identifier.clone())) {
920 entry.soft_expired = true;
921 entry.expires_at = now + SOFT_EXPIRY_EXTENDED;
922 }
923 tracing::debug!(
924 owner = %owner,
925 identifier = %identifier,
926 "Announcement soft-expired: bare repo deleted, event retained for 24h"
927 );
928 }
822 } 929 }
823 930
824 let mut state_removed = 0; 931 let mut state_removed = 0;