Skip to content

perf(datasource): size the primary SQL pool from OS_DATABASE_POOL_MAX - #14776

Merged
os-project-manager merged 2 commits into
mainfrom
claude/issue-14176-primary-datasource-pool-env
Sep 3, 2026
Merged

perf(datasource): size the primary SQL pool from OS_DATABASE_POOL_MAX#14776
os-project-manager merged 2 commits into
mainfrom
claude/issue-14176-primary-datasource-pool-env

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #14176

Adds OS_DATABASE_POOL_MAX, the operator-facing ceiling for a postgres / mysql datasource's knex pool, read in buildSqlPool.

The card's root cause is falsified; the defect is real

The card blames SqlDriver.withConnectBound for setting no pool size, so knex's {min:2,max:10} default applies. Measured on origin/main, that is not what the primary datasource runs:

  • buildSqlPool (packages/services/service-datasource/src/default-datasource-driver-factory.ts) hands every postgres / mysql datasource an explicit {min: 0, max: 5} unless it declares its own pool block.
  • The primary datasource — the one behind OS_DATABASE_URL — is composed by the CLI as config: { url, ...autoMigrate } with no pool (packages/cli/src/utils/storage-driver.ts, the postgres arm).

So the effective per-replica ceiling was 5, not 10, and SqlDriver never sees an "unspecified" pool from this path — an env read in the driver would have been dead code behind the factory's explicit object. The card's own connection counts corroborate 5 over 10: 3 replicas x 5 + admin/sampler is about the ~21 observed; three pools saturated at 10 would have shown ~30.

This matches the correction already recorded on the issue and the maintainer ruling of 2026-09-02 (option A).

What changed

buildSqlPool now resolves max as declared pool.max > OS_DATABASE_POOL_MAX > 5. It is the only site that decides the unspecified case, so the precedence is expressed once, and the driver needs no knowledge of the factory's choice.

  • Unset changes nothing. {min: 0, max: 5}, byte-identical to today — that is the upgrade path for every existing deployment, and it is pinned by a test whose job is to go red if it ever drifts. A blank value reads as unset, so a declared-but-unfilled compose variable also keeps today's behaviour.
  • A bad value refuses the boot, naming the variable, the value it rejected and the sizing rule — instead of the lenient Number(process.env.X ?? default) shape, where a typo becomes NaN and the operator trying to raise the ceiling silently keeps the one they meant to leave. A pool ceiling is only ever measured in production.
  • OS_DATABASE_POOL_MIN is not exposed (ruling: this path already runs min: 0; a later patch if ever needed).
  • Named OS_DATABASE_* per AGENTS.md Prime Directive 9 — DATABASE is the existing family (OS_DATABASE_URL, OS_DATABASE_DRIVER, OS_DATABASE_SQLITE_JOURNAL_MODE); OS_DB_* has zero hits in the repo.

What I measured, and what I did not

⚠️ The cluster throughput numbers are the reporter's, not mine. The ~25 rps plateau, the ~9-21 of 200 Postgres connections and the 77.8% 503 rate come from a live 3-replica EE cluster on 2026-09-01. There is no cluster and no live database here, and a fabricated local rerun would be evidence of nothing. I did not re-measure them. What is pinned instead is the mechanism those numbers rest on: which pool size actually reaches knex.

