Repository navigation
Commit 9a7838e
perf(core): read the Default Organization once per engine, not on every authenticated request (#24627)
Fixes #24523
Clause-②: no (a per-engine memo of an unchanged read: no accept set,
standing verdict or served body moves; owes the seat's review of record
before the queue, at `CONTRACT_REVIEW_TIER` because it is the
platform-standing anchor)
Dispatched by the `domain:engine` seat 1 PM
(`session_01JtSQQ3EWsbNPzyYMug1tXy`, claim 6271076320). Dev session:
`session_01Ug7sGD6qWVrzQNdihVX3T6`.
⛔ **Landing waits** on the `epic:#15194` seat's answer to the territory
declaration 6271107528, or an objection-free read of that thread, and on
the seat's `CONTRACT_REVIEW_TIER` review of record. No answer was posted
when this PR opened.
## What changed
`resolveUserAuthzGrants` asks for the Default Organization whenever a
principal holds an organization-owned `admin_full_access` grant under
`single`. `readDefaultOrganizationId` answers with one
`sys_organization` read by slug, plus a second read when the only
organization is not slugged `default`. Nothing carried that answer
across requests, so every such request paid one or two reads.
This PR keeps the answer per engine, across requests
(`memoisedDefaultOrganizationId`,
`packages/core/src/security/resolve-authz-context.ts`). The call site in
`resolveUserAuthzGrants` is the only caller changed.
- **In-process retirement:** any `sys_organization` write through the
engine (insert, update, delete, a slug move included) retires the
answer. The write is observed after it settles, by #10757 leg 5's write
seam: a per-engine `WeakMap`, a watched-object set (`sys_organization`),
and a generation read before the reads and checked on the next hit. A
write that lands while the read is in flight leaves that answer dead on
arrival.
- **Across replicas:** a peer's `sys_organization` write arrives only
when the `authz.invalidated` bridge is attached, which needs the grants
cache on (`OS_AUTHZ_GRANTS_CACHE_TTL_MS` above 0) and a cross-process
cluster driver. It arrives as a `remote` epoch bump. That bump, and
every other non-`write` bump (`metadata`, which is plugin-security's
local declaration bump, and `manual`), retires the answer. On the
shipped default, no bump carries a peer's write.
- **Residual bound, 30 s** (`DEFAULT_ORGANIZATION_MEMO_TTL_MS`, a
constant, no new knob). It is never the in-process rule. It covers only
what no seam can see:
- a peer replica's write when no bridge is attached (#21868's
delete-and-recreate on another replica). On the shipped default that is
every peer's write, so the 30 s bound is its only cover;
- a write that started before this engine's observer registered
(measured below);
- a watched write inside an engine transaction, whose statement
completes before its commit (leg 5's window, named in the code).
- **An outage is not an answer.** A read that throws
(`AuthzStoreUnavailableError`, `SERVICE_UNAVAILABLE` / 503) memoises
nothing and propagates. The next request reads again.
- **`undefined` is memoised.** No organization, or several with none
slugged `default`, is a real answer, and a write retires it like any
other. A table that was never provisioned reads as zero organizations,
which is the true answer there.
- **Declines, reading every time as before**, in two cases:
- an engine without the write epoch and `registerMiddleware` (every
recording double);
- a registry that does not hold `sys_organization`. "Not registered" is
a verdict the boot can still contradict, and registering the object
writes no row the seam would see.
- **Which way a stale answer fails.** A stale id withholds standing from
the new Default Organization's grant. It also keeps granting standing to
the old organization's grant while that row survives. No design at zero
reads per request avoids the granting direction, so every window in
which the answer can be stale is capped by the residual bound. The
in-process slug-move pins assert the granting direction is retired.
**One copy of the seam.** `localizationWriteState`'s body is now
`watchedWriteState(ql, states, watched)`. Leg 5's localization cache and
this memo each call it with their own `WeakMap` and watched set. The
localization behaviour is unchanged, and its suite
(`resolve-localization-cache.test.ts`) passes unedited. This touches leg
5's function, which is outside the claim's file-surface list but in the
same file. Two copies of the seam would be the drift shape.
⛔ **Byte-identical:** `readDefaultOrganizationId` (both reads, filters
and limits), `grantAnchorsPlatformStanding`, `grantOrganizationOf` and
`grantAppliesInTenant` are not edited. The diff touches none of their
lines. Unit pins assert the two reads byte-for-byte wherever they are
issued.
## Measurement (M, a booted composition)
The composition is `default-organization-boot-invariant.pin.test.ts`'s
plugin set with `objectstack dev`'s seeded administrator, on sqlite-wasm
`:memory:`. The administrator's first-user promotion grant is owned by
the Default Organization. Each count is the number of statements the SQL
driver issued inside the request's own async context (knex `query`
events), warm (after two requests).
| warm request | `main` `5a871e4e41` | this branch |
|:--|--:|--:|
| `get-session`, organization-owned grant, slug `default` | 9 | **8** |
| data read (`GET /data/pin_note?$top=1`), organization-owned, slug
`default` | 11 | **10** |
| `get-session`, organization-owned, sole organization slugged `acme` |
10 | **8** |
| data read, organization-owned, `acme` | 12 | **10** |
| `get-session`, organization-less grant (either slug) | 8 | 8 |
| data read, organization-less grant (either slug) | 10 | 10 |
The `sys_organization` statements per warm request go from 1 (slug
`default`) or 2 (`acme`) to 0. After a `sys_organization` write, the
next request reads it once.
**Controls, byte-identical:** the resolved authz envelope (positions,
permissions, `org_user_ids`, `accessible_org_ids`, posture, tab
permissions) on a booted composition. Three personas: the administrator,
a member, and a user with no organization. Each was resolved with no
tenant and in the Default Organization, twice each, for 12 envelopes.
The run used the resolver at `BASE` (`5a871e4e41`, blob `4029c98d…`) and
at `HEAD` (blob `a4c2ec74…`), with random ids normalized. All 12 are
deep-equal: canonical md5 `3fd8be5122375762cf9d49f28bae84e8` on both.
The raw byte difference is only key order, from per-boot random user
ids. This was a one-off scratch file, deleted, and the restore was
checked blob-equal to `HEAD`.
## Pins
- `packages/runtime/src/default-organization-statements.golden.test.ts`
(new, 6 cases, a real kernel):
- the statements golden (8 / 10, with no `sys_organization` statement,
for both slugs);
- the organization-less control at the same counts;
- the Default Organization deleted and recreated, and the next
`get-session` reads the new id (#21868);
- `default` moved to another organization: the old organization's grant
stops anchoring, and the new one's starts.
-
`packages/core/src/security/resolve-authz-context.default-organization-memo.test.ts`
(new, 14 cases). Each case drives `resolveUserAuthzGrants` on a
recording double that carries the engine's seams. Covered:
- one read, then none (both slugs);
- `undefined` memoised;
- the no-seam control;
- delete-and-recreate;
- the slug move, granting direction;
- unrelated writes do not retire it, a watched grants table included;
- `remote` and `metadata` retire it, a bare `write` bump does not;
- in-flight write: dead on arrival;
- the TTL bound;
- the outage (envelope `SERVICE_UNAVAILABLE` / 503, then a re-read);
- the registry case, re-read once the object is registered;
- the pre-registration window, measured.
- **Pre-registration window, measured:** a write that starts before the
observer registers is not seen. The first resolution registers the
observer and reads the pre-write row. That answer stands, stale, until
the 30 s bound, and is re-read after it. This is the same blind spot leg
5 documents. At boot, plugin-auth's invariant writes the Default
Organization before the listener accepts, so a request racing that write
is the only way to reach the window.
## Ablations (each through `scripts/ablation-replace.mjs`, restore
proven blob == `HEAD`, `git diff HEAD` empty)
Runtime resolves `@objectstack/core` to source through its vitest alias,
and core's tests import it relatively, so no `dist/` leg applies. The
alias was also observed directly: the counts moved with no core rebuild.
1. **Memo removed** (the call site back to `readDefaultOrganizationId`):
golden red, 4 of 6. It reads 9 / 11 with 1 organization statement, and
10 / 12 with 2. Both retirement cases go red on `organizationReads: 0`.
2. **Write-seam retirement removed** (an empty watched set for the
memo): golden red, 2 of 6. Delete-and-recreate reads `standing: false,
organizationReads: 0`, and so does the slug move. Unit suite red, 4 of
14.
3. **A failed read memoised:**
- variant `.catch(() => undefined)`: the outage pin is red (`expected
undefined to match { code: 'SERVICE_UNAVAILABLE', status: 503 }`);
- variant "memoise, then rethrow": red on the re-read (`reads: []`,
`standing: false`).
## Verification (final head `307260452`, `origin/main` `c03b17d78`
merged)
- `pnpm --filter @objectstack/core test`: 107 files, 2594 tests passed.
- `pnpm --filter @objectstack/core test:repo`: 6 files, 82 tests passed.
No new caller of the reference-target census.
- `pnpm --filter @objectstack/plugin-security test`: 188 files, 3754
passed, 45 skipped.
- `pnpm --filter @objectstack/plugin-auth test`: 165 files, 3165 passed,
10 skipped.
- `pnpm --filter @objectstack/rest test`: 300 files, 6095 passed, 326
skipped.
- `pnpm --filter @objectstack/runtime test`: 423 files, 6651 passed, 19
skipped.
- `pnpm --filter @objectstack/core typecheck` exit 0 and `pnpm --filter
@objectstack/runtime typecheck` exit 0. Both test layers compile under
`tsconfig.test.json` with no new debt.
- After the merge: build of core, runtime, plugin-auth and plugin-email
(35 turbo tasks) exit 0; both typechecks exit 0; the two new pin files
and the leg 4/5 suites (request-rounds, batch-equivalence, localization
cache) pass.
- Derived gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`, with no paths, at `307260452`. All 67 were
run, each exit 0, captured before any pipe. `--ran` reports: "67
derived, 67 run, 0 NOT-MEASURED, 0 UNRUN" (a derived zero).
`check:dual-build-cjs-loads` exited 3 on the first pass, before a full
build. After `turbo run build` (76 tasks) it exits 0.
## Acceptance notes
- **Pin sweep.** Searched for `readDefaultOrganizationId`, the `{ slug:
'default' }` read, `grantAnchorsPlatformStanding`, and the
`*admin-standing*`, `*batch-equivalence*`, `*request-rounds*` and
`*default-organization*` tests. None moves: their doubles carry no write
seam, so the memo declines and their read multisets hold.
`resolve-authz-context.request-grants-memo.test.ts` and
`resolve-user-grants-cache.test.ts` carry the seams but hold no
organization-owned admin grant, and both pass unedited.
- **plugin-auth's own `readDefaultOrganizationId`**
(`last-admin-guard.ts`) is untouched. It runs on guarded writes
(`anchorOwnerTest`), not on every authenticated request, so it does not
pay this per-request cost.
- **The golden boots a real kernel:** test body about 3 s; most of the
wall is module import on the shared box.
- The PR body is written once at creation. Any later change is named in
the report for the seat to write.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Ug7sGD6qWVrzQNdihVX3T6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent b61353f commit 9a7838e
4 files changed
Lines changed: 673 additions & 20 deletions
File tree
- .changeset
- packages
- core/src/security
- runtime/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
0 commit comments