Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/tracedecay-cli/src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,8 @@ fn print_freshness_row(freshness: Option<&CodeIndexWorktreeFreshnessV1>, inner_w
format_relative_time(unix_seconds_from_micros(sealed))
));
}
if let Some(state) = freshness.staleness_state.as_deref() {
parts.push(state.to_owned());
if let Some(state) = freshness.staleness_state {
parts.push(state.as_str().to_owned());
}
if parts.is_empty() {
"no sealed generation yet".to_owned()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,13 @@ fn is_terminal_publication_authority_park(parked: &CodeIndexConvergenceParkedV1)
parked.blocked_reason == Some(CodeIndexBuildBlockedReasonV1::PublicationAuthorityCorrupt)
}

fn publication_authority_is_terminal(slot: &RwLock<Option<CodeIndexConvergenceParkedV1>>) -> bool {
slot.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.is_some_and(is_terminal_publication_authority_park)
}

/// Record one observation of a deterministic contract violation on a mounted
/// worktree's park slot.
///
Expand Down Expand Up @@ -923,6 +930,17 @@ mod terminal_publication_park_tests {
}
}

#[test]
fn publication_authority_terminal_is_the_shared_slot_not_task_local() {
let slot = RwLock::new(None);
assert!(!publication_authority_is_terminal(&slot));
*slot
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(terminal_park("planted by another actor"));
assert!(publication_authority_is_terminal(&slot));
}

#[test]
fn clear_convergence_park_preserves_terminal_publication_corruption() {
let slot = RwLock::new(Some(terminal_park("corrupt-authority")));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,18 @@ async fn a_legacy_permissive_text_artifacts_root_self_heals_and_serves() {
.await;

let observed = fixture
.wait_for_freshness(|freshness| freshness.staleness_state.as_deref() == Some("fresh"))
.wait_for_freshness(|freshness| {
freshness.staleness_state
== Some(
tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh,
)
})
.await
.expect("freshness projection for the mounted worktree");

assert_eq!(
observed.staleness_state.as_deref(),
Some("fresh"),
observed.staleness_state,
Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh),
"the healed store must converge to serving instead of warming forever: {observed:?}"
);
assert!(
Expand Down Expand Up @@ -221,8 +226,8 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed()
)
});
assert_eq!(
parked.staleness_state.as_deref(),
Some("parked"),
parked.staleness_state,
Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked),
"status must report parked, not indexing/warming: {parked:?}"
);
assert!(
Expand All @@ -249,7 +254,7 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed()

let recovered = fixture
.wait_for_freshness(|freshness| {
freshness.parked.is_none() && freshness.staleness_state.as_deref() == Some("fresh")
freshness.parked.is_none() && freshness.staleness_state == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh)
})
.await
.expect("freshness projection for the mounted worktree");
Expand All @@ -259,8 +264,8 @@ async fn an_unhealable_text_artifacts_root_parks_typed_and_recovers_when_fixed()
"the park must clear once the violation is removed: {recovered:?}"
);
assert_eq!(
recovered.staleness_state.as_deref(),
Some("fresh"),
recovered.staleness_state,
Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh),
"convergence must resume on the ordinary wake cadence after the fix: {recovered:?}"
);
let mode = fs::metadata(&fixture.artifacts_root)
Expand Down Expand Up @@ -301,8 +306,8 @@ async fn fresh_graph_activation_waits_while_the_published_text_owner_is_parked()
.await
.expect("freshness projection for the mounted worktree");
assert_eq!(
parked.staleness_state.as_deref(),
Some("parked"),
parked.staleness_state,
Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked),
"the text owner must park on the unhealable root: {parked:?}"
);

Expand All @@ -318,8 +323,8 @@ async fn fresh_graph_activation_waits_while_the_published_text_owner_is_parked()
.await
.expect("freshness projection for the mounted worktree");
assert_eq!(
observed.staleness_state.as_deref(),
Some("parked"),
observed.staleness_state,
Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Parked),
"waiting graph activation must not unpark an owner that has not finished: {observed:?}"
);
assert!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ use super::{
PendingWakeV1, PublishedTextProjectionOutcomeV1, ServingSwapOutcomeV1,
TEXT_PROJECTION_DOCUMENTS_PER_PASS_V1, clear_convergence_park,
convergence_park_retries_on_wake, is_repeated_conflict_verdict, park_convergence,
retained_noop_requires_follow_up_wake,
publication_authority_is_terminal, retained_noop_requires_follow_up_wake,
};

