Skip to content
Merged
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 @@ -608,6 +608,37 @@ impl SourceFreshnessFenceV1 {
})
&& self.snapshot_is_recently_verified(&state, project_root, shutting_down)
}

/// Whether the last completed proof was sealed from exactly this snapshot.
///
/// Clock age is not part of the answer. A seal or clone backfill can
/// outlive the admission window without the snapshot changing identity.
pub(super) fn proof_describes_snapshot(
&self,
snapshot_content_identity: &ContentDigest,
) -> bool {
let state = self.snapshot();
state.verified_against_source
&& state.source_witness.as_ref().is_some_and(|witness| {
witness
.content_manifest
.describes_snapshot(snapshot_content_identity)
})
}

/// Refresh the admission clock and the git-metadata sample after the
/// sealed digests still matched. The content witness and reconciled
/// epoch stay put: this is the same proof, not a new generation.
fn rebind_admission_clock(&self, git_metadata: identity::GitMetadataFingerprintV1) {
let micros = now_micros().0;
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.git_metadata = git_metadata;
state.last_reconciled_at = Instant::now();
state.verified_against_source = true;
state.freshness_unknown = false;
self.last_reconciled_at_micros
.store(micros, Ordering::Release);
}
}

/// What the cheap Git/stat freshness ladder concluded about the retained
Expand Down Expand Up @@ -1737,6 +1768,45 @@ impl CodeIndexWorktreeSchedulerV1 {
Ok(Some(outcome))
}

/// Record that `metadata` is the generation the live worktree still seals.
///
/// The in-memory fence takes this snapshot. The disk witness, when one
/// exists, is rewritten to this generation id so the next open does not
/// treat the predecessor's proof as a reason to drop it and reseal.
fn accept_unchanged_sealed_snapshot(
&mut self,
metadata: &VerifiedSealedTextGenerationMetadataV1,
git_metadata: identity::GitMetadataFingerprintV1,
stat_signature: String,
source_manifest: SourceContentManifestV1,
prior_witness: Option<&RestoreFreshnessWitnessV1>,
) -> CodeIndexReconcileOutcomeV1 {
let snapshot_content_identity = metadata.snapshot().content_identity.clone();
self.latest_content_identity = Some(snapshot_content_identity.clone());
self.mark_reconciled_retained_generation_state(
git_metadata.clone(),
Some(ReconciledSourceWitnessV1 {
stat_signature: stat_signature.clone(),
content_manifest: source_manifest,
}),
);
if let Some(prior) = prior_witness {
RestoreFreshnessWitnessV1 {
generation_id: metadata.manifest().generation_id.as_str().to_owned(),
git_metadata_signature: git_metadata.stable_signature(),
stat_signature,
repository_parse_identity_digest: prior.repository_parse_identity_digest.clone(),
ignored_source_admissions_digest: prior.ignored_source_admissions_digest.clone(),
ignored_source_paths: Vec::new(),
}
.persist(&self.store_root);
}
CodeIndexReconcileOutcomeV1::Noop(CodeIndexNoopEvidenceV1 {
snapshot_content_identity,
overflow_reconciled: false,
})
}