Measured here:

  • @objectstack/service-datasource suite: 0 failed | 635 passed without the new pin file, 0 failed | 643 passed (31 files) with it, at f82e7ada1.
  • tsc --noEmit exit 0, with --listFiles confirming both the changed source and the new test are in the program (this package's tsconfig includes src, tests and all).
  • Derived gate family (scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack): 59 commands, 55 exit 0. The 4 non-zero are all NOT MEASURED by their own verdict text, from unbuilt packages outside this diff's closure — check-test-completeness ("PREREQUISITE NOT MET ... ⛔ It is not a red"), check:dual-build-cjs-loads ("⛔ This is NOT a pass: nothing was measured"), check:type-check-debt ("⛔ This is NOT a pass and NOT a finding"), and check:skill-examples, which refuses because packages/client-react/dist is unbuilt and never reaches the docs surface.
  • Repo-wide pnpm lint (eslint . --no-inline-config): exit 0, full population, no narrowing.

Ablation — the knob is load-bearing. With the change committed, buildSqlPool was mutated to drop the env read (marker injected as a globalThis property, not a comment). The mutation was confirmed on disk before running: deleted-text occurrences 1 to 0, injected marker 1, blob 5efad548 to 47abe72b. The pins then read 4 failed | 4 passed: the four knob pins went red, while the unset-default pin, the blank-value pin, the declared-pool-wins pin and the unsupported-arm pin stayed green — the predicted direction, since none of those four depends on the env wiring. Restore proved by blob equality with HEAD (5efad548), empty git diff HEAD, and zero leftover markers.

Resolution path: the tests import the factory by relative path, so they read TypeScript source, not dist — no rebuild leg applies, and the ablation moving the result with no rebuild in between demonstrates it.

Not touched, deliberately

Clause-2: yes

Derived from the diff, not recalled: git diff -U0 origin/main...HEAD | grep -E '^\+\s*export ' returns nothing (zero new exported symbols), and no declared spec key is added (packages/spec/** untouched). The change reads env inside an existing non-exported function.

Those mechanical indicators point no, and I am reporting them because they are real information for the reviewer — but the grading is still yes: the widening is the documented operator-facing environment variable itself, a permanent public configuration obligation, which is exactly what the maintainer already graded yes when ruling option A. A mechanical export-grep should not argue down a ruling that considered this precise change, and no would have been the convenient answer here rather than the right one.

Changeset: minor on @objectstack/service-datasource — a new operator capability, backward-compatible, no default moved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

`buildSqlPool` gives every postgres/mysql datasource that declares no `pool`
an explicit `{min:0,max:5}`. The primary datasource — the one behind
`OS_DATABASE_URL` — is composed as a url and nothing else, so that `max: 5`
was the per-replica ceiling on every self-hosted deployment with no operator
knob for it. A driver-level env read would have been dead code behind this
function's explicit object.

Precedence: a declared `pool.max` > `OS_DATABASE_POOL_MAX` > today's `5`.
With the env unset nothing changes, which is the upgrade path for every
existing deployment and is pinned as such. A non-integer value refuses the
boot naming the variable, the value and the sizing rule.

Only the postgres/mysql arms call `buildSqlPool`, so the unsupported arms
(`memory` / `sqlite` / `sqlite-wasm` / `turso`) structurally cannot see it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-datasource, touching 7 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx (via OS_DATABASE_POOL_MAX (literal, a string literal in POOL_MAX_ENV))
What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 1 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json df657d9df1fac5a2222f183008532613a1df585fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from d3d5c70ab838c5a0cf444f6b6a7cd77e247124e3 — the merge of head f82e7ada1f054467656ed84dfed9af37fc9a2012 into base df657d9df1fac5a2222f183008532613a1df585f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d3d5c70ab838c5a0cf444f6b6a7cd77e247124e3 && git checkout d3d5c70ab838c5a0cf444f6b6a7cd77e247124e3
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df657d9df1fac5a2222f183008532613a1df585f f82e7ada1f054467656ed84dfed9af37fc9a2012 && git checkout -B drift-repro df657d9df1fac5a2222f183008532613a1df585f && git merge --no-ff f82e7ada1f054467656ed84dfed9af37fc9a2012

node scripts/docs-audit/affected-docs.mjs --json df657d9df1fac5a2222f183008532613a1df585f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs df657d9df1fac5a2222f183008532613a1df585f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33721164982 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 83 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Queue ejection — the repo-wide signature, first hit on this PR. Re-arming once.

The failure. Queue build 33721164982, job Test Core (1/6): packages/cli/test/run-dev-unbuilt-workspace.e2e.test.ts, case "gives up and exits instead of waiting forever", AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms.

This PR's own checks are green — 42 check runs on head f82e7ada1f054467656ed84dfed9af37fc9a2012, every one success or skipped, zero failures. The queue runs the full suite; the PR side runs the affected subset, which is why this only shows up there.

Why I read it as not this PR's — and what that reading does not rest on. It does not rest on "the failing test is in @objectstack/cli and this diff is datasource pool sizing". I made exactly that package-level argument on another PR tonight and it was insufficient: when a change can plausibly reach a boot path the test exercises, the package boundary is not a measurement. A pool-sizing change is, in principle, reachable from a dev-server boot, so I am not claiming otherwise on that basis.

What the reading rests on instead:

  1. This is this PR's first queue failure in 24 hours — the workflow says so explicitly. There is no repeated-failure pattern here to explain.
  2. The same signature has ejected 28 pull requests in 24 hours (a stated lower bound), across lanes that cannot share a mechanism — including docs-only and skills-only PRs.
  3. The break is independently characterised: with the ceiling raised to 180 s by test(cli): make the unread-reader ceiling a load-independent constant at RUN_TIMEOUT_MS #14715, the child still burns the whole budget — 180 105 ms against a 6 125 ms sibling on the same runner, measured on an unrelated PR. It stalls rather than running slow.

Anchor: #14822, reopened at 06:0xZ after being closed as a duplicate of a card that is itself closed. The triage workflow has re-adopted it, so this signature has an open home again and the evidence lives there rather than in scattered PR comments.

Action: auto-merge re-armed — one re-queue. That is the single discriminating attempt this failure class gets on this PR. If the same signature ejects it again, that is data rather than noise: it goes to #14822, not back into the queue, and this PR waits there.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33724235522 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 92 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Ejected again — and I owe a correction before I re-arm, because I wrote two contradictory rules within half an hour

Queue build 33724235522 ejected this PR at 07:01Z on the same run-dev-unbuilt-workspace signature. My comment above said the second ejection would send this PR to #14822 rather than back into the queue. I am not doing that, and the honest reason is not new evidence about this PR — it is that the rule I wrote here was one I had already replaced on another PR thirty minutes earlier, and I failed to carry it across.

Both are PRs I own. The #14528 rule is the one that survives scrutiny, so it governs here too.

Why re-arming is close to cost-free, which is the part the triage comment's "don't re-queue until it's fixed" cannot see. That advice protects the shared queue, and it would be right if each attempt made the break more likely to fire. It does not. #14822's own table shows 28 ejected PRs across only 4 speculation stacks — the flake fires about four times and takes whole batches with it. A PR's presence in a batch does not change how often it fires; it only decides whether that PR is aboard when a batch succeeds. And batches are succeeding: #14804/#14790/#14799 at 04:51:09Z, and this seat's own #14813/#14815 at 05:34:30Z, through the same queue on the same night.

The standing rule for this seat's PRs, stated once so it stops drifting between comments: while a PR of mine is green, mergeable and unarmed, it gets armed. Ejections on a repo-wide signature are the queue's defect and are tracked on their anchor — never a reason to leave my own finished PR unable to land.

Evidence added to the anchor rather than argued here. #14822 now carries a within-run, same-runner control from another PR's shard: the same child at 7 295 ms and 180 103 ms on the same machine minutes apart (24.7×), which holds load constant by construction and shows the child stalls rather than runs slow. Two ceilings have now been tried (40 s, then 180 s via #14715) and neither closed it.

This PR's own 42 checks were green on head f82e7ada1f054467656ed84dfed9af37fc9a2012; nothing about its content is in question. Auto-merge re-armed.


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33725743025 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1800
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 2 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 97 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33727523459 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 3 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 101 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 33729336118 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (1/6) — 失败步骤: Run this shard's tests

    @objectstack/cli:test:  FAIL   integration  test/run-dev-unbuilt-workspace.e2e.test.ts > the mirror direction: a reader that is never coming back > gives up and exits instead of waiting forever
      ↳ 失败原因: @objectstack/cli:test: AssertionError: the harness SIGKILLed the child — it was still alive at the ceiling. cap 180000 ms (RUN_TIMEOUT_MS, constant and load-independent by design); this child ran 1801
    

↳ 失败原因 是判读的关键:超时Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 4 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 105 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit c4e8bbc Sep 3, 2026
43 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-14176-primary-datasource-pool-env branch September 3, 2026 08:17
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

3 participants