impl CodeIndexSchedulerRegistryV1 {
Expand Down Expand Up @@ -385,7 +385,6 @@ impl CodeIndexSchedulerRegistryV1 {
// artifact build. Releasing that capacity emits no wake, so this
// worker must schedule its own.
let mut capacity_retry = ReconcileCapacityRetryV1::new();
let mut publication_authority_terminal = false;
// The last arrival this worker restored for a nothing-seated
// warming outcome. A quiet remount's seat pass restores its
// arrival exactly once so the next pass can restore and warm the
Expand Down Expand Up @@ -428,7 +427,11 @@ impl CodeIndexSchedulerRegistryV1 {
.await;
return;
}
if publication_authority_terminal {
// The shared park slot is the authority admission already
// consults. A task-local bool would ignore a park planted by
// another actor and would reset if this loop state were lost
// while the slot remained.
if publication_authority_is_terminal(&worker_convergence_park) {
let _ = Self::take_pending_arrival(
&worker_pending_wake,
CodeIndexCadenceTriggerV1::Mount,
Expand Down Expand Up @@ -2097,7 +2100,6 @@ impl CodeIndexSchedulerRegistryV1 {
// Mid-wait branch publication rechecks the park on
// serving-generation notifications.
worker_serving_generation_changed.send_replace(());
publication_authority_terminal = true;
} else if transient_capacity {
// Shared process capacity was held by another
// holder when this pass asked for it. Releasing
Expand Down Expand Up @@ -2169,7 +2171,7 @@ impl CodeIndexSchedulerRegistryV1 {
),
Ok((Ok(_), _, _)) => {}
}
if !publication_authority_terminal {
if !publication_authority_is_terminal(&worker_convergence_park) {
// Restore arrival so the next pass measures this wake's full queue wait.
Self::restore_pending_arrival(&worker_pending_wake, arrival, trigger);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,45 @@ async fn retire_and_remount_clears_terminal_publication_park_for_new_admission()
fixture.registry.shutdown().await;
}

/// A park written by an actor other than this worker must still suppress
/// the loop. The old worker-local bool stayed false here, so later wakes
/// reconciled against a terminal publication authority.
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn planted_terminal_publication_park_suppresses_later_wakes() {
let fixture = Fixture::mount("project.reconcile-planted-publication-park").await;
let fault = fixture
.install_fault(ReconcileFaultKindV1::Permanent, usize::MAX)
.await;
fixture
.plant_terminal_publication_park("planted by an actor other than the worker")
.await;

fixture.wake_with_pending_arrival().await;
fixture.drive_external_wakes().await;
fixture.settle_for(TERMINATION_QUIET_WINDOW).await;

assert_eq!(
fault.attempts(),
0,
"a shared terminal park must stop the worker without a task-local latch"
);
assert_eq!(
fixture.pending_wake_micros().await,
0,
"the suppressed wake must drain the pending arrival"
);
let freshness = fixture
.registry
.dashboard_freshness(&fixture.project)
.await
.expect("mounted freshness");
assert!(
!freshness.rebuild_in_flight,
"a terminal park is not a rebuild: {freshness:?}"
);
fixture.registry.shutdown().await;
}

fn run_git_in(root: &Path, args: &[&str]) {
let output = Command::new("git")
.args(args)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,12 +360,23 @@ impl CodeIndexSchedulerRegistryV1 {
graph_activation_enabled,
&code_graph_serving,
);
let verifying = ready && refresh_in_flight && !source_change_pending;
let refreshing = refresh_in_flight && !verifying;
let rebuild_in_flight = refreshing;
let stale = hook_hint_count != Some(0);
let observation = tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderV1::project(
tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderInputsV1 {
ready,
refresh_in_flight,
source_change_pending,
parked: parked.as_ref(),
// The scheduler lock is held by the pass this read
// could not join, so verification is unknown here.
source_verified: None,
hook_hint_count,
},
);
let clone_index = text.as_ref().map_or_else(Default::default, |text| {
text.clone_index_status(stale || refreshing, clone_update)
text.clone_index_status(
hook_hint_count != Some(0) || observation.rebuild_in_flight,
clone_update,
)
});
let last_reconcile_micros = match last_reconciled_at_micros
.load(Ordering::Acquire)
Expand All @@ -378,38 +389,10 @@ impl CodeIndexSchedulerRegistryV1 {
code_graph_serving,
clone_index: Some(clone_index),
last_reconcile_micros,
rebuild_in_flight,
staleness_state: Some(
if parked.is_some() && !ready {
"parked"
} else if verifying {
"verifying"
} else if refreshing {
if ready {
"refreshing"
} else {
"indexing"
}
} else if stale && ready {
"stale"
} else if ready {
"fresh"
} else {
"indexing"
}
.to_owned(),
),
rebuild_in_flight: observation.rebuild_in_flight,
staleness_state: Some(observation.staleness_state),
hook_hint_count,
coverage: if refreshing {
"partial_refresh_in_progress"
} else if verifying {
"partial_source_verification"
} else if hook_hint_count.is_some() {
"complete"
} else {
"partial_hook_hint_overflow"
}
.to_owned(),
coverage: observation.coverage,
progress,
parked,
generation_recovery,
Expand Down Expand Up @@ -451,33 +434,19 @@ impl CodeIndexSchedulerRegistryV1 {
graph_activation_enabled,
&code_graph_serving,
);
let verifying = ready && refresh_in_flight && !source_change_pending;
let refreshing = refresh_in_flight && !verifying;
let rebuild_in_flight = refreshing;
let observation = tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderV1::project(
tracedecay_contracts::code_index_freshness::CodeIndexFreshnessLadderInputsV1 {
ready,
refresh_in_flight,
source_change_pending,
parked: parked.as_ref(),
source_verified: Some(verified),
hook_hint_count,
},
);
let clone_index = text.as_ref().map_or_else(Default::default, |text| {
text.clone_index_status(stale || refreshing, clone_update)
text.clone_index_status(stale || observation.rebuild_in_flight, clone_update)
});
let staleness_state = if parked.is_some() && !ready {
"parked"
} else if verifying {
"verifying"
} else if refreshing {
if ready {
"refreshing"
} else {
"indexing"
}
} else if stale || hook_hint_count != Some(0) {
if ready {
"stale"
} else {
"indexing"
}
} else if ready {
"fresh"
} else {
"indexing"
};
let identity = if text.is_some() {
dashboard_text_freshness_identity(text.as_ref())
} else {
Expand All @@ -488,21 +457,10 @@ impl CodeIndexSchedulerRegistryV1 {
code_graph_serving,
clone_index: Some(clone_index),
last_reconcile_micros: scheduler.last_reconciled_at_micros(),
rebuild_in_flight,
staleness_state: Some(staleness_state.to_owned()),
rebuild_in_flight: observation.rebuild_in_flight,
staleness_state: Some(observation.staleness_state),
hook_hint_count,
coverage: if refreshing {
"partial_refresh_in_progress"
} else if verifying {
"partial_source_verification"
} else if !verified {
"partial_unverified_restore"
} else if hook_hint_count.is_some() {
"complete"
} else {
"partial_hook_hint_overflow"
}
.to_owned(),
coverage: observation.coverage,
progress,
parked,
generation_recovery,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1505,8 +1505,8 @@ async fn wait_for_dashboard_ready(registry: &CodeIndexSchedulerRegistryV1, path:
.dashboard_freshness(path)
.await
.is_some_and(|freshness| {
freshness.staleness_state.as_deref() == Some("fresh")
&& freshness.coverage == "complete"
freshness.staleness_state == Some(tracedecay_contracts::code_index_freshness::CodeIndexStalenessStateV1::Fresh)
&& freshness.coverage == tracedecay_contracts::code_index_freshness::CodeIndexFreshnessCoverageV1::Complete
});
if ready {
break;
Expand Down
Loading
Loading