pub(super) fn reconcile_retained_text_generation_with(
&mut self,
metadata: &VerifiedSealedTextGenerationMetadataV1,
Expand All @@ -1758,10 +1828,14 @@ impl CodeIndexWorktreeSchedulerV1 {
.observe_retained_text_compatibility(metadata)
.is_reusable();
let witness = RestoreFreshnessWitnessV1::load(&self.store_root);
if witness.as_ref().is_some_and(|witness| {
witness.generation_id != metadata.manifest().generation_id.as_str()
|| !witness.ignored_source_paths.is_empty()
}) || !self.ignored_source_admissions.is_empty()
// A predecessor freshness witness is not a reason to drop this
// generation. It names the proof that sealed an earlier snapshot.
// Ignored-source rosters still require the complete capture: their
// digest is not the ordinary file manifest this path compares.
if witness
.as_ref()
.is_some_and(|witness| !witness.ignored_source_paths.is_empty())
|| !self.ignored_source_admissions.is_empty()
{
return Ok(None);
}
Expand All @@ -1788,37 +1862,54 @@ impl CodeIndexWorktreeSchedulerV1 {
// generation's sealed file digests; its matching stat signature is
// the negative cache that lets a moved tree skip the byte comparison.
let source_manifest = SourceContentManifestV1::for_snapshot(metadata.snapshot());
if retained_is_reusable
let sealed_bytes_match = retained_is_reusable
&& !has_hints
&& let Some(witness) = witness.as_ref()
&& witness.git_metadata_signature == sampled_metadata.stable_signature()
&& witness.stat_signature == sampled_sweep.signature
&& sampled_sweep.content_matches(
&self.project_root,
&source_manifest,
&self.shutting_down,
)
);
let quiet_witness = sealed_bytes_match
&& witness.as_ref().is_some_and(|witness| {
witness.git_metadata_signature == sampled_metadata.stable_signature()
&& witness.stat_signature == sampled_sweep.signature
});
// Identical source bytes do not make a moved commit or branch the same
// generation. `finish_retained_reconcile` rebuilds on exactly this
// drift, and branch-scoped reads resolve generations by their sealed
// `source_revision`, so accepting here would leave the retained
// generation attributed to a commit the checkout has left for as long
// as the bytes hold still. `self.identity` was re-resolved above, so
// this costs no extra walk. A snapshot sealed without a revision
// (a dirty capture) has no commit attribution to invalidate.
let sealed_attribution_is_current = metadata.snapshot().reference.as_ref()
== self.identity.head_ref()
&& metadata
.snapshot()
.source_revision
.as_ref()
.is_none_or(|sealed| self.identity.head_commit() == Some(sealed));
// Graph-on refuses to decode the sealed generation just because the
// predecessor witness, or a git-index mtime this seal itself moved,
// does not name this generation. The sealed digests are the proof.
// Graph-off still captures so a metadata-only drift is verified
// without a full decode when the quiet witness is absent.
if sealed_bytes_match
&& sealed_attribution_is_current
&& (quiet_witness || !rebuild_changed_source_without_decode)
{
let snapshot_content_identity = metadata.snapshot().content_identity.clone();
self.latest_content_identity = Some(snapshot_content_identity.clone());
self.mark_reconciled_retained_generation_state(
return Ok(Some(self.accept_unchanged_sealed_snapshot(
metadata,
sampled_metadata,
Some(ReconciledSourceWitnessV1 {
stat_signature: sampled_sweep.signature,
content_manifest: source_manifest,
}),
);
return Ok(Some(CodeIndexReconcileOutcomeV1::Noop(
CodeIndexNoopEvidenceV1 {
snapshot_content_identity,
overflow_reconciled: false,
},
sampled_sweep.signature,
source_manifest,
witness.as_ref(),
)));
}

// A compatible generation whose witness did not prove a quiet tree
// falls through to the full graph-on reconcile. An incompatible
// lightweight owner rebuilds here without decoding the retained graph.
// A compatible generation whose bytes moved falls through to the full
// graph-on reconcile. An incompatible lightweight owner rebuilds here
// without decoding the retained graph.
if retained_is_reusable && !rebuild_changed_source_without_decode {
return Ok(None);
}
Expand Down Expand Up @@ -2813,6 +2904,50 @@ impl CodeIndexWorktreeSchedulerV1 {
.source_currency_witness_for(generation_id, snapshot_content_identity)
}

/// Bind a sealed snapshot to the source proof, renewing an expired clock
/// when the sealed digests still match.
///
/// The admission window is 30s. This does not move the clone-successor
/// copy off the publication advance. It only stops an expired clock, or a
/// predecessor disk witness, from clearing the generation those digests
/// already name. A hook epoch or a digest mismatch still refuses.
pub(super) fn currency_witness_for_sealed_snapshot(
&self,
generation_id: &CodeGenerationId,
snapshot_content_identity: &ContentDigest,
) -> Option<ServingSourceWitnessV1> {
if self.shutting_down.load(Ordering::Acquire) {
return None;
}
if self.freshness_fence.serves_recently_verified_source(
snapshot_content_identity,
&self.project_root,
&self.shutting_down,
) {
return self
.freshness_fence
.source_currency_witness_for(generation_id, snapshot_content_identity);
}
if !self
.freshness_fence
.proof_describes_snapshot(snapshot_content_identity)
|| self.freshness_fence.source_change_pending()
{
return None;
}
let freshness = self.freshness_fence.snapshot();
if !self.source_witness_matches_worktree(&freshness) {
return None;
}
// Sample after the walk. `gix::open` inside the digest comparison can
// move index metadata; storing the post-walk sample is what keeps the
// next probe from calling that side effect a new generation.
let git_metadata = identity::GitMetadataFingerprintV1::capture(&self.project_root);
self.freshness_fence.rebind_admission_clock(git_metadata);
self.freshness_fence
.source_currency_witness_for(generation_id, snapshot_content_identity)
}

/// A cheap stat-level (path, mtime, size) signature of the present source
/// candidates. It opens gix and runs stat-based status (no byte reads, no
/// content hashing). A changed signature skips straight to reconcile; an
Expand Down Expand Up @@ -3252,6 +3387,19 @@ impl CodeIndexWorktreeSchedulerV1 {
self.publication.sealed_decode_count()
}

/// Age the admission clock past its own threshold without touching source.
#[cfg(test)]
pub(super) fn expire_source_proof_for_test(&self) {
let mut state = self
.freshness_fence
.state
.lock()
.unwrap_or_else(PoisonError::into_inner);
state.last_reconciled_at = Instant::now()
.checked_sub(state.staleness_threshold + Duration::from_secs(1))
.unwrap_or_else(Instant::now);
}

#[cfg(any(test, feature = "test-helpers"))]
pub fn poison_decoded_publication_cache_for_test(&self) {
self.publication.poison_decoded_cache_for_test();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1801,8 +1801,6 @@ impl CodeIndexSchedulerRegistryV1 {
let text_generation = Arc::clone(&worker_text_generation);
let serving_seats = Arc::clone(&worker_serving_seats);
let serving_generation_changed = worker_serving_generation_changed.clone();
let source_freshness = worker_source_freshness.clone();
let project_root = worker_project_root.clone();
let text_latest = latest.clone();
let latest = latest.clone();
let shutting_down = Arc::clone(&worker_shutting_down);
Expand Down Expand Up @@ -1836,13 +1834,15 @@ impl CodeIndexSchedulerRegistryV1 {
// proofs to the seat. Asking the fence whether it
// has verified *this* sealed snapshot is what makes
// the binding truthful for a seat this pass did not
// publish.
let pass_proves_latest = source_freshness
.serves_recently_verified_source(
&latest.generation().snapshot().content_identity,
&project_root,
&shutting_down,
);
// publish. An expired clock, or a git-index sample
// this seal moved, is not a different snapshot.
// Dropping the witness here cleared the newer
// generation. The lexical full-copy is not decided
// on this swap.
let sealed_currency = scheduler.currency_witness_for_sealed_snapshot(
&latest.generation().manifest().generation_id,
&latest.generation().snapshot().content_identity,
);
let mut serving = serving_generation
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
Expand Down Expand Up @@ -1883,17 +1883,7 @@ impl CodeIndexSchedulerRegistryV1 {
*serving_source_witness
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
pass_proves_latest
.then(|| {
source_freshness.source_currency_witness_for(
&latest.generation().manifest().generation_id,
&latest
.generation()
.snapshot()
.content_identity,
)
})
.flatten();
sealed_currency;
}
// The durable pointer names a successor, so no
// proof of this seat's currency exists to bind.
Expand Down
Loading
Loading