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
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ mod query_authority;
mod reconcile_failure_isolation_tests;
mod scope_identity;
#[cfg(test)]
mod seat_swap_tests;
#[cfg(test)]
mod serving_readiness_tests;
mod serving_reads;

Expand Down Expand Up @@ -352,6 +354,42 @@ impl ServingSwapOutcomeV1 {
}
}

/// The prepared generation id after a graph-activation failure.
///
/// Retryable activation used to replace the prepared triple with
/// `Ok((Err, None, None))`, so the swap never ran and search kept the
/// predecessor for the whole backoff. Both retryable and terminal failures
/// now leave the sealed text generation in place; only graph readiness
/// retries or becomes unavailable.
pub(super) fn serving_generation_after_activation_failure<'a>(
prepared_generation: Option<&'a str>,
retryable: bool,
repeated_conflict: bool,
) -> Option<&'a str> {
if activation_failure_keeps_serving_candidate(retryable, repeated_conflict) {
prepared_generation
} else {
None
}
}

fn activation_failure_keeps_serving_candidate(retryable: bool, repeated_conflict: bool) -> bool {
// `retryable && !repeated_conflict` used to wipe the candidate. Terminal
// failures already kept it. Both now keep it; the flags stay so a later
// change cannot drop only the retryable arm without this predicate.
let _ = (retryable, repeated_conflict);
true
}

/// An unfinished text projection withholds the serving seat only when exact
/// or lexical owners are still missing.
///
/// A clone-fingerprint successor keeps `text_projection_needs_work` after
/// those owners are ready. That is not `published_text_owner_unfinished`.
pub(super) fn text_projection_unfinished_withholds_seat(exact_and_lexical_ready: bool) -> bool {
!exact_and_lexical_ready
}

#[cfg(any(test, feature = "test-helpers"))]
struct ColdMountFinalCommitGateV1 {
project_root: PathBuf,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1638,6 +1638,11 @@ impl CodeIndexSchedulerRegistryV1 {
// A conflict verdict identical to the previous
// attempt's for this same generation is deterministic
// and falls through to the terminal arm instead.
//
// The prepared text candidate stays. Wiping it to
// `Ok((Err, None, None))` skipped the serving swap,
// so search kept the predecessor while graph backoff
// ran.
if error.is_retryable_activation() && !repeated_conflict {
last_seat_conflict = error
.activation_conflict_context()
Expand All @@ -1654,7 +1659,7 @@ impl CodeIndexSchedulerRegistryV1 {
retry_delay_micros = retry_delay.as_micros() as u64,
error = %error,
"graph activation failed retryably; the sealed generation \
stays unseated until the scheduled retry"
still seats and the next pass retries native graph"
);
hotpath::gauge!("daemon.code_index.graph_seat.retry_total")
.inc(1_u64);
Expand All @@ -1668,7 +1673,12 @@ impl CodeIndexSchedulerRegistryV1 {
// The scheduled retry is the seat attempt, so it
// must not be turned away as already attempted.
graph_seat_attempted = None;
result = Ok((Err(error), None, None));
if !super::activation_failure_keeps_serving_candidate(
error.is_retryable_activation(),
repeated_conflict,
) {
result = Ok((Err(error), None, None));
}
} else {
next_seat_attempt_at = None;
seat_retry_backoff = ACTIVATION_RETRY_BACKOFF_FLOOR;
Expand Down Expand Up @@ -1699,6 +1709,18 @@ impl CodeIndexSchedulerRegistryV1 {
// and serving-swap boundary. Graph work above ran only when
// the outcome was ready.
if let Some(outcome) = published_text_projection_outcome.take() {
// A clone-fingerprint successor is still `Unfinished` work
// after exact and lexical owners are ready. That must not
// clear the prepared generation the way a missing owner does.
let owners_ready = exact_and_lexical_ready_for_graph(graph_text.as_ref());
let outcome = match outcome {
PublishedTextProjectionOutcomeV1::Unfinished
if !super::text_projection_unfinished_withholds_seat(owners_ready) =>
{
PublishedTextProjectionOutcomeV1::Finished
}
other => other,
};
match outcome {
PublishedTextProjectionOutcomeV1::Finished => {
// The seat needs only the ready exact/lexical
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::code_index_scheduler::CodeIndexSchedulerErrorV1;

use super::ServingSwapOutcomeV1;

/// A retryable native-graph failure must not drop the generation search will
/// serve. The swap installs that same id; graph activation retries beside it.
#[test]
fn retryable_activation_keeps_the_serving_generation_matched() {
let error = CodeIndexSchedulerErrorV1::GraphActivation(
"graph runtime unavailable during activation".to_owned(),
);
assert!(
error.is_retryable_activation(),
"GraphActivation is the retryable class that used to erase the seat candidate"
);
let prepared = Some("generation.head");
let seated = super::serving_generation_after_activation_failure(
prepared,
error.is_retryable_activation(),
false,
);
assert_eq!(
seated, prepared,
"retryable graph activation must leave the prepared generation on the seat"
);
let outcome = ServingSwapOutcomeV1::decide(true, true, seated.is_some());
assert!(
outcome.installs(),
"the serving swap still writes the slot when the candidate survives: {outcome:?}"
);
}

/// Clone-fingerprint backfill is still unfinished after exact and lexical
/// owners are ready. That successor is not `published_text_owner_unfinished`.
#[test]
fn unfinished_clone_fingerprint_successor_is_not_text_projection_unfinished() {
assert!(
!super::text_projection_unfinished_withholds_seat(true),
"ready exact and lexical owners must still seat while the clone successor runs"
);
assert!(
super::text_projection_unfinished_withholds_seat(false),
"missing exact or lexical owners still withhold the seat"
);
}
Loading