Repository navigation
Commit 2a40f98
fix(plugin-security): the seed ownership hand-over yields to the event loop between 50-row batches, so a sign-in during it is answered (#26913)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X9mRXg8QNKdP2mmSLvmtm2L)_
Fixes #26825
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:种子落定后把种子数据移交给平台管理员的那一步(`claimSeedOwnership`,以及多组织版
`claimOrgSeedOwnership`)改为每批 50 行写入,每写一批让出一次事件循环;按行写的路径每 50 行让出一次。
- **为什么改**:实测在 `os serve --dev`(文件型 sqlite,1,500 行带种子文件的种子)上,这一步是一次覆盖
1,500 行的写入,耗时 2,641 ms,整个事件循环被占住约 2.7 秒,期间的登录请求要等 2.0–2.9 秒;改后最长卡顿 190
ms,同期登录 p50 317 ms、最慢 539 ms。
- **风险与回滚**:成功路径结果不变(同样的行、同样的所有者、每行一条审计行)。唯一变化在失败时:一个 5,000
行分块里只有部分批次被拒时,其余批次已归属、剩下的行由下一次认领补上(以前整块都不归属);整块全被拒时与原来完全相同。席位已接受此差异。回滚即撤销本
PR。
-
**先看**:`packages/plugins/plugin-security/src/claim-seed-ownership.ts`(`claimPredicateInBatches`、`CLAIM_HANDOVER_ROWS`、`claimRows`
里的让出);`packages/plugins/organizations/src/claim-org-seed-ownership.ts`(`claimChunk`);两个新的
`*-yield.pin.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/plugin-security exec vitest run
src/claim-seed-ownership-yield.pin.test.ts`,预期 5 passed。
- **需要你做的**:无,席位落地。
## What changes
**The reading first, on `main` at `e78d7057`.** The card's attribution
came from a CPU profile whose async stacks are partial, so it was
re-measured before any edit. A scratch app (one object with a file
field, 1,500 seeded rows, each with a 20 KB `seedFile`) booted with `os
serve --dev` over file-backed sqlite, `OS_INLINE_SEED_BUDGET_MS=1` so
the seed settles in the background, a 20 ms event-loop lag sampler
preloaded, and a sign-in (`POST /api/v1/auth/sign-in/email`) every 250
ms:
- Three boots: the event loop stalled 2,763 / 2,762 / 2,742 ms in one
stretch that starts at `Seed loading complete` and ends at `handed 1500
seeded record(s)`. Sign-ins issued inside it took 2.0–3.2 s.
- A timing marker spliced into `plugin-security`'s `dist` around the
claim's `ql.update` (removed afterwards; the dist was copied back) read
three writes: `usr_system` arm 2 ms, the NULL arm's ONE write over 1,500
ids **2,641 ms**, then the warm re-pass 2 ms. So the whole stall is one
engine write.
- The CPU profile of that stretch is per-row hook work inside that
write: better-sqlite3 `_query`/`prepare`, plugin-audit's `writeAudit` →
`persistAuditTrailRow` (one audit row per row), engine `insert`, the
storage file-reference hooks, plugin-auth and plugin-sharing middleware.
Control on the same boot without a file field: the claim ran about 1.1 s
with no single stall above 483 ms.
**Why the cure is a smaller write, not a yield inside one.** The engine
runs a predicate write and its after-hooks in one transaction
(`inAfterAbortUnit`), and SQLite's pool holds one connection. A yield
inside the write would leave the sign-in waiting on the pool instead of
the event loop. So the atomic unit has to shrink.
**The fix.**
1. `claim-seed-ownership.ts`: the NULL arm's chunk (still
`CLAIM_PAGE_ROWS` = 5,000 ids, the unit of failure reporting) is written
`CLAIM_HANDOVER_ROWS` = 50 ids at a time by the new
`claimPredicateInBatches`, with one `setImmediate` turn after every
write. 50 is measured: about 1.8 ms of hook work per claimed row, so
about 90 ms per batch. The `owner_id = usr_system` arm is unchanged (one
predicate write, paged only on the per-row hook refusal) and yields
after it. The by-row path (`claimRows`, objects whose stored expression
reads `owner_id`) yields after every 50 rows; its writes were already
one per row.
2. `claim-org-seed-ownership.ts` (the multi-organization twin) had the
same unbroken run: one write per 5,000-id chunk, inside the request that
creates the organization. `claimChunk` now writes 50 ids at a time with
a yield after each, and `claimChunkByRow` yields every 50 rows.
3. The yield helper is a module-private copy in each file, by the seat's
ruling on this card (option B): the existing copies in
`metadata-protocol/src/seed-files.ts` and
`core/src/utils/import-runner.ts` are not exported, and exporting one
would widen a published surface. Each docblock names the other copies.
**Failure semantics, as accepted by the seat.** Success is unchanged:
the same rows, the same owner, one audit row per claimed row. A chunk
every batch of which is refused behaves exactly as on `main`:
plugin-security rethrows the first refusal so the caller logs the same
single warning and the chunk stays unowned; organizations logs the same
`the N seeded row(s) of this write stay owner-less` warning. When only
some batches of a chunk are refused, the accepted batches stay owned,
one warning names the rows left unowned (count, the first 20 ids, the
first error), and the next run claims them; on `main` one refused write
left the whole chunk unowned.
**Wall time.** The hand-over itself is not faster: on the live boot it
spanned 3.4 s after the fix against 2.6 s before, because sign-ins now
run between its batches.
## Why
The first admin who signs in while a demo or package seed is still
settling waited several seconds on a fresh boot.
## Tests
- New
`packages/plugins/plugin-security/src/claim-seed-ownership-yield.pin.test.ts`,
on a real `ObjectQL` over `SqlDriver` (better-sqlite3 `:memory:`), 320
seeded rows, a code-registered `afterUpdate` hook writing one audit row
per updated row (the shape of plugin-audit's writer) and a
`beforeUpdate` hook that can refuse named rows:
- `re-owns at most one batch of rows between two macrotask turns`
- `serves a read issued during the hand-over before the hand-over ends`
- `hands every seeded row to the admin with one audit row per claimed
row`
- `a refused batch leaves only its rows unowned, says which, and the
next run claims them`
- `every batch refused: nothing claimed, and the one warning a refused
chunk always gave`
- New
`packages/plugins/organizations/src/claim-org-seed-ownership-yield.pin.test.ts`:
the same five on `SqliteWasmDriver`, plus a row in another organization
that stays untouched.
- Updated shape pins that counted writes per 5,000-id chunk:
plugin-security `issues ONE predicate write per unowned shape and
hand-over batch`, the over-ceiling pin (the NULL arm's writes counted
apart from the `usr_system` arm's), `writes the seeded ids in hand-over
batches`; organizations' 10,500-row pin, the unreadable-count pin (now
one warning per chunk) and the failed-chunk pin (now refuses every write
of the second chunk).
**Red first.** Production source restored to `main` (`git show
e78d705:FILE`), the new pins run, restored from `HEAD` and proven by
blob hash:
- plugin-security: 3 failed, 2 passed. `no macrotask turn ran while the
hand-over ran: expected 1 to be greater than 6.4`; `expected [
'hand-over', 'read' ] to deeply equal [ 'read', 'hand-over' ]`; the
partial-failure pin red (`main` leaves the whole chunk unowned). The
result pin and the every-batch-refused pin are GREEN on `main`: that is
the proof that ownership, audit rows and the total-failure warning are
identical.
- organizations: the same 3 failed, 2 passed.
**Ablations** (`scripts/ablation-replace.mjs`, anchor counts and the
HEAD-blob restore checked by the tool; the tests import source, so no
`dist` is involved):
- A, the yield resolves at once: plugin-security 2 failed (the two yield
pins); organizations 2 failed (the same two).
- B, `CLAIM_HANDOVER_ROWS = CLAIM_PAGE_ROWS`: plugin-security 3 failed
(`expected 5 to be greater than 6.4`, `expected 320 to be less than
320`, partial-failure pin).
- C, no rethrow when every batch is refused: plugin-security 1 failed
(the every-batch-refused pin's warning text).
**Live re-measure** with the fix built into `dist`, same app and boot:
the stretch from `Seed loading complete` to `handed 1500` had no stall
above 190 ms (before: 2,669–2,763 ms in one stretch), and the 14
sign-ins issued in it answered p50 317 ms, slowest 539 ms (before: p50
1,967 ms, slowest 2,698 ms).
## Acceptance notes
- **Sharing rules on a large seeded object.** plugin-sharing's
`afterUpdate` recomputes per row up to 1,000 rows per write and above
that revokes the object's rule grants and re-grants them in the
background (with a warning). A 50-row batch is always under that cap, so
on an object with sharing rules and more than 1,000 seeded rows the
claim now recomputes per row inline (no revoke-and-re-grant window)
instead of one revoke plus a queued re-grant. The end state is the same
rule grants; the `CLAIM_PAGE_ROWS` docblock reasoning about the 1,000
cap still applies to the `usr_system` arm's pages.
- **The `usr_system` arm is unchanged**, by ruling. It is still one
predicate write over every such row, so a seed whose records set
`owner_id` to the seed identity would still be claimed in one run. No
producer was found: HotCRM and the examples write no `owner_id` in
seeds, and the measured seed landed every row NULL. Noted, not filed.
- On `InMemoryDriver` the NULL arm's cost does not change: an `id IN`
list of B ids over T rows costs T×B per write and there are N/B writes,
the same T×N as one write.
## Verification
<details><summary>Readings (final head d51a116)</summary>
All at `d51a116d` (the final head); each exit code captured before any
pipe.
- `pnpm --filter @objectstack/plugin-security test && pnpm --filter
@objectstack/plugin-security typecheck` under the verify lock: `Test
Files 232 passed (232)`, `Tests 5037 passed | 45 skipped (5082)`,
`check:test-typecheck: OK`, lock VERDICT command-exit 0. The new test
files are in `tsconfig.test.json`'s program, which
`check:test-typecheck` compiles.
- `pnpm --filter @objectstack/organizations test && pnpm --filter
@objectstack/organizations typecheck`: `Test Files 18 passed (18)`,
`Tests 248 passed (248)`, lock VERDICT command-exit 0.
- `pnpm exec eslint --no-inline-config packages/plugins/plugin-security
packages/plugins/organizations`: exit 0.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 62 commands; each was run: 61 exit
0, `pnpm check:dual-build-cjs-loads` exit 3 (PREREQUISITE NOT MET, needs
a full build: NOT MEASURED, CI runs it). `node
scripts/check-plugin-teardown-shape.mjs --self-test` first exited 3 (a
shallow checkout lacked its positive-control commit) and exit 0 after
`git fetch --depth=1` of that commit. `--ran` reconciliation: `62
derived famil(ies) accounted for — 61 run, 1 NOT-MEASURED`, exit 0.
- Dependency closure built before any reading: `pnpm turbo run build
--filter=@objectstack/cli... --filter=@objectstack/organizations...
--concurrency=2`, lock VERDICT command-exit 0; then both touched
packages rebuilt for the live re-measure.
- Overlap: none of the 41 open PRs touches `claim-seed-ownership*` or
`claim-org-seed-ownership*` (each PR's `/files` read).
- Live boots and the dist timing marker: scratch scripts, not committed;
numbers in What changes and Tests.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No `packages/spec` change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes (eslint on both touched
packages, exit 0).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the maintainer summary
is in Chinese by the lane's convention).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5mvv8ZvVXKGoTSX3eE)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0c47e87 commit 2a40f98
7 files changed
Lines changed: 759 additions & 59 deletions
File tree
- .changeset
- packages/plugins
- organizations/src
- plugin-security/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
120 | 120 | | |
121 | 121 | | |
122 | 122 | | |
| 123 | + | |
| 124 | + | |
123 | 125 | | |
124 | 126 | | |
125 | 127 | | |
| |||
291 | 293 | | |
292 | 294 | | |
293 | 295 | | |
294 | | - | |
295 | | - | |
296 | | - | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
297 | 300 | | |
298 | 301 | | |
299 | 302 | | |
300 | | - | |
| 303 | + | |
301 | 304 | | |
302 | 305 | | |
303 | 306 | | |
304 | 307 | | |
305 | 308 | | |
306 | | - | |
| 309 | + | |
307 | 310 | | |
308 | 311 | | |
309 | 312 | | |
310 | 313 | | |
311 | 314 | | |
312 | 315 | | |
313 | | - | |
| 316 | + | |
314 | 317 | | |
315 | 318 | | |
316 | 319 | | |
| |||
323 | 326 | | |
324 | 327 | | |
325 | 328 | | |
326 | | - | |
327 | | - | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
328 | 334 | | |
329 | 335 | | |
330 | 336 | | |
| |||
340 | 346 | | |
341 | 347 | | |
342 | 348 | | |
343 | | - | |
| 349 | + | |
344 | 350 | | |
345 | | - | |
346 | | - | |
| 351 | + | |
| 352 | + | |
347 | 353 | | |
348 | | - | |
349 | | - | |
| 354 | + | |
350 | 355 | | |
351 | 356 | | |
352 | 357 | | |
| |||
361 | 366 | | |
362 | 367 | | |
363 | 368 | | |
| 369 | + | |
364 | 370 | | |
365 | | - | |
366 | | - | |
| 371 | + | |
| 372 | + | |
367 | 373 | | |
368 | 374 | | |
369 | 375 | | |
0 commit comments