diff-aware incremental grant expansion#1013
Conversation
99c1208 to
4e46616
Compare
General PR Review: [DO NOT REVIEW YET] feat(expand): diff-aware incremental grant expansionBlocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0 Review SummaryScanned the full PR diff for security and correctness. This adds diff-aware incremental grant expansion ( Security IssuesNone found. Correctness IssuesNone found. Suggestions
Prompt for AI agents |
|
Review feedback from both me, and two Fable passes over the PR considering it in an of itself, and in regard to the big sync replay effort. Replays (eg delta queries in entra) may as well be a new flavor of online/live compaction with certified merge semantics around connector defined scopes (an etag for a page...) instead of the SDK's row level scoping, and they already solve the negative case of tomb stones. I plan on finishing off both sides after both change sets land - incremental expansion for replays, and tombstones for compaction. Review: incremental grant expansionThe design direction is right. But there are two blockers that mean the incremental path never actually executes against a real store today, two correctness divergences from full expansion that are masked by those blockers, and several soundness/lifecycle gaps. Details below, then the requested changes. BlockersB1. The incremental expander cannot read grants through the real store adapter — it always errors and silently falls back to full expansion. B2. SQLite lifecycle refuses the resume the incremental path depends on. Correctness bugs (currently masked by B1)C1. Wrong directness predicate for shallow edges. C2. Sources/provenance are never merged, and the drift compounds. C3. Edge-spec changes on an existing edge are invisible. UnsoundnessU1. The caller's base graph is mutated in place, poisoning retries. U2. Requested changes
Items 1–4 are correctness blockers; 5–8 and 10 are unsoundness/API-shape (6 and 7 are cheap now and structural later); 9 is convergence hygiene; 11–12 are cheap tests and a measurement. Notes (no action required)
Other stuff
|
4e46616 to
f996ba5
Compare
| NextPage(ctx context.Context, actionID string, pageToken string) error | ||
| EntitlementGraph(ctx context.Context) *expand.EntitlementGraph | ||
| ClearEntitlementGraph(ctx context.Context) | ||
| ClearEntitlementGraphTransientState(ctx context.Context) |
There was a problem hiding this comment.
🟡 Suggestion: This adds a method to the exported State interface, which is a breaking change for any downstream that implements it. In practice State is only implemented by the unexported *state and can't be injected into the syncer (no public constructor/setter accepts a custom State), so the real-world impact is low. Flagging only for SDK-compat awareness — no change required if State isn't intended as a downstream extension point. (low confidence)
Fixes from kans's review of the diff-aware incremental grant expansion: - B1: pass full entitlement records (not bare ids) to all grant reads so the incremental path stops erroring into a silent full-expansion fallback; compactor tests assert it actually ran. - B2: scope incremental to Pebble; degrade gracefully to full on SQLite. - C1: use isGrantDirectOnEntitlement for shallow filtering and IsDirect. - C2: merge sources into existing grants and record all contributing edges; parity tests compare full rows including sources maps. - Derive changed entitlements inside the compactor; drop the caller param. Compare edge specs (not just endpoints): widened re-expands, narrowed auto-declines via a named ErrIncrementalRevocationDecline hook. - U1: clone the base graph so a failed/declined run can't poison a retry. - U2: clear a preserved graph in PrepareExpansionReplayToken so replay works. - Converge the finish path: Cleanup/EndSync/Close on a detached ctx, fatal teardown errors vs safe fallback, run-duration bound + ctx polling, dangling-ref skip-with-warn. - Strip transient graph state before the final checkpoint. - Tests: dangling-ref + sealed-artifact lifecycle + revocation parity + shallow-directness differentials. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f996ba5 to
e69dac6
Compare
| return false, fmt.Errorf("incremental expansion: get sync: %w", err) | ||
| } | ||
| syncType := connectorstore.SyncType(syncResp.GetSync().GetSyncType()) | ||
| if _, _, err := c.compactedC1z.StartOrResumeSync(walkCtx, syncType, newSyncId); err != nil { |
There was a problem hiding this comment.
🟡 Suggestion: If StartOrResumeSync fails here, the store may be left partially resumed, but this returns a plain error that the caller treats as a safe fallback to full expansion (case err != nil → warn + full). Elsewhere the code deliberately marks state-ambiguous failures (restoreEndedSync, finishIncrementalExpansion) as errIncrementalFatal precisely because running full expansion against an unknown store state is unsafe. Consider whether a resume failure should also be fatal (or otherwise guaranteed to leave the store in the ended state the full path expects). Low confidence — depends on StartOrResumeSync leaving the store untouched on error.
Expand only the subgraph affected by an incremental change instead of rebuilding and walking the whole entitlement graph. Seeds from both new edges and changed-membership entitlements, so a new member on an existing group propagates; new edges that close a cycle fall back to full expansion. Source reads stream and writes flush in chunks to bound memory. Additions only — callers use full expansion for change sets with revocations. - expand: IncrementalExpander.ExpandChanges (streaming, chunked flush) - sync: WithPreserveEntitlementGraph, GraphFromToken, NewExpanderStore - synccompactor: WithIncrementalExpansion (diff-aware, cycle fallback) - tests: expander + Pebble-c1z compactor differentials (incremental == full) Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes from kans's review of the diff-aware incremental grant expansion: - B1: pass full entitlement records (not bare ids) to all grant reads so the incremental path stops erroring into a silent full-expansion fallback; compactor tests assert it actually ran. - B2: scope incremental to Pebble; degrade gracefully to full on SQLite. - C1: use isGrantDirectOnEntitlement for shallow filtering and IsDirect. - C2: merge sources into existing grants and record all contributing edges; parity tests compare full rows including sources maps. - Derive changed entitlements inside the compactor; drop the caller param. Compare edge specs (not just endpoints): widened re-expands, narrowed auto-declines via a named ErrIncrementalRevocationDecline hook. - U1: clone the base graph so a failed/declined run can't poison a retry. - U2: clear a preserved graph in PrepareExpansionReplayToken so replay works. - Converge the finish path: Cleanup/EndSync/Close on a detached ctx, fatal teardown errors vs safe fallback, run-duration bound + ctx polling, dangling-ref skip-with-warn. - Strip transient graph state before the final checkpoint. - Tests: dangling-ref + sealed-artifact lifecycle + revocation parity + shallow-directness differentials. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fold already reads every applied increment record; collect the distinct grant entitlement ids there (FoldStats.GrantEntitlementIDs) and seed incremental expansion from that set instead of re-opening and re-scanning each increment c1z. No-op resubmissions (byte-identical or older-than-incumbent records) change nothing and are excluded, so the walk gets fewer wasted seeds. Rebuild-mode compactions (no fold) keep the re-read fallback. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
d1ced91 to
a0d5774
Compare
| FinishAction(ctx context.Context, action *Action) | ||
| NextPage(ctx context.Context, actionID string, pageToken string) error | ||
| EntitlementGraph(ctx context.Context) *expand.EntitlementGraph | ||
| PeekEntitlementGraph() *expand.EntitlementGraph |
There was a problem hiding this comment.
🟡 Suggestion: These two additions (PeekEntitlementGraph, ClearEntitlementGraphTransientState) expand the exported State interface. Any external type that implements sync.State would fail to compile until it defines them. In practice only the internal *state implements this, so the risk is likely nil — but per the SDK compatibility criteria, adding methods to an exported interface is a breaking change. If State is intended to be internal-only, consider documenting that. Low confidence.
…he token A preserved graph costs ~170-190 bytes/node in the sync token (~10MB at 50k entitlements, measured in TestGraphBlobSizeAtScale), and tokens travel through workflow state. Store it as a Pebble engine-meta sidecar (same single-key shape as the stats sidecar) instead: - WithPreserveEntitlementGraph writes the sidecar when the store supports it and keeps the token skinny; SQLite (or a failed sidecar write) keeps the graph in the token as before. - sync.GraphFromStore(ctx, store, syncID) loads it; a sync-id guard in the blob rejects a stale fold-inherited sidecar. GraphFromToken remains for legacy artifacts. - The compactor writes the post-expansion graph into the compacted artifact (updated clone on incremental success; fresh graph via preserve on the decline->full path when opted in) so the artifact self-carries its base graph for the next round. Without the opt-in, any inherited sidecar is deleted. - StartNewSync wipes the sidecar with the rest of the keyspace, so a replacement sync never inherits a prior sync's graph. Co-authored-by: c1-squire-dev[bot] <c1-squire-dev[bot]@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
a0d5774 to
b0b83ea
Compare
|
@manojacs - review by me, Fable, and a bunch of sub-agents. Since you started work on this, we landed a major refactor to the SDK that centralizes the declaration and handling of SDK invariants (referential & business consistency) for the new write paths that I'm working towards - ie, partial sync replays. Task: Rebase
|
|
Another random thought. We are accumulating fuzzers - it would be nice to have a |
Expand only the subgraph affected by an incremental change instead of rebuilding and walking the whole entitlement graph. Seeds from both new edges and changed-membership entitlements, so a new member on an existing group propagates; new edges that close a cycle fall back to full expansion. Source reads stream and writes flush in chunks to bound memory. Additions only — callers use full expansion for change sets with revocations.