diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 13:24:46 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-02-13 17:29:23 +0000 |
| commit | 1d09e4bdea7e328cf2740818df9df660c5532a99 (patch) | |
| tree | dcb758a70a2e9b84709df247cc685a2f6423094e /src/sync | |
| parent | a2a99d5a4137b57e4141cf2840f2f51b38035cfa (diff) | |
feat: implement announcement purgatory core (breaks archive sync test)
Route new announcements to purgatory instead of accepting immediately.
Announcements are promoted to the database when git data arrives,
ensuring we only serve announcements for repos with actual content.
Implemented:
- AnnouncementPurgatoryEntry type and DashMap store
- Route new announcements to purgatory (replacement announcements skip)
- Promote announcements on git data arrival (process_purgatory_announcements)
- Authorization checks purgatory announcements (fetch_repository_data_with_purgatory)
- State policy uses purgatory announcements for maintainer validation
- Cleanup task handles announcement expiry
- Updated count()/cleanup() to 3-tuples
Known broken:
- test_archive_read_only_creates_bare_repo fails: sync module does not
treat purgatory announcements as confirmed repos, so per-repo sync
(state events, PRs) is never triggered for purgatory announcements
- Announcement persistence (save/restore) not implemented
- SyncLevel (StateOnly vs Full) not implemented
- Soft expiry two-phase not implemented
- Expiry extension on state event / git auth not wired up
Diffstat (limited to 'src/sync')
| -rw-r--r-- | src/sync/mod.rs | 68 |
1 files changed, 66 insertions, 2 deletions
diff --git a/src/sync/mod.rs b/src/sync/mod.rs index 1ee1872..872df66 100644 --- a/src/sync/mod.rs +++ b/src/sync/mod.rs | |||
| @@ -1719,8 +1719,50 @@ impl SyncManager { | |||
| 1719 | // For sync-triggered events that go to purgatory, trigger immediate sync | 1719 | // For sync-triggered events that go to purgatory, trigger immediate sync |
| 1720 | // (instead of the default 3-minute delay for user-submitted events) | 1720 | // (instead of the default 3-minute delay for user-submitted events) |
| 1721 | if result == ProcessResult::Purgatory { | 1721 | if result == ProcessResult::Purgatory { |
| 1722 | // Announcements (kind 30617) - re-process rejected state events | ||
| 1723 | // When an announcement goes to purgatory, state events that were | ||
| 1724 | // previously rejected ("no announcement exists") can now be authorized | ||
| 1725 | // via fetch_repository_data_with_purgatory. | ||
| 1726 | if event.kind == Kind::GitRepoAnnouncement { | ||
| 1727 | use crate::nostr::events::RepositoryAnnouncement; | ||
| 1728 | |||
| 1729 | if let Ok(announcement) = RepositoryAnnouncement::from_event((*event).clone()) { | ||
| 1730 | // Re-process rejected state events for this announcement | ||
| 1731 | let (removed, hot_events) = rejected_events_index.invalidate_and_get( | ||
| 1732 | &event.pubkey, | ||
| 1733 | &announcement.identifier, | ||
| 1734 | Some(rejected_index::EventType::State), | ||
| 1735 | ); | ||
| 1736 | |||
| 1737 | if removed > 0 { | ||
| 1738 | tracing::info!( | ||
| 1739 | pubkey = %event.pubkey, | ||
| 1740 | identifier = %announcement.identifier, | ||
| 1741 | removed_from_cold_index = removed, | ||
| 1742 | hot_cache_events = hot_events.len(), | ||
| 1743 | "Invalidated rejected state events (announcement now in purgatory)" | ||
| 1744 | ); | ||
| 1745 | } | ||
| 1746 | |||
| 1747 | // Re-process state events from hot cache immediately | ||
| 1748 | if !hot_events.is_empty() { | ||
| 1749 | let _stats = Self::reprocess_events_from_hot_cache( | ||
| 1750 | hot_events, | ||
| 1751 | "state event (announcement in purgatory)", | ||
| 1752 | &event.pubkey, | ||
| 1753 | &announcement.identifier, | ||
| 1754 | &relay_url_clone, | ||
| 1755 | &database, | ||
| 1756 | &write_policy, | ||
| 1757 | &local_relay, | ||
| 1758 | &rejected_events_index, | ||
| 1759 | ) | ||
| 1760 | .await; | ||
| 1761 | } | ||
| 1762 | } | ||
| 1763 | } | ||
| 1722 | // State events (kind 30618) - extract identifier and trigger immediate sync | 1764 | // State events (kind 30618) - extract identifier and trigger immediate sync |
| 1723 | if event.kind.as_u16() == 30618 { | 1765 | else if event.kind.as_u16() == 30618 { |
| 1724 | if let Some(identifier) = event.tags.iter().find_map(|tag| { | 1766 | if let Some(identifier) = event.tags.iter().find_map(|tag| { |
| 1725 | let tag_vec = tag.clone().to_vec(); | 1767 | let tag_vec = tag.clone().to_vec(); |
| 1726 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { | 1768 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { |
| @@ -1754,7 +1796,9 @@ impl SyncManager { | |||
| 1754 | 1796 | ||
| 1755 | // Track pagination state for this subscription (REQ+EOSE) | 1797 | // Track pagination state for this subscription (REQ+EOSE) |
| 1756 | // and received event IDs for negentropy batches | 1798 | // and received event IDs for negentropy batches |
| 1757 | if result == ProcessResult::Saved || result == ProcessResult::Duplicate { | 1799 | // Include Purgatory results so announcements in purgatory still trigger |
| 1800 | // per-repo sync (state events, PR events) from the source relay. | ||
| 1801 | if result == ProcessResult::Saved || result == ProcessResult::Duplicate || result == ProcessResult::Purgatory { | ||
| 1758 | let mut pending = pending_sync_index.write().await; | 1802 | let mut pending = pending_sync_index.write().await; |
| 1759 | if let Some(batches) = pending.get_mut(&relay_url_clone) { | 1803 | if let Some(batches) = pending.get_mut(&relay_url_clone) { |
| 1760 | for batch in batches.iter_mut() { | 1804 | for batch in batches.iter_mut() { |
| @@ -2506,6 +2550,26 @@ impl SyncManager { | |||
| 2506 | "{} added to purgatory (waiting for git data)", | 2550 | "{} added to purgatory (waiting for git data)", |
| 2507 | context | 2551 | context |
| 2508 | ); | 2552 | ); |
| 2553 | // Trigger immediate sync for re-processed events that go to purgatory | ||
| 2554 | // (same as sync-triggered events in the main event loop) | ||
| 2555 | if event.kind.as_u16() == 30618 { | ||
| 2556 | // State event - extract identifier from 'd' tag | ||
| 2557 | if let Some(id) = event.tags.iter().find_map(|tag| { | ||
| 2558 | let tag_vec = tag.clone().to_vec(); | ||
| 2559 | if tag_vec.len() >= 2 && tag_vec[0] == "d" { | ||
| 2560 | Some(tag_vec[1].clone()) | ||
| 2561 | } else { | ||
| 2562 | None | ||
| 2563 | } | ||
| 2564 | }) { | ||
| 2565 | write_policy.purgatory().enqueue_sync_immediate(&id); | ||
| 2566 | } | ||
| 2567 | } else if event.kind.as_u16() == 1617 || event.kind.as_u16() == 1618 { | ||
| 2568 | // PR event - extract identifier from 'a' tag | ||
| 2569 | if let Some(id) = crate::git::sync::extract_identifier_from_pr_event(&event) { | ||
| 2570 | write_policy.purgatory().enqueue_sync_immediate(&id); | ||
| 2571 | } | ||
| 2572 | } | ||
| 2509 | } | 2573 | } |
| 2510 | ProcessResult::Rejected => { | 2574 | ProcessResult::Rejected => { |
| 2511 | stats.rejected += 1; | 2575 | stats.rejected += 1; |