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"
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ use sha2::{Digest, Sha256};
use tracedecay_code_extraction::incremental::ParseLimits;
use tracedecay_code_index::{
capabilities::expected_seal_digest,
chunks::{CodeIndexImportEvidenceV1, ExtractionAdmittedCodeSearchChunkV1, content_digest},
chunks::{
CodeIndexEdgeAbstentionReasonV1, CodeIndexImportEvidenceV1,
ExtractionAdmittedCodeSearchChunkV1, content_digest,
},
clones::{CloneBodyEligibilityV1, CloneBodyOccurrenceV1},
graph_projection::{
CODE_GRAPH_PROJECTOR_REVISION, CodeGraphProjectionError,
Expand All @@ -37,7 +40,7 @@ use tracedecay_code_index::{
};
use tracedecay_domain::{
ChunkerRevision, CodeGenerationId, CodeGenerationManifestV1, CodeSearchChunkGrainV1, CommitId,
EdgeAuthorityV1, ExtractorRevision, FileOccurrenceId, LanguageId, ManifestDigest,
EdgeAuthorityV1, EdgeKind, ExtractorRevision, FileOccurrenceId, LanguageId, ManifestDigest,
PolicyRevisionId, PrivacyDomainId, ProjectId, ProjectionBatchRequestV1, ProjectionKeyV1,
ProjectionKindV1, ProjectionOperationV1, ProjectionOutcomeV1, ProviderEvaluationStateV1, RefId,
RelationEdgeKindV1, RepositoryDirtyStateV1, RepositoryId, SanitizationReceiptId,
Expand Down Expand Up @@ -376,6 +379,59 @@ pub(super) fn request_with_source(
request
}

#[test]
fn relation_free_function_batch_seals_without_canonical_edges() {
let source = (0..128_u32)
.map(|symbol_index| {
format!(
"pub fn refresh_probe_0000_{symbol_index:03}(input: u32) -> u32 {{ input + {symbol_index} }}\n"
)
})
.collect::<String>();
let generation = CodeIndexProductionOwnerV1::new(
config(),
SharedPublicationStore::default(),
ApplyingProjectionSink,
)
.expect("production owner")
.build_and_publish(
request_with_source(
"file.relation-free",
1_100_000,
"commit.relation-free",
"tree.relation-free",
&source,
),
&ActiveControl,
)
.expect("relation-free generation publishes");

let statistics = generation
.generation_statistics()
.expect("generation census");
assert_eq!(statistics.symbol_count, 128);
assert_eq!(statistics.edge_count, 0);
assert!(
generation.edges().is_empty(),
"independent functions must not fabricate canonical relations: {:?}",
generation.edges()
);
assert_eq!(
generation
.edge_abstentions()
.iter()
.filter(|abstention| {
abstention.source_node_id.starts_with("file:")
&& abstention.legacy_kind == EdgeKind::Contains.as_str()
&& abstention.reason == CodeIndexEdgeAbstentionReasonV1::MissingSymbolEndpoint
})
.count(),
128,
"each file-root Contains observation must abstain: {:?}",
generation.edge_abstentions()
);
}

#[test]
fn cross_file_edges_require_path_binding_evidence() {
let sources = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ use tracedecay::daemon::ProductionProjectCompositionHarnessV1;
use tracedecay_mcp::JsonRpcResponse;

const RECEIPT_TIMEOUT: Duration = Duration::from_secs(90);
const RELATION_FREE_BACKGROUND_FILE_COUNT: u32 = 768;
const RELATION_FREE_FUNCTIONS_PER_FILE: u32 = 128;

fn git(project: &Path, args: &[&str]) {
let output = Command::new("git")
Expand Down Expand Up @@ -226,12 +228,14 @@ async fn wait_for_background_refresh(
.unwrap_or_else(|_| panic!("reopen omitted background-refresh status: {last_status}"));
}

fn install_background_batch(isolation_root: &Path, project: &Path) {
fn install_relation_free_background_batch(isolation_root: &Path, project: &Path) {
let staging = isolation_root.join("refresh-batch-staging");
fs::create_dir_all(&staging).expect("background batch staging directory");
for file_index in 0..768_u32 {
// This is a throughput fixture, not an edge fixture: every function is
// independent, so the sealed canonical edge census is intentionally zero.
for file_index in 0..RELATION_FREE_BACKGROUND_FILE_COUNT {
let mut source = String::new();
for symbol_index in 0..128_u32 {
for symbol_index in 0..RELATION_FREE_FUNCTIONS_PER_FILE {
writeln!(
source,
"pub fn refresh_probe_{file_index:04}_{symbol_index:03}(input: u32) -> u32 {{ input + {symbol_index} }}"
Expand Down Expand Up @@ -269,7 +273,7 @@ async fn background_refresh_and_reopen_report_only_servable_generations_inner()
let initial_generation =
wait_for_current_generation(&harness, &project, &initial_revision, "before_reopen").await;

install_background_batch(isolation.path(), &project);
install_relation_free_background_batch(isolation.path(), &project);
commit_all(&project, "install background refresh batch");
let refreshed_revision = head(&project);
let receipt = tool(
Expand Down
Loading