Skip to content

perf(core): authenticated request issues ~16 sequential queries (duplicate authz + 3× localization) — add request-scoped memoization#2412

Merged
os-zhuang merged 1 commit into
mainfrom
perf/execctx-request-memo
Jun 28, 2026
Merged

perf(core): authenticated request issues ~16 sequential queries (duplicate authz + 3× localization) — add request-scoped memoization#2412
os-zhuang merged 1 commit into
mainfrom
perf/execctx-request-memo

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

An authenticated REST request resolves its execution context (identity + RBAC/RLS + localization) multiple times within a single handler — the data operation itself, app-nav RBAC filtering, dashboard widget gating, and the ADR-0069 auth gate all call RestServer.resolveExecCtx. Each pass runs the full resolveAuthzContext aggregation plus the localization read — ~16 sequential queries — and nothing memoized it. So a request that resolves the context twice (e.g. GET /meta/:type, which gates app nav at one site and dashboard widgets at another) pays for duplicate authz + repeated localization.

Concrete duplication confirmed by tracing the path:

  • rest-server.tsresolveExecCtx is invoked 2× in the same handler (app RBAC filter + dashboard widget gating), each re-running everything.
  • resolve-authz-context.tssys_user read twice per pass (email fallback at one site, ai_seat synthesis at another) on the API-key path; the localization direct-read fallback issues 3 sequential sys_setting queries (timezone / locale / currency).

Fix — request-scoped memoization

@objectstack/rest

  • resolveExecCtx is now a thin memoizing wrapper; the heavy resolution moved to computeExecCtx.
  • Memo is a WeakMap keyed by the per-request req object (collected with the request → naturally request-scoped, no TTL, no cross-request leak), then by the input environmentId (one host can route multiple environments).
  • Caches the in-flight Promise, so concurrent callers share one resolution. Anonymous (undefined) resolutions are cached too, so repeat callers don't re-run getSession.

@objectstack/core

  • resolveAuthzContext reads sys_user at most once per pass (a memoized getUserRow() shared by the email fallback and the ai_seat synthesis).
  • resolveLocalizationContext's direct-read fallback batches timezone/locale/currency into one sys_setting query ($in on key) instead of three sequential reads.

Safety

  • No authorization-behavior change — the same roles / permissions / RLS context is resolved, just without the redundant reads. Verified the 47 resolveExecCtx call sites treat the result as read-only (no mutation), so sharing one cached instance per request is safe.
  • The two sys_member reads (per-user roles vs. all-org-members for RLS scoping) are intentionally left distinct — different filters and limits; merging them would risk a subtle behavior change for marginal gain.

Tests

  • resolve-authz-context.test.ts — query-counting harness asserts sys_user is read once (email + ai_seat both satisfied) and localization reads sys_setting once for all three keys; plus UTC/en-US fallback.
  • rest-exec-ctx-memo.test.ts (new) — pins the memo contract: once per request object, re-resolves for a different request, keyed by environmentId, and caches anonymous results.
  • Full suites green: @objectstack/core security (79) and @objectstack/rest (144). Both packages build clean (incl. strict DTS).

🤖 Generated with Claude Code

… resolution

An authenticated REST request resolves its execution context (identity +
RBAC/RLS + localization) many times in one handler — the data op, app-nav RBAC
filtering, dashboard widget gating, the ADR-0069 auth gate. Each resolveExecCtx
pass is the full resolveAuthzContext aggregation plus the localization read
(~16 sequential queries), and nothing memoized it, so a request that resolved
twice paid for duplicate authz + repeated localization.

- rest: memoize resolveExecCtx per request (WeakMap keyed by req + input
  environmentId; caches the in-flight Promise; heavy path moved to
  computeExecCtx). Anonymous resolutions cached too.
- core: read sys_user at most once per resolveAuthzContext pass (email fallback
  + ai_seat synthesis shared a duplicate query on the API-key path); batch the
  localization direct-read fallback (timezone/locale/currency) into one
  sys_setting query ($in on key) instead of three sequential reads.

No authorization-behavior change. sys_member reads (per-user vs all-org) left
distinct on purpose (different filters/limits).

Tests: query-counting regressions (sys_user once, localization once) +
rest-server memo contract (per-request, per-environment, anonymous cached).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 28, 2026 2:42am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jun 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/core, @objectstack/rest.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/core/index.mdx (via @objectstack/core)
  • content/docs/concepts/core/plugins.mdx (via @objectstack/core)
  • content/docs/concepts/core/services.mdx (via @objectstack/core)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/core, @objectstack/rest)
  • content/docs/concepts/north-star.mdx (via packages/core)
  • content/docs/concepts/packages.mdx (via @objectstack/core, @objectstack/rest)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/core)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/core)
  • content/docs/guides/api-reference.mdx (via @objectstack/rest)
  • content/docs/guides/authentication.mdx (via @objectstack/core)
  • content/docs/guides/contracts/index.mdx (via @objectstack/core)
  • content/docs/guides/kernel-services.mdx (via @objectstack/core)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/core)
  • content/docs/guides/packages.mdx (via @objectstack/core, @objectstack/rest)
  • content/docs/guides/plugin-development.mdx (via @objectstack/core)
  • content/docs/guides/plugins.mdx (via @objectstack/core, @objectstack/rest)
  • content/docs/guides/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/core)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/core)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang merged commit 9ccfcd6 into main Jun 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the perf/execctx-request-memo branch June 28, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant