mirror of
https://relay.ngit.dev/npub15qydau2hjma6ngxkl2cyar74wzyjshvl65za5k5rl69264ar2exs5cyejr/ngit-grasp.git
synced 2026-10-05 23:18:24 +00:00
fix: prune unreconstructable state replacements
This commit is contained in:
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed superseded same-author repository states from purgatory after their
|
||||
replacement is promoted when the pushed Git data cannot reconstruct them.
|
||||
Reconstructable rollback states and other maintainers' states are retained.
|
||||
|
||||
## [2.0.0] - 2026-07-27
|
||||
|
||||
### Breaking changes
|
||||
|
||||
@@ -440,6 +440,12 @@ sequenceDiagram
|
||||
end
|
||||
```
|
||||
|
||||
State promotion remains oldest-first so reconstructable predecessors reach
|
||||
relay history before a newer replacement. After a promotion pass, purgatory
|
||||
removes only older events on the promoted event's exact coordinate (same author
|
||||
and `d` tag) when their Git objects are still unavailable. Reconstructable
|
||||
predecessors and other maintainers' states remain as rollback candidates.
|
||||
|
||||
### PR Event Arrival (Kind 1617/1618)
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -1060,6 +1060,7 @@ async fn process_purgatory_state_events(
|
||||
promotion_hooks: Option<&dyn PurgatoryPromotionHooks>,
|
||||
) -> ProcessResult {
|
||||
let mut result = ProcessResult::default();
|
||||
let mut promoted_states = Vec::new();
|
||||
|
||||
// Find state events in purgatory for this identifier
|
||||
let mut purgatory_states = purgatory.find_state(identifier);
|
||||
@@ -1235,6 +1236,7 @@ async fn process_purgatory_state_events(
|
||||
// Remove from purgatory
|
||||
purgatory.remove_state_event(identifier, &entry.event.id);
|
||||
result.states_released += 1;
|
||||
promoted_states.push(entry.event.clone());
|
||||
|
||||
// Add the newly saved state to db_repo_data so subsequent iterations
|
||||
// can correctly determine if they're the latest
|
||||
@@ -1331,6 +1333,20 @@ async fn process_purgatory_state_events(
|
||||
}
|
||||
}
|
||||
|
||||
let removed = purgatory.remove_unreconstructable_state_replacements(
|
||||
identifier,
|
||||
&promoted_states,
|
||||
source_repo_path,
|
||||
);
|
||||
result.states_released += removed;
|
||||
if removed > 0 {
|
||||
info!(
|
||||
identifier = %identifier,
|
||||
removed,
|
||||
"Removed unreconstructable superseded state events from purgatory"
|
||||
);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
@@ -640,6 +640,58 @@ impl Purgatory {
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove older states on coordinates promoted in this pass when their Git
|
||||
/// objects are still unavailable. Reconstructable predecessors and states
|
||||
/// from other authors remain available as rollback candidates.
|
||||
pub(crate) fn remove_unreconstructable_state_replacements(
|
||||
&self,
|
||||
identifier: &str,
|
||||
promoted: &[Event],
|
||||
source_repo_path: &Path,
|
||||
) -> usize {
|
||||
let is_preferred = |candidate: &Event, current: &Event| {
|
||||
candidate.created_at > current.created_at
|
||||
|| (candidate.created_at == current.created_at && candidate.id < current.id)
|
||||
};
|
||||
let mut winners = HashMap::<PublicKey, &Event>::new();
|
||||
for event in promoted {
|
||||
winners
|
||||
.entry(event.pubkey)
|
||||
.and_modify(|current| {
|
||||
if is_preferred(event, current) {
|
||||
*current = event;
|
||||
}
|
||||
})
|
||||
.or_insert(event);
|
||||
}
|
||||
|
||||
let removable: HashSet<EventId> = self
|
||||
.find_state(identifier)
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
winners.get(&entry.event.pubkey).is_some_and(|winner| {
|
||||
is_preferred(winner, &entry.event)
|
||||
&& !can_apply_state(&entry.event, source_repo_path)
|
||||
})
|
||||
})
|
||||
.map(|entry| entry.event.id)
|
||||
.collect();
|
||||
if removable.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let Some(mut entries) = self.state_events.get_mut(identifier) else {
|
||||
return 0;
|
||||
};
|
||||
let before = entries.len();
|
||||
entries.retain(|entry| !removable.contains(&entry.event.id));
|
||||
let removed = before - entries.len();
|
||||
drop(entries);
|
||||
self.state_events
|
||||
.remove_if(identifier, |_, entries| entries.is_empty());
|
||||
removed
|
||||
}
|
||||
|
||||
/// Find state events that could be satisfied by ref updates.
|
||||
///
|
||||
/// Returns state events waiting for this identifier where applying the
|
||||
@@ -2799,6 +2851,58 @@ async fn test_multiple_state_events_same_identifier() {
|
||||
assert!(authors.contains(&keys3.public_key()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_keeps_reconstructable_and_independent_state_fallbacks() {
|
||||
let purgatory = Purgatory::new(PathBuf::new());
|
||||
let author = Keys::generate();
|
||||
let other_author = Keys::generate();
|
||||
let identifier = "state-cleanup";
|
||||
let state = |keys: &Keys, created_at: u64, oid: Option<&str>| {
|
||||
let mut tags = vec![Tag::identifier(identifier)];
|
||||
if let Some(oid) = oid {
|
||||
tags.push(Tag::custom("refs/heads/main", [oid.to_string()]));
|
||||
}
|
||||
EventBuilder::new(Kind::RepoState, "")
|
||||
.tags(tags)
|
||||
.custom_created_at(Timestamp::from_secs(created_at))
|
||||
.finalize(keys)
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let missing_oid = "a".repeat(40);
|
||||
let missing_older = state(&author, 100, Some(&missing_oid));
|
||||
let reconstructable_older = state(&author, 101, None);
|
||||
let promoted = state(&author, 200, None);
|
||||
let missing_newer = state(&author, 300, Some(&missing_oid));
|
||||
let other_coordinate = state(&other_author, 100, Some(&missing_oid));
|
||||
|
||||
for event in [
|
||||
&missing_older,
|
||||
&reconstructable_older,
|
||||
&missing_newer,
|
||||
&other_coordinate,
|
||||
] {
|
||||
purgatory.add_state(event.clone(), identifier.to_string(), event.pubkey, false);
|
||||
}
|
||||
|
||||
let removed = purgatory.remove_unreconstructable_state_replacements(
|
||||
identifier,
|
||||
&[promoted],
|
||||
Path::new("/not/a/git/repository"),
|
||||
);
|
||||
|
||||
assert_eq!(removed, 1);
|
||||
let remaining: HashSet<EventId> = purgatory
|
||||
.find_state(identifier)
|
||||
.into_iter()
|
||||
.map(|entry| entry.event.id)
|
||||
.collect();
|
||||
assert!(!remaining.contains(&missing_older.id));
|
||||
assert!(remaining.contains(&reconstructable_older.id));
|
||||
assert!(remaining.contains(&missing_newer.id));
|
||||
assert!(remaining.contains(&other_coordinate.id));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mixed_pr_events_and_placeholders() {
|
||||
use nostr_sdk::prelude::{Kind, Tag};
|
||||
|
||||
Reference in New Issue
Block a user