diff options
Diffstat (limited to 'src/purgatory')
| -rw-r--r-- | src/purgatory/mod.rs | 139 |
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) |
| 34 | const DEFAULT_EXPIRY: Duration = Duration::from_secs(1800); | 34 | const 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. | ||
| 41 | const 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. |
| 38 | const DEFAULT_SYNC_DELAY: Duration = Duration::from_secs(180); | 45 | const 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; |