injective keys - 75%+ faster expansion, 50% reduction in file size, massive allocation reduction && Soundness#993
Conversation
7eb0dda to
9616b70
Compare
General PR Review: injective keys - 75%+ faster expansion, 50% reduction in file size, massive allocation reduction && SoundnessBlocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0 Review SummaryThe new commit ("adds logging during the migration") adds a 15s-cadence progress log to the grant id-index migration loop plus two migration test files (export_test.go test-only key/marshal exports, and store_migration_roundtrip_test.go exercising the legacy-envelope -> writable-open -> re-fence -> read-only-reopen path). The full PR diff was scanned for security and correctness: the proto change (new PebbleIdIndexFormat field 42) is additive with matching generated code and no field renumbering, and the pebble2 -> pebble3 manifest fence is read-compatible (old name accepted, never written). No new security or correctness issues were introduced by the new commit; the three prior suggestion-level findings are on code untouched by this commit and are not re-raised. Security IssuesNone found. Correctness IssuesNone found. Suggestions
Prompt for AI agents |
9616b70 to
93646ec
Compare
93646ec to
84500e6
Compare
| message Grant { | ||
| c1.connector.v2.Entitlement entitlement = 1 [(validate.rules).message = {required: true}]; | ||
| c1.connector.v2.Resource principal = 2 [(validate.rules).message = {required: true}]; | ||
| // These ids may not map one to one with the grant itself. |
There was a problem hiding this comment.
🟡 Suggestion: The generated pb/c1/connector/v2/grant.pb.go rawDesc in this PR marks field id = 3 as deprecated=true (the added \x18\x01 option bytes), but this .proto source only adds a comment — it does not declare deprecated = true on the field. This is generated-artifact drift: running make protogen would drop the deprecated flag. If the intent is to deprecate id, add deprecated = true to the field options here and regenerate; otherwise the generated file shouldn't carry it. Confidence: high (mismatch), low blast radius.
84500e6 to
a37841f
Compare
| // isGranting serializes granting from Done and the periodic granter, | ||
| // and lets Unregister wait out an in-flight grant loop. | ||
| isGranting bool | ||
| isGrantingCond *sync.Cond |
There was a problem hiding this comment.
🟡 Suggestion: pausableCompactionScheduler reimplements pebble's ConcurrencyLimitScheduler with non-trivial concurrency (the isGranting handoff, Unregister waiting out an in-flight grant loop, Done/periodic-granter/TrySchedule all mutating runningCompactions). It has no direct unit test — only exercised indirectly via the env-gated whale benchmark. A focused test covering pause during an in-flight grant, resume-poke pickup, and Register/Unregister ordering would guard this against future pebble-version bumps. Same applies to the new Begin/Add/Finish/Abort layer session (segment cut, worker-error propagation, abort mid-wave), only reached with BATON_PEBBLE_SYNTH_LAYER_SST set.
| for k := 0; k < len(entitlementID); k++ { | ||
| if entitlementID[k] != ':' { | ||
| continue | ||
| } | ||
| for l := k + 1; l < len(entitlementID); l++ { | ||
| if entitlementID[l] != ':' { | ||
| continue | ||
| } | ||
| cand := entitlementIdentity{ | ||
| resourceTypeID: entitlementID[:k], | ||
| resourceID: entitlementID[k+1 : l], | ||
| stripped: true, | ||
| tail: entitlementID[l+1:], | ||
| } | ||
| nonEmpty, err := e.grantPrimaryPrefixNonEmpty(encodeGrantPrimaryEntitlementPrefix(cand)) | ||
| if err != nil { | ||
| return entitlementIdentity{}, err |
There was a problem hiding this comment.
🟡 Suggestion: This direct-split fallback opens a fresh Pebble iterator (grantPrimaryPrefixNonEmpty) for every O(colons²) candidate with no upper bound, unlike resolveGrantIdentityByExternalID which caps enumeration at maxGrantIDCandidates. An entitlement id with many colons and no matching entitlement record would open a large number of iterators on this bare-id edge. Consider applying a similar cap here for consistency and robustness. (low confidence)
24fba77 to
687fe2d
Compare
687fe2d to
e47abd8
Compare
6b42749 to
05f4183
Compare
| if s.sorter == nil { | ||
| if err := e.initSynthLayerSession(ctx, s); err != nil { | ||
| return err | ||
| } | ||
| // Segment SSTs carry primary rows only; make sure EndSync builds | ||
| // the deferred by_principal index even if no batch write set the | ||
| // flag. | ||
| if err := e.markDeferredIdxPending(); err != nil { | ||
| return err | ||
| } |
There was a problem hiding this comment.
🟡 Suggestion: initSynthLayerSession sets s.sorter and starts the worker goroutine before returning, so if markDeferredIdxPending() fails here (after init succeeded), the session is left initialized (s.sorter != nil) with a live worker, but the durable deferred-index marker is unarmed and its CAS rolled back. A subsequent Add skips the if s.sorter == nil block and never re-attempts the marker, so EndSync could silently skip building the synth by_principal index. Consider arming the marker before initSynthLayerSession (or tearing the session down on marker failure) so the failure is fully retryable. (confidence: low)
05f4183 to
938a797
Compare
| err := e.IterateAllSyncRuns(ctx, func(r *v3.SyncRunRecord) bool { | ||
| syncID = r.GetSyncId() | ||
| return false | ||
| }) |
There was a problem hiding this comment.
🟡 Suggestion: This yield returns false on the first record, and IterateAllSyncRuns stops iteration on false, so syncID binds to the first (earliest KSUID) sync run, not the latest. If a legacy c1z ever carries multiple sync-run records, post-migration stats are recomputed against an arbitrary/earliest sync rather than the current one. Low impact today since the Pebble primary keyspace holds a single generation, but the control flow reads as "some sync" when the intent is "the current sync." (confidence: medium)
ggreer
left a comment
There was a problem hiding this comment.
Approved so that this can be merged at your leisure (once the tests pass).
| // combinatorial prober above answers SDK-shaped ids without ever getting | ||
| // here), never from sync or expansion. Exactly-one rule: zero matches is | ||
| // pebble.ErrNotFound, several is ErrAmbiguousExternalID. | ||
| func (e *Engine) scanGrantIdentityByStoredExternalID(ctx context.Context, grantID string) (grantIdentity, error) { |
There was a problem hiding this comment.
🟡 Suggestion: This fallback changes grant-id resolution behavior — a connector-custom id that previously returned NotFound in O(1) now triggers an O(all grants) full-keyspace scan on every miss. The code comment correctly bounds this to the CLI/reader-API edge, but worth confirming no reader-API caller resolves unknown/custom ids in a loop (each miss is now a whale-sized scan). The change itself improves SQLite parity and is well-guarded (context checks, exactly-one rule). (low confidence)
9e80d67 to
a59f234
Compare
| lastLog := start | ||
| var rows, skippedMissingRefs int64 | ||
| for iter.First(); iter.Valid(); iter.Next() { | ||
| if rows&0xFFFF == 0 { |
There was a problem hiding this comment.
🟡 Suggestion: The rows&0xFFFF == 0 gate uses rows, which only increments for successfully re-keyed grants (line 232), not for skipped rows (line 221). A long contiguous run of ref-missing skips leaves rows frozen at a value where the gate is false, suppressing both the ctx.Err() cancellation check and the progress log for the duration of that run — the exact silent-open scenario this commit targets. Gating on the raw loop count (or checking iter-level progress) instead of the added-row count would keep both behaviors firing. Low severity: skipped grants are pathological, so in practice the gate fires steadily. (confidence: medium)
Summary
This branch replaces the Pebble engine's record addressing — previously keyed by the lossy :-joined external-id string — with structural identity keys derived only from the structured fields records already carry, and layers on the write-path, expansion, and lifecycle work that the new keyspace enables. External ids are unchanged as an external contract: baton stores and emits them byte-identical to what connectors produce, and never parses them to decide identity.
Structural (injective) identity keys
Write-path and index restructuring
Grant expansion pipeline
Sync lifecycle: sealing and compaction scheduling
Testing
End-to-end fuzz tests over the sync/expansion pipeline.
Property tests for identity ↔ external-id bijectivity against adversarial ids (ARN-style colon-bearing resource ids, rt:rid:-prefixed opaque ids, empty tails).
Migration-semantics tests (duplicate folds, missing-ref skips), lookup-tier tests, sealed-lifecycle and deferred-index resume tests, and SQLite-parity/differential suites.