Repository navigation
[finding][services] datasource pool 还有两处静默丢弃:turso 整块、mongodb 的两个 timeout 键(#5714/#5931 拒绝集合覆盖不到的剩余面) #7243
Description
Activity
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)
tursojoinsPOOL_UNSUPPORTED_DRIVER_IDSwhole-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
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; thePOOL_UNSUPPORTED_DRIVER_IDSset 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 touchesservice-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
{ "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
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 —
tursojoinsPOOL_UNSUPPORTED_DRIVER_IDSwhole-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 byconcurrency) 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_DRIVERrejects mongodb's two unread timeout keys by name whilemin/maxkeep working (they are honoured, and regression nails pin that they still build tominPoolSize/maxPoolSize). A data table, not a per-armif— the next half-reading arm is one line. Both gates route through the existingunsupportedPoolIssue, 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
DatasourceSchemaand 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 wiresmaxIdleTimeMS(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:
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 fourpool.*verdicts read "still dropped in silence", which is false the moment this lands, and@objectstack/spec check:livenessis 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 thedomain:specseat 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.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 upmemory(datasourcepool声明在 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 datasourcepool声明在 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
- Half 1 —
- added a commit that references this issue
on Aug 17, 2026
发现于 #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原文 —#6345 的验收评论(PR #6910)里没有立这张卡,所以「见 #6345 的 follow-ups」今天指向不存在的东西。本卡补上。
file:url)下 TursoDriver 确实就是 better-sqlite3SqlDriver,与 sqlite 臂同因;remote 模式(libsql://)下没有 knex 连接池、只有concurrency。是「整臂拒绝」还是「按 url 模式分叉」,不是实现方能自己扩的。事实二:
mongodb只读min/max,两个 timeout 键落地无人接mongodb在拒绝集合之外、且确实读pool—— 但只读两个键:pool.idleTimeoutMillis与pool.connectionTimeoutMillis在这一臂不出现在任何位置。二者只在buildSqlPool(:188-198,postgres/mysql)里被读。所以一个 mongo 数据源写
pool: { max: 20, idleTimeoutMillis: 30000 }会得到:maxPoolSize生效、idleTimeoutMillis无声消失。这是同一个块内的半生效,比整块丢弃更难被作者察觉 —— 「我的 pool 配置生效了」有一半是真的。MongoClient 本身有对应旋钮(
maxIdleTimeMS/connectTimeoutMS或serverSelectionTimeoutMS),所以这一处与 sqlite/memory 不同类:不是「没有东西能承接」,而是「有东西能承接但没接」,候选修向里多一个「接上」的选项。仓内标本
turso+pool:未发现声明标本(与 datasourcepool声明在 memory 驱动臂同样被静默丢弃(#5714 的姊妹臂,裁决未覆盖) #5931 里memory的情况相同 —— authoring 面缺陷,不是今天有人在踩的线上问题)。mongodb+ 两个 timeout 键:未发现声明标本。候选修向(不预判,列给分诊)
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()里写明。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),与本卡不同门。