Skip to content

phase 6a - replay functionality [source cache-scoped] - #1045

Open
kans wants to merge 1 commit into
mainfrom
kans/phase-6a
Open

phase 6a - replay functionality [source cache-scoped]#1045
kans wants to merge 1 commit into
mainfrom
kans/phase-6a

Conversation

@kans

@kans kans commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Sync Replay Phase 6a for the Pebble-backed dotc1z store, and secondarily, runs the experiment of using a semi-formal verification plan, created independently of the implementation.

This adds:

  • Source-scope stamping for connector-written resources, entitlements, and grants
  • Atomic maintenance of source-scope indexes through typed rawdb mutations
  • Durable source-cache manifests
  • Scope- and row-kind-isolated replay from a previous artifact
  • Retry-safe, bounded-batch replay and scoped tombstones
  • Pure-replay replacement semantics
  • Canonical and principal tombstones
  • Reset, cleanup, clone, reopen, and capability integration
  • Forward cacheability so replay output can serve as a subsequent replay source

Verification record

This PR was developed and tested against an implementation-blind verification plan.

The plan defines C01–C43, their oracles, coverage levels, failure models, and closure requirements. The evidence record states exactly which criteria are verified, sampled, incomplete, excluded, or deferred.

Verification-driven fixes

The verification work found and corrected defects involving:

  • Missing, wrong-kind, empty-validator, and invalidated manifest handling
  • Source primary/index corruption preflight
  • Occupied-destination replacement
  • Partial tombstone application
  • Malformed-row obligation cleanup
  • Replay from the same handle, path, or filesystem alias
  • Retry, cancellation, and interrupted-commit behavior
  • Timestamp and source-artifact preservation
  • Forward replay cacheability
  • Typed mutation-path atomicity
  • Unbounded scoped principal/resource tombstone batches

Scoped tombstone operations now commit in bounded chunks. If a later chunk fails, they report only committed progress, preserve primary/index agreement, mark the store dirty, and converge on retry.

Change orders

The committed plan records four post-freeze change orders:

  1. Clarified enforcement ownership between the public capability, engine replay, and deferred syncer orchestration.
  2. Documented symmetry and representative reductions instead of claiming a literal P1–P10 Cartesian expansion.
  3. Extended bounded-memory enforcement to scoped tombstone scans.
  4. Assigned manifest row-count optimization to a separate stacked follow-up PR.

Deliberately deferred

This PR does not implement:

  • Syncer/checkpoint orchestration
  • Compatibility matching or gating
  • Connector continuation/RPC behavior
  • Manifest invalidation policy
  • Compacted/non-FULL source eligibility
  • Compactor integration
  • Post-replay ingest-invariant evaluation
  • Scope-count replay optimization

Executable exclusions identify API-boundary cells that Phase 6a cannot represent. An exclusion is not counted as a behavioral pass.

Evidence

Passing gates:

make lint
go test ./pkg/sourcecache ./pkg/dotc1z/engine/pebble ./pkg/dotc1z
go test -race ./pkg/dotc1z ./pkg/dotc1z/engine/pebble -run '^TestVerification' -count=1

@kans
kans requested a review from a team July 29, 2026 04:16
@kans kans changed the title phase 6a - replay functionality [WIP] phase 6a - replay functionality Jul 29, 2026
Comment on lines +508 to +536
var scanned int
for primaries.First(); primaries.Valid(); primaries.Next() {
scanned++
if scanned&0x3FF == 0 {
if err := ctx.Err(); err != nil {
primaries.Close()
return err
}
}
stamp, err := rawdb.ScanSourceScopeKeyRaw(primaries.Value(), scopeField)
if err != nil {
primaries.Close()
return fmt.Errorf("source cache replay: preflight %s primary %x: %w", rowKind, primaries.Key(), err)
}
if stamp != scopeKey {
continue
}
indexKey, ok := rawdb.AppendBySourceScopeKeyFromPrimary(nil, primaries.Key(), scopeKey)
if !ok {
primaries.Close()
return fmt.Errorf("source cache replay: preflight %s primary %x cannot derive source index", rowKind, primaries.Key())
}
_, closer, err := prev.db.Get(indexKey)
if err != nil {
primaries.Close()
return fmt.Errorf("source cache replay: preflight %s primary %x missing source index: %w", rowKind, primaries.Key(), err)
}
closer.Close()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: This preflight iterates the entire primary keyspace of the row kind in prev on every replay call, skipping only the per-primary Get for non-matching stamps. Since validateReplaySourceScope runs once per scope (via each ReplaySourceCache*), a sync that replays S scopes against a previous file of N rows is O(S·N) — a quadratic cliff for delta-query connectors with many small scopes and a large prior sync. The single-scope benchmark (source_cache_replay_bench_test.go puts all rows under one scope) scales linearly and won't detect this. Per the repo cost-contract criteria, consider a multi-scope benchmark that pins the cost curve, and/or bounding the primary-side pass (e.g. persisting a per-scope row count/digest in the manifest so the index-side scan alone suffices). The code comment already acknowledges this as a known limitation. (Confidence: high on the full-keyspace scan; medium on real-world impact — depends on connector scope granularity.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be done in follow up.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

General PR Review: [WIP] phase 6a - replay functionality

Blocking Issues: 0
Suggestions: 0
Threads Resolved: 0

Criteria status: loaded .claude/skills/ci-review.md from trusted base 8c92491eafc5.
Review mode: full
Review run: https://github.com/ConductorOne/baton-sdk/actions/runs/30559396883

Review Summary

Scanned the full PR diff for security and correctness issues, plus the baton-sdk repo-local criteria (exported Go API stability, proto/wire contracts, serialized-state round-tripping, default-behavior stability, and go.mod/go.sum).

  • Proto/wire: source_scope_key field numbers (Resource=12, Entitlement=11, Grant=10) are consistent between records.proto, generated records.pb.go, and the raw protowire scanners (ScanSourceScopeKeyRaw). No renumbering or wire-break.
  • Serialized state: New TypeSourceCache (0x0B) manifest family and by-source-scope secondary indexes (0x09/0x0A/0x0B) are additive; no collision with existing keyspace discriminators.
  • Fast-path proofs: freshGrantsEmpty / freshEntitlementsEmpty / freshResourcesEmpty are disarmed on the replay clear/commit boundaries as required by the derived-state-as-proof risk model.
  • Concurrency: beginSourceCacheMutation holds the store closeMu across the whole engine mutation, serializing against Close.

The one real correctness issue found (a Pebble value-handle leak in PutGrantRecordsIfNewer, pkg/dotc1z/engine/pebble/if_newer.go:77-80, where closer from e.db.Get stays open if marshalRecord errors) is already tracked in existing findings and is pre-existing/unchanged since the last reviewed SHA — not re-posted here. The sibling entitlement/resource IfNewer paths close on all paths.

No new blocking security or correctness issues were found. The replay machinery is extensively self-verified.

Security: None found.
Correctness: None found (see note above re: already-tracked pre-existing leak).

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread pkg/dotc1z/engine/pebble/source_cache.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread pkg/dotc1z/engine/pebble/source_cache.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@kans
kans requested a review from mindymo as a code owner July 29, 2026 20:08
Comment on lines +147 to +175
if err != nil {
if errors.Is(err, pebble.ErrNotFound) {
continue
}
return err
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
identities = append(identities, id)
}
if len(identities) == 0 {
return nil
}

batch := e.db.NewRecordBatch()
defer func() { _ = batch.Close() }()
for _, id := range identities {
key := encodeGrantIdentityKey(id)
oldVal, closer, err := e.db.Get(key)
if errors.Is(err, pebble.ErrNotFound) {
continue
}
if err != nil {
return err
}
if err := batch.StageGrantDelete(key, oldVal); err != nil {
_ = closer.Close()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion (low confidence): DeleteGrantRecordsBounded (and the parallel DeleteEntitlementRecords) accumulate every resolved identity into an in-memory identities slice + seen map and stage all tombstones into a single RecordBatch committed once. Memory is O(len(externalIDs)), unlike the replay path which deliberately chunks at replayBatchRows (10k). The single atomic commit is a deliberate all-or-nothing tombstone choice and the delete set is asserted to be delta-sized (see DeleteSourceCacheRows doc), so this is likely fine — but it's exactly the "cleanup/tombstone scan builds O(scope size) batch" class this PR added to docs/BUG_CATCHING.md. Worth a bound or a note if a whole-scope tombstone can ever reach here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up work

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread pkg/dotc1z/engine/pebble/if_newer.go Outdated
Comment thread pkg/dotc1z/engine/pebble/grants.go Outdated
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

General PR Review: [WIP] phase 6a - replay functionality

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 10a6da053799.
Review mode: full
View review run

Review Summary

Full PR diff scanned for security and correctness. This is a HIGH-risk change per the repo triage model: it introduces new durable storage families (by_source_scope indexes, the source-cache manifest keyspace TypeSourceCache=0x0B), a new wire-stamped record field (source_scope_key), and cross-file replay that copies raw values between SDK versions — silent + durable + version-pair-dependent, so failures escape local testing and cost a fleet-wide re-sync. The concrete correctness findings below were already identified in the prior review and remain present in the current tree; no new bugs were found, and no security issues were found. Recommend the durable-format and replay paths get the escalated instrument coverage the trusted criteria calls for (a two-artifact cross-version replay harness plus a golden-artifact corpus for the new index/manifest families), which a single-shot CI review only samples.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/dotc1z/engine/pebble/if_newer.go:77-80 (PutGrantRecordsIfNewer): when getErr == nil, the pebble Get closer leaks if marshalRecord(r) returns an error, since the case-local closer.Close() was removed and only re-added around StageGrantPutInline. Rare error path; PutResourceRecordsIfNewer/PutEntitlementRecordsIfNewer handle it correctly. (previously identified; still present)
  • pkg/dotc1z/engine/pebble/grants.go:277-280 (PutExpandedGrantRecords): same closer-leak shape — marshalRecordAppend error returns while the getErr == nil closer is still held. (previously identified; still present)
  • pkg/dotc1z/engine/pebble/source_cache.go:632-687 (validateReplaySourceScope): the preflight scans the entire previous-file primary family for the row kind on every per-scope replay, so a many-scope delta sync is O(scopes × rows). Acknowledged in-code and deferred to a manifest row-count/digest follow-up; noted for tracking, not blocking. (previously identified)
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/dotc1z/engine/pebble/if_newer.go`:
- Around line 77-80 (PutGrantRecordsIfNewer): when the prior Get succeeded
  (getErr == nil), closer is open. If marshalRecord(r) returns an error the
  function returns without calling closer.Close(), leaking the pebble read
  handle. Close closer on the marshal-error return (mirror
  PutResourceRecordsIfNewer, which closes before returning the marshal error).

In `pkg/dotc1z/engine/pebble/grants.go`:
- Around line 277-280 (PutExpandedGrantRecords): same shape — when getErr == nil
  the Get closer is held while marshalRecordAppend(valScratch[:0], r) runs; the
  error return does not close it. Close closer (guard if closer != nil) before
  returning the marshal error.

In `pkg/dotc1z/engine/pebble/source_cache.go`:
- Around line 632-687 (validateReplaySourceScope): the primary-family pass is
  O(all rows of the kind) per scope, making a many-scope delta sync
  O(scopes x rows). Consider persisting a per-scope row count or digest in the
  manifest so the preflight can be bounded by scope size instead of the whole
  primary family. Tracking only; the code comment already defers this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread pkg/dotc1z/engine/pebble/internal/rawdb/records.go
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

General PR Review: phase 6a - replay functionality [source cache-scoped]

Blocking Issues: 0 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 56e4ff163efd.
Review mode: full
View review run

Review Summary

Scanned the full PR diff (84 files) for security and correctness, with focused reads of the new source-cache replay engine, the by_source_scope typed-record obligations and sourceScopeMayExist gate, the rawdb batch-accounting/leak oracle, compactor invalidation, and the WithPreviousSyncC1ZPath eligibility gate. Two previously-reported closer leaks are now fixed at this SHA: PutGrantRecordsIfNewer and PutExpandedGrantRecords both wrap the per-record body in a closure with defer closer.Close(), and TestGrantMarshalFailureReleasesExistingRowCloser covers both branches. No security issues and no confident correctness/compatibility break found; the two new items below are non-blocking. Several previously-identified items still stand and are re-listed for tracking.

Security Issues

None found.

Correctness Issues

None found (blocking).

Suggestions

  • pkg/dotc1z/source_cache.go:290-295DeleteSourceCacheRows's resource branch deletes one id per engine call, so a tombstone page is N separate durable commits, non-atomic, with no committed-progress reporting, unlike the grant/entitlement branches and the bounded-chunk scoped deletes. (new, medium confidence)
  • pkg/dotc1z/engine/pebble/internal/rawdb/records.go:349-353StageResourceDelete now swallows the malformed prior-value error and falls back to a full by_parent family scan per row (O(deleted × index size), silent); StageResourcePut still hard-fails on the same input. (new, medium confidence)

Previously identified, still present at this SHA — not re-flagged inline:

  • pkg/dotc1z/engine/pebble/source_cache.go:648-753 (validateReplaySourceScope) — per-scope preflight scans the whole previous-file primary family, so a many-scope delta sync is O(scopes × rows); acknowledged in-code and deferred to a manifest row-count/digest follow-up.
  • pkg/dotc1z/engine/pebble/source_cache.go:60-67SourceCacheReplayResult.StaleSkipped is permanently zero because validateReplaySourceScope hard-fails both conditions that would increment it, yet its doc tells future orchestration to gate on it.
  • pkg/synccompactor/compactor_pebble.go:577-582 — fold output keeps by_source_scope indexes while the fold's raw winner writes don't maintain them; contained only by the compacted replay-eligibility gate.
  • Makefile:99 and the new internal/testtier tiers — tests previously run in ci.yaml/main.yaml now require BATON_TEST_EXTRA/BATON_TEST_NIGHTLY.
  • pkg/sourcecache/sourcecache.go:4-26 and :105 — the package doc describes Lookup/SetLookup wiring on resource.SyncOpAttrs that does not exist at this SHA.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/dotc1z/source_cache.go`:
- Around line 290-295: The RowKindResources branch of DeleteSourceCacheRows loops over ids
  calling s.Engine.DeleteResourceRecord one at a time. Each of those calls opens its own
  withWrite scope and does its own batch.Commit(writeOpts(e.opts.durability)), so a page of
  N resource tombstones costs N separate durable commits and is not atomic. The grant and
  entitlement branches below batch all deletes into one RecordBatch and commit once, and the
  scoped variants (DeleteGrantsByPrincipalsInScope, DeleteResourcesByIDsInScope) commit in
  bounded chunks and return committed progress on error. Add an engine method such as
  DeleteResourceRecords(ctx, ids) that resolves all identities, stages every
  StageResourceDelete into one (or bounded-chunk) RecordBatch, and commits once, then call
  that from this branch so resource tombstones match the atomicity and cost profile of the
  other kinds.

In `pkg/dotc1z/engine/pebble/internal/rawdb/records.go`:
- Around line 349-353: StageResourceDelete replaced the previous
  stageResourceParentDelete(oldVal, ...) call, which returned the ScanResourceParentRaw
  error, with a silent fallback to deleteAllResourceParentKeysForChild. That helper iterates
  the entire by_parent index family once per deleted row, so a mass delete over a corrupted
  resource family degrades to O(deleted x |by_parent|) with no error surfaced and no log
  line indicating corruption was encountered. Make the corruption observable — e.g. keep the
  fallback but wrap/record the parse error, or expose a counter — so the degraded path is not
  invisible. Also note the asymmetry with StageResourcePut around line 322, which still
  propagates the same ScanResourceParentRaw error: overwriting a malformed row hard-fails
  while deleting it silently heals. Either align the two paths or document why they
  deliberately differ.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@kans kans changed the title [WIP] phase 6a - replay functionality phase 6a - replay functionality [source cache-scoped] Jul 31, 2026
Comment on lines +89 to +91
func (r SyncRun) UsableAsReplaySource() bool {
return r.Type == connectorstore.SyncTypeFull && !r.Compacted
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: UsableAsReplaySource() gates on !r.Compacted, but the SQLite adapter (getFinishedSync in pkg/dotc1z/sync_runs.go:306-352) never selects or sets Compacted, so it is always false for a SQLite prior run — a compacted SQLite full sync would pass this gate. Impact is currently bounded (the pebble replay path independently re-checks via validateReplaySourceEligible at source_cache.go:129, and replay is pebble-only), but the syncer comment at syncer.go:3735 says a SQLite prior run should work as a replay source. Consider documenting that Compacted is only meaningful for engines that track it, or having the SQLite adapter conservatively derive compaction provenance. (medium confidence)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread Makefile
test-extra: race-check compat-check interrupt-check fuzz-smoke differential-check bench-smoke ## Run bounded confidence checks omitted from CI.

.PHONY: test-nightly
test-nightly: export BATON_TEST_NIGHTLY=1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: neither new tier has an automated runner. .github/workflows/ci.yaml:39 and main.yaml:56 invoke bare go test ./... with neither BATON_TEST_EXTRA nor BATON_TEST_NIGHTLY set, and there is no schedule:/workflow_dispatch: workflow that calls make test-extra or make test-nightly. As a result the 21 newly gated tests — the full deterministic chaos corpora, scheduler soak, WAL-checkpoint race, C1Z integrity, the randomized source-cache lifecycle model, checkpoint-cut enumeration, exhaustive parallel-queue interleavings, and both differential suites — now run only when a human types a Make target, which is coverage removal rather than relocation. Consider adding a nightly scheduled job that runs make test-nightly.

Comment on lines +15 to +29
func RequireExtra(t testing.TB) {
t.Helper()
if os.Getenv(ExtraEnv) == "" && os.Getenv(NightlyEnv) == "" {
t.Skipf("set %s=1 or run the corresponding Make target", ExtraEnv)
}
}

// RequireNightly skips a randomized, repeated, or full-corpus test unless the
// nightly confidence suite explicitly enabled it.
func RequireNightly(t testing.TB) {
t.Helper()
if os.Getenv(NightlyEnv) == "" {
t.Skipf("set %s=1 or run make test-nightly", NightlyEnv)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: both gates test os.Getenv(...) == "", so BATON_TEST_EXTRA=0 (or BATON_TEST_NIGHTLY=false) enables the tier. docs/TESTING.md documents the contract as =1, so a CI matrix or shell profile that sets the variable to 0 in order to opt out would silently opt in instead. Consider accepting only truthy values (an explicit == "1", or strconv.ParseBool on a non-empty value).

// C10/C12: the replay commit seam supplies deterministic evidence that live
// batch cardinality is fixed, and lets retry be cut after one landed chunk.
func TestVerificationReplayBatchBoundAndInterruptedRetry(t *testing.T) {
testtier.RequireExtra(t)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: require.Equal(t, replayBatchRows, highWater) on line 57 is the only assertion anywhere that the live replay path actually honors the production replayBatchRows = 10_000 bound — TestVerificationReplayCommittedPrefixRetryAllKinds lowers the seam to 2, and source_scope_verification_test.go:1113 only exercises validateBatchHighWater in isolation. Gating this test to the extra tier therefore removes CI's only guard against a regression that drops or changes the production batch bound on the new durable replay path. Consider splitting the cheap bound assertion into a CI-tier test and leaving only the 10k-row fixture behind RequireExtra.

Comment thread pkg/sync/syncer.go
Comment on lines +4044 to +4059
run, metaErr := previousSyncStore.SyncMeta().LatestFinishedSyncOfAnyType(ctx)
if metaErr != nil {
closeErr := previousSyncStore.Close(ctx)
if s.previousSyncC1ZPathOptional {
ctxzap.Extract(ctx).Warn("previous-sync c1z metadata unusable; syncing without source-cache replay",
zap.String("previous_sync_c1z_path", s.previousSyncC1ZPath),
zap.Error(errors.Join(metaErr, closeErr)),
)
break
}
return nil, fmt.Errorf(
"error reading previous-sync c1z %q metadata: %w",
s.previousSyncC1ZPath,
errors.Join(metaErr, closeErr),
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this makes NewSyncer hard-fail for WithPreviousSyncC1ZPath callers on a metadata read error, where before this PR no metadata was read at all and the sync proceeded. Note the asymmetry with the branch immediately below — run == nil || !UsableAsReplaySource() only warns and degrades for the same non-optional caller. Since previousSyncReader is currently unconsumed scaffolding (the ETag read path is still t.Skipped in pebble_etag_replay_test.go), this only adds a new way for construction to fail; consider warn-and-degrade here too, reserving hard failure for open failures as the option doc describes. (confidence: medium)

Comment on lines +479 to +489
if mayExist {
oldScope, err := ScanSourceScopeKeyRaw(oldVal, field)
if err != nil {
return err
}
if oldScope != "" && oldScope != newScope {
if err := rb.deleteSourceScopeKey(key, oldScope); err != nil {
return err
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this returns the ScanSourceScopeKeyRaw(oldVal, …) error and fails the whole put, while the delete twin stageSourceScopeCleanup (records.go:452-455) deliberately degrades to deleteAllSourceScopeKeysForPrimary. Net effect: a record whose stored prior value is malformed can never be overwritten (permanent failure) but can be deleted (self-heals) — and before this PR the put path did not parse oldVal at all for grants/entitlements, so overwriting a bad row succeeded. Consider mirroring the cleanup path: on scan error fall back to deleteAllSourceScopeKeysForPrimary(key) and continue to the new-scope Set. (confidence: medium)

Comment on lines +281 to +284
func encodeGrantBySourceScopeIndexKey(scopeKey string, id grantIdentity) []byte {
key, _ := rawdb.AppendBySourceScopeKeyFromPrimary(nil, encodeGrantIdentityKey(id), scopeKey)
return key
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: the ok return from AppendBySourceScopeKeyFromPrimary is discarded, so a grant identity that fails to splice yields a nil key that flows on as a real index key. Every other splice site in this PR (setByPrincipalKey, deleteSourceScopeKey, stageSourceScopeChange) turns !ok into an error. Either propagate the bool or panic on !ok, rather than relying on downstream len(k) < 3 checks to catch it. (confidence: medium)

Comment on lines +1139 to 1144
closeErr := w.Close(ctx)
if closeErr != nil {
l.Error("compactPebble: error closing source store", zap.Error(closeErr), zap.String("file", sourcePath))
}
if err := joinSourceStoreCloseError(selectErr, closeErr, sourcePath); err != nil {
return err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: source stores here are read-only inputs that have already been fully consumed (SourceFile carries only Path/SyncID/Stats). Joining their Close error into the return — same at compactor_pebble.go:561-570 — changes previously log-only behavior into a hard compaction failure, so a transient temp-dir/unlink hiccup can now fail an otherwise correct compaction. Consider keeping these logged and reserving the join for destination-side close errors. (confidence: high that the behavior changed, medium that it matters in practice)

Comment on lines +16 to +21
t.Helper()
if os.Getenv(ExtraEnv) == "" && os.Getenv(NightlyEnv) == "" {
t.Skipf("set %s=1 or run the corresponding Make target", ExtraEnv)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: the gate is != "", so BATON_TEST_EXTRA=0 (or =false) enables the extra tier — the opposite of what the value suggests, and the skip message tells the reader to set =1. Same shape in RequireNightly. Consider comparing against "1" (or parsing with strconv.ParseBool) so an explicit off value turns the tier off. (confidence: high, low impact)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Comment thread Makefile
.PHONY: race-check
race-check: ## Run the complete Go suite with the race detector.
go test -race -tags=baton_lambda_support -count=1 -timeout=45m ./...
BATON_TEST_EXTRA=1 go test -race -tags=baton_lambda_support -count=1 -timeout=45m ./...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: ci.yaml and main.yaml run bare go test ./... with neither tier variable set, so every test newly gated by testtier.RequireExtra/RequireNightly now skips in all PR and main runs — including several that previously ran unconditionally (TestParallelQueueExhaustiveInterleavings, TestTopologicalMergeDifferentialRandom{,Store}, TestTopologicalMergeChunkedDirtyFlush, TestCleanupContextDeadlineExceeded) and the whole non-short set (TestC1ZIntegrity, TestWALCheckpointRace, all TestChaosConnector*Corpus). race-check here sets only BATON_TEST_EXTRA=1, so the chaos corpora that the deleted comment said "race-check already includes" now run only under nightly. Worth confirming that's the intended net coverage, or gating the cheap deterministic ones (e.g. TestTopologicalMergeChunkedDirtyFlush) at a lower tier.

Comment on lines +60 to +67
// StaleSkipped counts index entries under the scope that did NOT
// yield a copied row: the primary was missing, or its value stamp
// named a different scope. This is the discriminator between "scope
// legitimately empty" (index prefix empty, StaleSkipped == 0) and
// "scope's rows were clobbered without index cleanup" (index says
// rows existed, none survived the stamp check). Future syncer replay
// orchestration must fail a zero-row replay when StaleSkipped > 0.
StaleSkipped int64

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: StaleSkipped can never be non-zero. validateReplaySourceScope runs first and hard-errors on exactly the two conditions that increment it — an index entry whose primary is absent (preflight %s index %x has no primary) and an indexed primary whose stamp differs (index scope %q resolves to primary stamped %q) — over the same indexPrefix range the replay loop then walks on an immutable read-only prev. So the three res.StaleSkipped++ sites (lines 931, 956, 1126, 1140, 1277, 1295) are unreachable, and the documented contract "future syncer replay orchestration must fail a zero-row replay when StaleSkipped > 0" would be gating on a permanently-zero signal. Either drop the preflight's hard failure for these two cases (the replay loop already comments that stale index entries are an expected, tolerable state) or drop the field so downstream doesn't build on it.

Comment on lines 575 to +582
}

// A fold inherits its base's validators, but the merged winners no longer
// represent that connector snapshot. Drop only the small manifest keyspace;
// retaining the existing source-scope indexes avoids an O(base) rewrite.
if err := destEng.InvalidateSourceCacheReplayState(ctx, false); err != nil {
return "", fmt.Errorf("compactPebbleFold: invalidate source-cache replay state: %w", err)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: bucket_plans.go has no bucket for the three by_source_scope index families or TypeSourceCache, so the fold merge copies the partial sources' scope-stamped primary rows into the grants/entitlements/resources buckets without their index entries, while dropScopeIndexes=false keeps the base's entries — including ones whose primary was just overwritten by a winner with a different (or empty) stamp. The output therefore violates the primary↔index biconditional that stageSourceScopeCleanup and validateReplaySourceScope rely on. It's contained today only because baseRec.SetCompacted(true) makes the artifact replay-ineligible; if fold output ever becomes an eligible source, this becomes silent wrong-row replay. Worth a comment recording the dependency, or adding the scope families to the fold bucket plan.

Comment on lines +4 to +26
// A connector that can cheaply revalidate upstream data — HTTP conditional
// requests (GitHub), delta queries (Microsoft Graph) — opts in by attaching
// SourceCacheCapability MODE_READ_WRITE to its Validate response. During a
// sync it looks up the previous validator for a scope via the Lookup the SDK
// provides on SyncOpAttrs, revalidates upstream, and either emits fresh rows
// tagged with SourceCacheRecord or asks the SDK to replay the previous rows
// with SourceCacheReplay.
//
// The connector owns scope computation; the SDK only keys storage by the
// connector-supplied scope key. The validator (etag, delta token) is opaque
// to the SDK.
//
// Invariant that keeps replay safe: a connector must only emit
// SourceCacheReplay for a scope whose validator it received from THIS sync's
// Lookup. The lookup need not happen in the same call that emits the
// replay: a planning call may batch-resolve many scopes and pass the
// verdicts to sibling cursors through EnqueuePageTokens page tokens — that
// satisfies the invariant, because the validator still originates from the
// consuming sync. What's forbidden is a validator that outlives a sync
// (connector-side caches, config, upstream echoes). When source cache is
// disabled or degraded (no capability, no usable previous sync, unsupported
// storage engine) the SDK installs NoopLookup, every lookup misses, and a
// well-behaved connector naturally falls back to full fetch.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: this package doc describes wiring that doesn't exist yet — Lookup, SetLookup, and NoopLookup have no references anywhere outside this package and its tests, and resource.SyncOpAttrs has no source-cache field, so "the Lookup the SDK provides on SyncOpAttrs" and "the SDK installs NoopLookup" are not true at this SHA (the PR body confirms syncer orchestration is deferred). A connector author reading this new exported package would build against a surface that isn't reachable. Suggest a short "not yet wired; see phase 6b" note in the doc until the syncer consumes it.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

A sync can only skip refetching a page if the previous artifact is a
trustworthy source for it. This adds the storage, provenance, and
eligibility machinery for that decision. Nothing is on by default: the
write path is unchanged for connectors that do not opt in through
sourcecache.WithScope, and no production sync consumes replay yet.

Storage. Grant, resource, and entitlement records carry a
source_scope_key, indexed by a by_source_scope family so mid-sync
tombstones can drop a scope's rows without scanning the keyspace.
Unscoped syncs must not pay for an index they never populate, so the
index obligation sits behind a sourceScopeMayExist gate: probed at
Open, armed only when a scoped key is actually staged, and cleared when
the families are excised. Benchmarks attributed essentially all of the
added ingest cost to the prior-value Get this avoids, so the gate is
what keeps ordinary writes where they were.

Provenance. Replay is only sound from a finished, uncompacted, full
Pebble sync. SyncRunSummary gains a compacted flag that the v3 manifest
header projects, and the syncer rejects SQLite and compacted inputs
outright rather than degrading silently. Every compaction output is
replay-ineligible by construction: k-way and overlay drop the
source-scope indexes instead of playing them forward, and fold keeps
whatever is cheapest for fold without merging partial inputs.

Also fixes a deadlock in which the source-cache delete path re-entered
closeMu through a promoted store method, and adds the instruments this
work needed to stay honest: a commit-point enumeration meta-test that
fails when a new batch-commit site ships without a declared failure
seam, and opt-in test tiers so the long deterministic and randomized
cases run in the nightly sweep instead of every pull request.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment on lines +290 to +295
for i, r := range resources {
rid := r.GetId()
if err := s.Engine.DeleteResourceRecord(ctx, rid.GetResourceType(), rid.GetResource()); err != nil {
return fmt.Errorf("source cache delete resource %q: %w", ids[i], err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion (medium confidence): the resource branch issues one DeleteResourceRecord per id, and each of those is its own withWrite + batch.Commit(writeOpts(e.opts.durability)) — so a tombstone page of N resources costs N separate durable commits and is not atomic. The sibling branches below stage all grant/entitlement deletes into a single RecordBatch, and the scoped variants commit in bounded chunks and report committed progress on error. Here a mid-page failure leaves a partially applied page with only error returned, so a caller can't tell what landed. Consider an engine-level DeleteResourceRecords(ctx, ids) that batches like DeleteGrantRecordsBounded/DeleteEntitlementRecords.

Comment on lines +349 to +353
parentRT, parentID, parentErr := ScanResourceParentRaw(oldVal)
if parentErr != nil {
if err := rb.deleteAllResourceParentKeysForChild(childRT, childID); err != nil {
return err
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion (medium confidence): this replaces stageResourceParentDelete, which propagated the ScanResourceParentRaw error, with a silent fallback to deleteAllResourceParentKeysForChild — a full iteration of the entire by_parent index family, per deleted row. A mass delete over a corrupted resource family therefore becomes O(deleted × |by_parent|) with no error and no log line saying corruption was hit. Two things worth reconsidering: (1) emit some signal (wrapped error or a counter) so the corruption is observable rather than absorbed, and (2) note that StageResourcePut above (line 322) still hard-fails on the same malformed prior value, so overwriting a corrupt row errors while deleting it silently heals — the asymmetry is easy to trip over later.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant