Skip to content

resolveLocalizationContext reads the same sys_setting namespace three times per request #10826

Description

@os-zhuang

Split out of #10757 (direction 5) by PM ruling on PR #10824, at low priority — see the calibration below.

resolveLocalizationContext issues three identical select * from sys_setting where namespace = ? per request (queries 16–18 of 24, measured on a live rig in PR #10824). The function already batches its own direct fallback into one $in, but the settings-service path it prefers calls SettingsService.get() three times, and each call runs loadRows(namespace, …) over the whole namespace.

Three reads of one question, inside one request. Replace them with one namespace read the three keys are picked out of.

Calibration — this is smaller than it looks

cloud#1539 measured these three reads running in parallel: 3 queries but 1 leg. Since that same work established that legs, not queries, are the latency multiplier, the win here is ~2 queries and ≈0 legs.

So this is not a latency fix, and should not be scheduled as one. It is worth doing because:

Constraints

Activity

  1. added
    domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they land
    on Aug 21, 2026
  2. huangyiirene commented on Aug 21, 2026

    @huangyiirene
    Collaborator

    Triage: + domain:engine — resolveLocalizationContext lands in packages/core/src/security/ (engine by the lane table). Queue state was already set by the splitting PM ruling (PR #10824); type Task. Serial note: #10825 lives in the same resolve-authz-context.ts neighbourhood — the engine seat owes a fold-or-serial answer for the pair before dispatch (likely serial: different functions, but one file family; five-gate ① same-defect-shape holds — both are "one question asked repeatedly" — so a fold is also defensible if one dev takes both with per-card commits).


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 23, 2026
  4. self-assigned this
    on Aug 23, 2026
  5. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Claim — the serial block is discharged

    Seat domain:engine · session session_01RfyXxZ2WPjcjhuXpiQQc3y · branch claude/issue-10826-localization-namespace-single-read

    Why it is dispatchable now. The standing ruling on this card (issuecomment-5376874279, reaffirmed at issuecomment-5377364903) was serial behind #10825, keyed on the file not the seat — 「本卡在 #10825 落地前不派发,无论哪个席位来做」. That condition is now satisfied: PR #11197 merged, #10825 is closed as completed, and 3b2af5ebd is the current HEAD commit for packages/core/src/security/resolve-authz-context.ts on origin/main. The epic seat is done with the file; no other open PR in this lane touches it.

    I'm honouring that ruling as written rather than re-deriving it — the previous seat established the order by file, which was the right method, and it stayed correct across the card changing hands.

    ⚠️ Stale-premise check — run, and it found something that needed ruling out

    git log on that file surfaced 9d7d2def0 fix(core): **cache resolveLocalizationContext across requests** to stop repeated sys_setting read noise (#10301) — the same function this card targets, doing the one thing this card forbids. That is exactly the shape that makes a card's premise stale, so I checked origin/main rather than assuming either way.

    Premise holds. On current main, resolveLocalizationContextUncached still issues three separate calls — settings.get('localization', 'timezone' | 'locale' | 'currency', sctx) at lines 749–760. What #10301 landed is localizationFailureCache, which caches only failed reads — precisely what this card's own constraints already recorded ("#10221 caches only failed reads deliberately"). The success path is untouched and still asks one question three times. No overlap, no conflict, nothing to re-scope.

    Declared file surface

    • packages/core/src/security/resolve-authz-context.ts — the resolveLocalizationContextUncached region (~743–775).

    A prediction, as always. If it lands narrower, say so and the hold releases early.

    Calibration that must travel into the work — this is NOT a latency fix

    Carrying the previous seat's instruction verbatim, because it is the thing most likely to be mis-reported:

    这不是延迟修复 —— cloud#1539 测得那三次读是并行发生的(约 2 个 query、≈0 个 leg)

    cloud#1539 measured the three reads running in parallel: 3 queries but 1 leg, and that same work established that legs, not queries, are the latency multiplier. So the win is ~2 queries and ≈0 legs. ⛔ Do not schedule, justify, or report this as a latency improvement. It is worth doing for three other reasons, all of which stand on their own: it is the same defect class as PR #10824's two fixes (one question asked repeatedly inside one request); it is correct whether or not #10757 direction 5 ever ships; and it shrinks the surface direction 5 would have to cache, making that future card's invalidation design smaller and safer.

    If your measurement disagrees with the ≈0-leg calibration, report the disagreement — don't quietly upgrade the card's value.

    Constraints

    1. ⛔ No caching. One read instead of three, within the request. Anything surviving the request boundary is Authenticated data requests issue ~20 DB queries in ~17 sequential legs — per-request auth/session/localization/metadata resolution has no cross-request caching, costing ~1.5s/request on remote Postgres #10757 direction 5 — a different card with a security review attached. The existing localizationFailureCache is pre-existing and out of scope: do not extend it, do not route the success path through it.
    2. Preserve read-after-write. A dogfood test pins analytics-timezone read-after-write. A single batched read inside the request satisfies it; a cache would not without invalidation. Name the test you ran.
    3. ⛔ packages/spec off limits.
    4. ⚠️ Lane boundary. The card notes the settings-service path calls SettingsService.get() three times, each running loadRows(namespace, …) over the whole namespace. If the honest fix requires changing SettingsService itself in packages/services, stop and report — that is domain:services and not this seat's to take. A fix that stays inside resolve-authz-context.ts (one namespace read, three keys picked out of it) is in bounds.
    5. ⛔ Never edit content/docs/releases/. Breaking changes ship as minor, never major.
    6. ⛔ Do not skip, disable or quarantine a test; do not raise a ratchet ceiling.

    Generated by Claude Code

  6. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    ⛔ Correcting my claim above — this card is being landed by PR #11208, not by my branch

    My claim comment (03:16:06) is superseded. I dispatched a duplicate. The error is mine and here is exactly how it happened, because "PM claimed a card someone else was already finishing" is worth being able to reconstruct.

    The collision

    time event
    02:45:09 PR #11200 opened — SettingsService.getMany, the service half of this card (packages/services)
    03:11:18 #11200 merged
    ~03:14 I read this card and ran a stale-premise check
    03:16:06 I claimed + dispatched
    03:22:17 PR #11208 opened — the caller half, resolveLocalizationContext → getMany

    Why my check didn't catch it — two reads that were each correct and jointly blind

    I did run a stale-premise check, and it answered a different question than the one that mattered. I asked "is the defect still present?" — it was: three settings.get calls still at resolve-authz-context.ts:749–760. I never asked "is someone already fixing it?"

    Both probes were structurally incapable of seeing #11200:

    1. issue_read get returned closed_by_pull_requests: {total_count: 0} — because perf(service-settings): getMany resolves same-namespace keys with one grouped row load (#10826) #11200 names "(resolveLocalizationContext reads the same sys_setting namespace three times per request #10826)" in its title with no closing keyword, so GitHub never linked it to this card. A PR can target a card and be invisible to that field.
    2. My git log probe was scoped to packages/core/src/security/resolve-authz-context.ts. perf(service-settings): getMany resolves same-namespace keys with one grouped row load (#10826) #11200 only touches packages/services. The one file I watched was the one file the merged half could not appear in.

    ⭐ Worse, #11200's own body announced the plan I collided with: "The resolve-authz-context.ts caller switches to getMany in a follow-up commit … after it lands." The two-half sequence was declared in public, in advance. I simply never found the PR that declared it.

    The claim-time check that would have caught this, and which I'm adopting: search PRs by card number (10826 in title/body across open and recently-merged), not closed_by_pull_requests.

    The dev did its job correctly — the miss is not its

    The dispatched dev found #11200 before writing any code, verified it landed as ba6c9ed1a via git log -S, read its body, and correctly recorded it as resolving my lane-boundary assumption in the good direction: no packages/services change was needed because the services half had already shipped. It then wrote a complete local implementation. It pushed only an empty branch (claude/issue-10826-localization-namespace-single-read, still at 21756b325, zero commits) as a first-minute write-route probe. No PR, no comment, no label, nothing else written to GitHub. Its work is stood down and preserved locally, unpushed.

    Its one gap — not re-checking for a newer open PR after cutting its worktree — is real but third-order next to mine, and #11208 did not exist when it started.

    #11208 reviewed and endorsed

    I read its diff rather than waving it through, since it lands in this lane's file:

    ⛔ I have not touched it, and will not. It is good work and should land.

    One follow-up finding, not a blocker

    ResolveLocalizationInput declares settings?: any under the doc comment "Settings service exposing get(namespace, key, { tenantId, userId })." After #11208 a caller also uses getMany, through an any. It type-checks (hence green CI, and this is not a merge blocker), but the declared surface now under-states what the consumer calls — the same declared≠actual class this repo treats as a defect in its own right. Also relevant: SettingsReadSurface is declared per-consumer (plugin-auth, rest, plugin-email each name their own) and getMany appears in none of them. I'll file this separately once I have the last reading, and it belongs to whoever owns the declaration, not to this card.

    No labels changed here — the card is genuinely in flight, just on someone else's branch, and #11208's Closes #10826 will close it on merge.


    Generated by Claude Code

  7. os-zhuang commented on Aug 23, 2026

    @os-zhuang
    ContributorAuthor

    Closed — service half #11200 (SettingsService.getMany: N same-namespace keys → ≤2 grouped row loads, cascade extracted not copied, per-key answers deep-equal get()'s by pinned contract; getNamespace rides the same path) + caller half #11208 (resolveLocalizationContext prefers one getMany, feature-detected, thrown-getMany lands in the same direct $in fallback). Landed serially after #10825 on the shared file per the engine seat's ruling. Queries 16–18/24 collapse to one; per the card's own calibration this is a query-count fix, not a latency fix.

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

Metadata

Metadata

Assignees

Labels

domain:engineRetired 2026-10-22, alias of domain:server (engine surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions