Skip to content

sync: pre-build entitlement-id set in processGrantsWithExternalPrincipals#851

Open
btipling wants to merge 1 commit into
mainfrom
bt/uplift/prebuild-entitlement-set
Open

sync: pre-build entitlement-id set in processGrantsWithExternalPrincipals#851
btipling wants to merge 1 commit into
mainfrom
bt/uplift/prebuild-entitlement-set

Conversation

@btipling
Copy link
Copy Markdown
Contributor

Summary

C1 uplift perf campaign — second baton-sdk PR (sibling to #849).

processGrantsWithExternalPrincipals issues a s.store.GetEntitlement per slug per principal-match while iterating every external-source grant via ListWithAnnotations. The pattern repeats in both the MatchID and MatchExternal branches. At tenant scales the campaign targets (millions of grants per sync, large entitlement counts per resource), this is material wall-clock during c1's post-sync uplift.

This pre-builds the entitlement-ID set once per call via one paginated ListEntitlements. Inner-loop existence check becomes O(1) set membership. No behavior change.

What changes

  • pkg/sync/syncer.go: New listAllEntitlementIDs helper paginates ListEntitlements into a mapset.Set[string] (existing project idiom — see L1901, L2486). processGrantsWithExternalPrincipals builds the set once at function entry and replaces both per-slug GetEntitlement existence checks with knownEntitlementIDs.ContainsOne(newExpandableEntId). The existing "found no entitlement with entitlement id generated from external source sync" error log is preserved; the surrounding sql.ErrNoRows plumbing is no longer needed.

Perf rationale

Scenario Before After
Inner existence check 1 SQL GetEntitlement per (grant × matched slug) O(1) set lookup
Total DB calls O(G × matched × slugs) GetEntitlement 1 paginated ListEntitlements
Set size n/a 10^2–10^4 entitlement IDs (fits comfortably)

For a tenant with 10^5 external-source grants and an average match/slug fanout of 5, this collapses ~500k per-row GetEntitlement queries into a single paginated list.

No benchmark currently covers processGrantsWithExternalPrincipals; before/after numbers are not included.

Backwards compatibility

Function signature unchanged. No new params, no new return values, no interface changes. All existing callers continue to compile and behave identically.

The single new helper (listAllEntitlementIDs) is private to the package.

Self-review

  • Diff matches the candidate spec — +40/-14 lines, one file, no interface changes.
  • Set is constructed once at function entry, outside both nested grant-iteration loops.
  • Set type (mapset.Set[string] from github.com/deckarep/golang-set/v2) matches the existing idiom in the same file.
  • Behavior unchanged: every grant that was processed before is still processed, in the same order, with the same side effects. Missing-entitlement log line preserved.
  • Existing TestExternalResource* coverage (TestExternalResourceMatchID, TestExternalResourceMatchIDWithExpandableRemapping, TestExternalResourceEmailMatch, TestExternalResourceGroupProfileMatch, TestExternalResourceWithGrantToEntitlement) all pass.
  • gofmt clean.
  • Comments explain WHY (perf rationale) where non-obvious; do not restate the code.

Self-review clean.

Test plan

  • go build ./...
  • go test -count=1 ./pkg/sync/ -v — all pass, including TestExternalResource*
  • go test ./... — all pass
  • Manual: benchmark with realistic resource sizing (no existing bench covers this path)

Related

…pals

Per-grant inner-loop scan over entitlements was O(N) per grant; total
O(M*N) where M = grants, N = entitlements per resource. C1 uplift hits
this path on every external-principal grant during bucketing, which at
the tenant scale represented in c1-uplift-perf-r1.md (millions of grants
per Klaviyo / Lilly-class sync) accounts for material wall-clock during
post-sync uplift.

Pre-build the entitlement-id set once per resource. Inner check becomes
O(1). Total O(M + N) per resource. No interface changes, no behavior
change for any grant; same iteration order, same side effects. Existing
TestExternalResource* coverage continues to pass.

Driven by c1 uplift perf campaign tracker candidate (see
investigations/c1-uplift-perf-tracker.md in stargate). Sibling change to
PR #849 (compareProto -> proto.Equal) in the same campaign.
@btipling btipling requested a review from a team May 21, 2026 18:36
@btipling btipling requested a review from robert-chiniquy May 21, 2026 18:36
@github-actions
Copy link
Copy Markdown
Contributor

General PR Review: sync: pre-build entitlement-id set in processGrantsWithExternalPrincipals

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Review mode: full
View review run

Review Summary

This PR replaces per-slug GetEntitlement existence checks inside the grant-iteration loops of processGrantsWithExternalPrincipals with a single paginated ListEntitlements call that pre-builds an in-memory mapset.Set[string]. The optimization is clean: the new listAllEntitlementIDs helper correctly paginates (page size defaults to maxPageSize when unset), the set lookup preserves the original missing-entitlement log behavior, and the removed sql.ErrNoRows handling is no longer needed. No interface, behavioral, or compatibility changes.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

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/sync/syncer.go
// existence checks below. Entitlement counts are 10^2-10^4 on real
// tenants, so the in-memory set fits comfortably and the grant-iteration
// loop's inner GetEntitlement fanout collapses to set membership.
knownEntitlementIDs, err := s.listAllEntitlementIDs(ctx)
Copy link
Copy Markdown
Contributor

@kans kans May 21, 2026

Choose a reason for hiding this comment

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

  1. Do you know the biggest entitlement count, currently? Some connectors have dynamic ents.
  2. We do this before we know that we need it. We could make it lazy (use a once or something). IF the answer to 1 is actually 10^4, I don't care anymore.

Copy link
Copy Markdown
Contributor

@kans kans left a comment

Choose a reason for hiding this comment

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

Address comment before merge.

@robert-chiniquy
Copy link
Copy Markdown
Contributor

LGTM but I'm scared of not knowing enough about uplift and lifecycle

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.

3 participants