Skip to content

[finding][services] datasource pool 还有两处静默丢弃:turso 整块、mongodb 的两个 timeout 键(#5714/#5931 拒绝集合覆盖不到的剩余面) #7243

Description

@os-zhuang

发现于 #6214 的台账精确化工作(把 packages/spec/liveness/datasource.json 的四行 pool.* 按驱动限定)。为了让台账不再高估 pool 的生效面,必须逐臂读一遍落地真值 —— 读出来的时候发现,#5714 / #5931 把 sqlite / sqlite-wasm / memory 变成响亮拒绝之后,仍有两处「可写、被静默吞掉」的面。两处都不在本卡声明面内(收紧公开 authoring 面是契约决策),单独记录,严重度请分诊定。

台账已如实把两处记为「still dropped in silence」,不是漏看 —— 与 #5714 当初把 memory 记进模块注释是同一个做法。

事实一:turso 整块 pool 被静默丢弃

turso 不在 POOL_UNSUPPORTED_DRIVER_IDS 里,而 driverReadsDeclaredPool 对不在拒绝集合里的 id 一律答 true,所以工厂的拒绝闸门放行;turso 臂随后压根不读 spec.pool。

  • packages/services/service-datasource/src/datasource-pool-support.ts:79 — 拒绝集合是 ['memory', 'sqlite', 'sqlite-wasm']。
  • packages/services/service-datasource/src/default-datasource-driver-factory.ts:488-530 — turso 臂只把 url / authToken / encryptionKey / concurrency / syncUrl / sync 传给 TursoDriver,spec.pool 从不出现。

这一处落地代码自己已经写明,不是新发现,只是没有卡:datasource-pool-support.ts:90-99 原文 —

TursoDriverConfig has no min/max, only concurrency, and in local mode the driver is a better-sqlite3 SqlDriver — the very engine POOL_UNSUPPORTED_DRIVER_IDS rejects a pool block for. A declared pool on a turso datasource is therefore dropped in silence today. Changing that is a new rejection on an authoring surface and needs its own ruling; see the #6345 PR's follow-ups.

#6345 的验收评论(PR #6910)里没有立这张卡,所以「见 #6345 的 follow-ups」今天指向不存在的东西。本卡补上。

⚠️ 注意这一臂的形状比 sqlite 更难判:local 模式(file: url)下 TursoDriver 确实就是 better-sqlite3 SqlDriver,与 sqlite 臂同因;remote 模式(libsql://)下没有 knex 连接池、只有 concurrency。是「整臂拒绝」还是「按 url 模式分叉」,不是实现方能自己扩的。

事实二:mongodb 只读 min / max,两个 timeout 键落地无人接

mongodb 在拒绝集合之外、且确实读 pool —— 但只读两个键:

packages/services/service-datasource/src/default-datasource-driver-factory.ts:477-483
  const pool = (spec.pool ?? {}) as Record<string, unknown>;
  const driver = new MongoDBDriver({
    url: buildMongoUrl(spec),
    ...(cfg.database ? { database: cfg.database } : {}),
    ...(cfg.options && typeof cfg.options === 'object' ? { options: cfg.options } : {}),
    ...(typeof pool.min === 'number' ? { minPoolSize: pool.min } : {}),
    ...(typeof pool.max === 'number' ? { maxPoolSize: pool.max } : {}),
  });

pool.idleTimeoutMillis 与 pool.connectionTimeoutMillis 在这一臂不出现在任何位置。二者只在 buildSqlPool(:188-198,postgres / mysql)里被读。

所以一个 mongo 数据源写 pool: { max: 20, idleTimeoutMillis: 30000 } 会得到:maxPoolSize 生效、idleTimeoutMillis 无声消失。这是同一个块内的半生效,比整块丢弃更难被作者察觉 —— 「我的 pool 配置生效了」有一半是真的。

MongoClient 本身有对应旋钮(maxIdleTimeMS / connectTimeoutMS 或 serverSelectionTimeoutMS),所以这一处与 sqlite/memory 不同类:不是「没有东西能承接」,而是「有东西能承接但没接」,候选修向里多一个「接上」的选项。

仓内标本

候选修向(不预判,列给分诊)

  1. turso:(a) 整臂并入 POOL_UNSUPPORTED_DRIVER_IDS(按 [skill] pm-dispatch: record two meta-judgments approved 2026-08-07 (silently-dropped declarations; two implementations of one operation) #6140 第 1 条 meta-裁定,「静默丢弃的键默认并入既有拒绝集合」的默认路径,需要为它写自己的 POOL_UNSUPPORTED_REASONS 条目);(b) 按 url 模式分叉判定;(c) 维持现状并在 spec 的 .describe() 里写明。
  2. mongodb 的两个 timeout:(a) 接上 MongoClient 的 maxIdleTimeMS / connectTimeoutMS(「实现」而非「拒绝」,ADR-0049 enforce-or-remove 的 enforce 边);(b) 做成按键的拒绝(现有 helper 是按驱动 id 全块拒绝,这需要新形状);(c) 维持现状并在台账/文档写明。

注意 #6140 的边界条款:「母单的理由被实测为臂特有时,默认不顺延」。sqlite 的理由(:memory: 会把一个数据源劈成多个库)对 mongo 的 timeout 键明显不适用,对 turso remote 也不适用 —— 所以两处都不能直接套用默认路径,这正是分别立卡的原因。

出处链

#5714(sqlite / sqlite-wasm 臂拒绝)→ #5931(memory 并入)→ #6214(spec 台账按驱动限定,PR #7242 —— 台账已把这两处如实记为 still-silent)。

去重

已搜索 issues/PR:turso pool、mongodb pool minPoolSize maxPoolSize、idleTimeoutMillis、datasource pool —— 除 #5714 / #5931 / #6214 三张既有卡外无重复。#6268 / #7099 是 turso 的另两条缝(loader 双份、remote upsert),与本卡不同门。

Activity

  1. claude commented on Aug 11, 2026

    @claude
    Contributor

    Maintainer ruling — 2026-08-11. From the four-lens decision review (platform long-term coherence / measured business pull / AI-agent error-resistance / startup scope discipline); the maintainer accepted the recommendation set in full.

    Ruling, both halves: (1) turso joins POOL_UNSUPPORTED_DRIVER_IDS whole-arm — no url-mode forking. Local mode is literally the engine the rejection set exists for; remote mode has no pool either; the fork buys complexity and serves no measured consumer. (2) mongodb's two unread timeout keys are REJECTED loudly, not wired. Wiring would be pull-less behavior-surface expansion; rejection matches the #5714/#5931 family, keeps declared=enforced, and tells an author (human or AI) immediately instead of silently dropping their config. Wire them later iff real demand appears — at that point it is a one-line, pulled change. State: needs-user-decision → pm:queue.

    Recorded by the triage seat Routine (#5474 pilot) on the maintainer's direct instruction — ruling record, not a claim.


    Generated by Claude Code

  2. self-assigned this
    on Aug 11, 2026
  3. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    Claim: PM loop, services lane, wave 2 round 1
    Session: session_015fkdTyGmMD5s8ZtEifvuGy
    Branch: claude/issue-7243-pool-rejection-remainder
    Worktree: objectstack-issue-7243
    Domain: domain:services
    File surface: packages/services/service-datasource/src/ (default-datasource-driver-factory.ts + its tests; the POOL_UNSUPPORTED_DRIVER_IDS set and the two mongodb pool-key reads). Stop on breach; explain in the report.
    Container & model: S-grade with the design fully ruled (maintainer 2026-08-11 comment above — both halves specified, rejection-test pattern established by #5714/#5931), mode:subagent, model: opus — tiered up from sonnet because rejection-message content pins still carry judgment; container stays subagent because correctness is gate- and family-pattern-checked.
    Serial constraints cleared: this file's serial queue is clear — #7384 and #7418 (same file, this shift) are both MERGED and in the base; no open PR or in-flight claim touches service-datasource. #7314 (same file) is open but ⛔ not dispatchable (cross-lane re-route pending), noted as the next-in-line if it ever returns here.


    Generated by Claude Code

  4. os-help commented on Aug 11, 2026

    @os-help
    Collaborator
    {
      "issue": 7243,
      "status": "done",
      "branch": "claude/issue-7243-pool-rejection-remainder",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7600",
      "premise_still_valid": true,
      "summary": "Both halves of the 2026-08-11 ruling implemented as written. `turso` joins POOL_UNSUPPORTED_DRIVER_IDS whole-arm (no url-mode fork) with its own explanation naming both transports, rather than borrowing SQLite's. mongodb's two unread timeout keys are rejected BY NAME through a new per-key layer (POOL_UNREAD_KEYS_BY_DRIVER / unreadPoolKeys / unreadPoolKeysMessage) — the whole-block set could not express it, because `min` / `max` on that arm are honoured and must keep working. Both gates route through the existing unsupportedPoolIssue, so all three doors (Setup wizard create/update, boot auto-connect pre-pass, driver factory) inherit them with no call-site change. Premise verified by MEASUREMENT before any edit, through the real factory on origin/main: the turso arm never references spec.pool (its create() reached the missing-package throw with the pool block never consulted), and mongodb built a driver whose config was {url, database:'orders', maxPoolSize:20} — both timeouts gone. PM mechanism assumptions confirmed except one line-number detail: the machinery lives in datasource-pool-support.ts (POOL_UNSUPPORTED_DRIVER_IDS at :79 on origin/main), not in default-datasource-driver-factory.ts; the factory only calls assertDatasourcePoolSupported at the top of create(). The exact keys mongodb reads are `min` / `max` and nothing else — pinned against the arm's own source rather than remembered. TWO FILES OUTSIDE THE CARD'S NAMED SURFACE, flagged deliberately: packages/spec/liveness/datasource.json (its four pool.* rows and block note say these are 'still dropped in silence' — true when #6214 wrote it, false the moment this lands) and content/docs/data-modeling/drivers.mdx (the pool paragraph named only the two sqlite arms and had never picked up `memory` from #5931; rewritten as a per-driver verdict table, since the sentence being edited IS the list of who rejects the block). Neither touches the out-of-surface items named on the card: missingTursoDriverMessage, the other arms' remedy builders, and #7314's loader-identity questions are untouched.",
      "tests": "pnpm --filter @objectstack/service-datasource test → 'Test Files 12 passed (12) / Tests 326 passed (326)' (30 new, all in datasource-pool-support.test.ts; 72/72 in that file). typecheck → 'tsc --noEmit' clean (one lap: rejectionOf() typed Promise<unknown> while IDatasourceDriverFactory.create may answer synchronously — widened to unknown). DOWNSTREAM SWEEP BY PREFIX FILTER ('...@objectstack/service-datasource' = the 14 consumers, not the suffix/upstream direction): @objectstack/runtime 'Test Files 124 passed / Tests 1990 passed', @objectstack/cli 'Test Files 109 passed / Tests 1182 passed' — the two hosts that dispatch turso. Build closure first per toolchain trap 2 ('@objectstack/service-datasource^...' then the runtime/cli closures). REVERSE VERIFICATION, run twice, by surgically reverting the two behaviour changes (turso out of the set, key table emptied) while keeping the module loadable so failures name real cells: 20 red / 52 green, and the direction is the point — 'turso + pool is rejected by the pool gate, not by the missing-package arm' failed as \"expected 'datasource 'edge': a libSQL/Turso d…' to contain 'Datasource 'edge' declares a `pool`…'\", i.e. the UNFIXED arm still THROWS (the missing-@objectstack/driver-turso error), so a bare .rejects.toThrow() would have been green on the very arm this card targets. That is why every new case asserts message content — this path throws plain Errors by family convention (no ADR-0112 envelope), so the pins are on the datasource name, the key names and the per-arm reason text. Factory/wizard cases that used to build silently fail as 'expected the declaration to be rejected, but it built it instead of rejecting: {…}' via a helper added so acceptance never surfaces as a confusing undefined.message. The anti-drift pin failed as \"expected [ 'max', 'min' ] to deeply equal [ 'connectionTimeoutMillis', …(3) ]\" — it re-derives the four declared keys from DatasourceSchema and mongodb's read keys from the factory arm's source, asserting read ∪ rejected covers the block exactly and disjointly, so wiring maxIdleTimeMS later (the deferred option) fails until the key leaves the table. GATES run locally, scoped per the card: check:nul-bytes OK (7015 files), check:docs-audit-scope ✓ (179 docs in sync), scripts/check-changeset-fixed.mjs ✓, plus @objectstack/spec check:liveness ✓ with the ledger edit. No new fake engine double, so check:engine-double-contract has no new surface. CI at report time: ESLint / TypeScript Type Check / Check Changeset / Spec property liveness all in_progress, nothing concluded yet (reported honestly per the dispatch's report-at-draft-PR-time instruction; PM owns convergence).",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-help commented on Aug 11, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7600, reviewed against GitHub rather than the report's own claims. The maintainer's 2026-08-11 two-half ruling is executed as written.

    Verified in the diff (7 files):

    • Half 1 — turso joins POOL_UNSUPPORTED_DRIVER_IDS whole-arm, no url-mode fork, with its own explanation naming both modes (local better-sqlite3 engine = the class the set exists for; libsql:// = request transport with no persistent connections, capped by concurrency) rather than borrowing SQLite's prose. An author on the remote transport reads about their own datasource.
    • Half 2 — a new, narrower shape done right: POOL_UNREAD_KEYS_BY_DRIVER rejects mongodb's two unread timeout keys by name while min/max keep working (they are honoured, and regression nails pin that they still build to minPoolSize/maxPoolSize). A data table, not a per-arm if — the next half-reading arm is one line. Both gates route through the existing unsupportedPoolIssue, so all three doors (Setup wizard, boot pre-pass, factory) inherit with zero call-site changes.
    • Message discipline held: datasource named, keys named, deliberate refusal stated, one fix, no escape hatch (datasource fail-fast 不认识「工作区未构建」这个成因 —— 对 ERR_MODULE_NOT_FOUND 仍建议改配置或设 OS_ALLOW_DRIVER_CONNECT_FAILURE=1(两条都是有害建议) #5794) — and the mongodb message states what survives the edit, which a whole-block message cannot.
    • The anti-drift pin is the standout: it re-derives the declared pool keys from DatasourceSchema and mongodb's actually-read keys from the factory arm's own source, then asserts read ∪ rejected covers the block exactly and disjointly. If anyone later wires maxIdleTimeMS (the ruling's deferred option), this pin fails until the key leaves the rejection table — declared = enforced, kept true by construction.
    • Reverse verification asserts content, not throwing: 20 red under surgical revert, including the case that proves why — the reverted turso arm still throws (the missing-package error), so a bare .rejects.toThrow() is green on the unfixed code. Exactly the defect class the dispatch clause named.
    • Downstream sweep by prefix filter: runtime 1990 / cli 1182 passed — the two hosts that dispatch turso.

    Two declared out-of-surface files, adjudicated under the three-criteria rule — both accepted:

    1. packages/spec/liveness/datasource.json (+5/−5) — the fix(service-storage): refuse a predicate update that writes a file field (#7102) #7224/fix(rest,objectql): the import dry run asks the engine for its verdict instead of predicting it (#4633) #6532 precedent shape exactly. (a) Loudly declared in its own PR section; (b) mandated by the ledger discipline: its four pool.* verdicts read "still dropped in silence", which is false the moment this lands, and @objectstack/spec check:liveness is green with the edit; (c) no atomicity-preserving split — landing the rejection without the verdict update ships a false ledger on main. Cross-seat declaration to the domain:spec seat posted on [PM seat] domain:spec — ⏳ vacant #6017, per precedent. Pre-existing stale line numbers in the file were left alone rather than churned — correct restraint.
    2. content/docs/data-modeling/drivers.mdx (+27/−10) — the paragraph being edited is the list of who rejects the block; it named only the two sqlite arms and had never picked up memory (datasource pool 声明在 memory 驱动臂同样被静默丢弃(#5714 的姊妹臂,裁决未覆盖) #5931), so it was already stale and becomes more wrong after landing. Rewritten as a per-driver verdict table. Declared, necessary for the docs not to lie, and it incidentally repairs the pre-existing datasource pool 声明在 memory 驱动臂同样被静默丢弃(#5714 的姊妹臂,裁决未覆盖) #5931 omission.

    CI at review time: in progress. Driving to landing: gate conclusions → ready → arm → queue-branch verify (behind #7592/#7595, which are already queued).


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions