Skip to content

Commit 2c1988c

Browse files
os-helpclaude
andauthored
fix(services,spec): reject the last two silent pool drops — turso whole-arm, mongodb's two timeouts by name (#7243) (#7600)
`datasource.pool` was already a loud authoring error on `sqlite` / `sqlite-wasm` / `memory` (#5714 / #5931). #6214's ledger pass found two faces the rejected set could not cover, both still silent. Measured on origin/main before this change: turso + pool{min:3,max:9,idleTimeoutMillis:30000} the arm never references `spec.pool` at all mongodb + pool{max:20,idleTimeoutMillis:30000,connectionTimeoutMillis:3000} driver config: url + database + maxPoolSize:20, nothing else The mongodb one is half-effective, which is harder to notice than total silence: `max` landed, so the author's evidence that their pool config worked was real and half wrong. Maintainer ruling 2026-08-11, both halves: 1. `turso` joins POOL_UNSUPPORTED_DRIVER_IDS whole-arm, no fork by url mode, with its own explanation rather than SQLite's borrowed one. 2. mongodb's two unread timeout keys are rejected by name, not wired onto MongoClient's maxIdleTimeMS / connectTimeoutMS — no measured consumer, so wiring would be behaviour-surface expansion. The second half is a new shape here: a rejection scoped to individual keys (POOL_UNREAD_KEYS_BY_DRIVER) rather than the whole block, because `min` / `max` on mongodb are honoured and must keep working. It routes through unsupportedPoolIssue, so all three doors — Setup wizard, boot pre-pass, driver factory — inherit it unchanged. Also updates liveness/datasource.json, whose four `pool.*` rows recorded both cases as "still dropped in silence", and the drivers doc's pool paragraph, which named only the two sqlite arms. Claude-Session: https://claude.ai/code/session_015fkdTyGmMD5s8ZtEifvuGy Co-authored-by: Claude <noreply@anthropic.com>
1 parent d063a96 commit 2c1988c

7 files changed

Lines changed: 750 additions & 34 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
---
2+
'@objectstack/service-datasource': patch
3+
'@objectstack/spec': patch
4+
---
5+
6+
datasource `pool`: the last two silent drops are now loud — `turso` whole-arm, and mongodb's two timeout keys by name (#7243)
7+
8+
`datasource.pool` is declared, strict and documented, and #5714 / #5931 already
9+
made it an authoring error on the three arms that cannot honour it
10+
(`sqlite` / `sqlite-wasm` / `memory`). #6214's ledger pass read every remaining
11+
arm and found two faces the rejected set could not cover, both still dropped in
12+
silence. Measured on `origin/main` before this change:
13+
14+
```text
15+
turso + pool{min:3,max:9,idleTimeoutMillis:30000} the arm never references `spec.pool` at all
16+
mongodb + pool{max:20,idleTimeoutMillis:30000,connectionTimeoutMillis:3000}
17+
→ driver config: url + database + maxPoolSize:20, and nothing else
18+
```
19+
20+
The mongodb line is the harder of the two because it is **half**-effective: `max`
21+
took effect, so the author had real evidence their pool config worked, and the
22+
two timeouts vanished anyway.
23+
24+
Maintainer ruling 2026-08-11, both halves:
25+
26+
1. **`turso` joins `POOL_UNSUPPORTED_DRIVER_IDS` whole-arm**, with no fork by url
27+
mode. `TursoDriverConfig` has no `min` / `max`; a `file:` / `:memory:` url runs
28+
the same better-sqlite3 engine the set already rejects for, and a `libsql://`
29+
url is a remote request transport with no persistent connections, capped by
30+
`config.concurrency`. The arm carries its own explanation rather than
31+
borrowing SQLite's, because an author on the remote transport told about
32+
`:memory:` would be reading about somebody else's datasource.
33+
2. **mongodb's two unread timeout keys are rejected by name, not wired.**
34+
`MongoClient` does expose `maxIdleTimeMS` / `connectTimeoutMS`, so this one
35+
could have been implemented; with no measured consumer asking for it, wiring
36+
would be behaviour-surface expansion. Rejection keeps declared = enforced and
37+
tells the author at authoring time. It stays a one-line change on the day real
38+
demand appears.
39+
40+
The second half is a new shape for this module: a rejection scoped to individual
41+
**keys** rather than the whole block, because `min` / `max` on `mongodb` are
42+
honoured and must keep working. It is a data table (`POOL_UNREAD_KEYS_BY_DRIVER`)
43+
rather than a per-arm `if`, so the next arm that half-reads the block is one line
44+
and inherits all three doors — the Setup wizard's create/update, the boot-time
45+
auto-connect pre-pass, and the driver factory's last door.
46+
47+
Both rejections name the datasource, name the offending key(s), say the rejection
48+
is deliberate, and give the one edit that fixes it. Neither offers an escape-hatch
49+
env var (#5794), and the mongodb message says what SURVIVES the edit — telling a
50+
mongo author to "remove `pool`" would delete two keys that do take effect.
51+
52+
Nothing that was honoured changes: `postgres` / `mysql` still receive all four
53+
keys, `mongodb` still maps `min` / `max` onto `minPoolSize` / `maxPoolSize`. New
54+
API surface is `POOL_UNREAD_KEYS_BY_DRIVER` / `unreadPoolKeys` /
55+
`unreadPoolKeysMessage`; `unsupportedPoolIssue` and `assertDatasourcePoolSupported`
56+
keep their signatures and now cover both gates, so an injected host factory that
57+
already calls them inherits this with no change.
58+
59+
`@objectstack/spec` carries the ledger half: `liveness/datasource.json`'s four
60+
`pool.*` rows and their block note recorded both of these as "still dropped in
61+
silence" — the honest record #6214 left, and false the moment this lands. They now
62+
state the new verdicts. No schema, type or runtime behaviour changes in `spec`.

content/docs/data-modeling/drivers.mdx

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,16 +121,33 @@ Two things live **outside** `config`, because they are not driver-specific:
121121

122122
- **Pool sizing** — the `pool` block on the datasource (`min`, `max`,
123123
`idleTimeoutMillis`, `connectionTimeoutMillis`), honoured by the pooled
124-
drivers: `postgres` and `mysql` pass it to Knex, `mongo` maps `min` / `max`
125-
onto the client's `minPoolSize` / `maxPoolSize`. Declaring it on a **sqlite**
126-
or **sqlite-wasm** datasource is rejected — by the Setup wizard when you save,
127-
and by the boot when a declared datasource carries one: a SQLite connection
128-
strategy is owned by the driver (one connection per database, because a second
129-
connection to `:memory:` opens a separate, empty one), so a pool declared
130-
there could never take effect. It used to be dropped in silence — an `app-crm`
131-
datasource asking for `max: 5` measurably ran on one connection
132-
([#5714](https://github.com/objectstack-ai/objectstack/issues/5714)). The fix
133-
is to delete the block; it is a no-op on SQLite either way.
124+
drivers: `postgres` and `mysql` pass all four to Knex, and `mongodb` maps
125+
`min` / `max` — and only those two — onto the client's `minPoolSize` /
126+
`maxPoolSize`. Everywhere else the block is an **authoring error**, rejected by
127+
the Setup wizard when you save and by the boot when a declared datasource
128+
carries one, rather than dropped:
129+
130+
| Driver | Verdict on `pool` |
131+
| --- | --- |
132+
| `postgres`, `mysql` | all four keys honoured |
133+
| `mongodb` | `min` / `max` honoured; `idleTimeoutMillis` and `connectionTimeoutMillis` **rejected by name** |
134+
| `sqlite`, `sqlite-wasm` | whole block **rejected** |
135+
| `memory` | whole block **rejected** |
136+
| `turso` / `libsql` | whole block **rejected** |
137+
138+
Each arm says why in its own terms. A SQLite connection strategy is owned by
139+
the driver (one connection per database, because a second connection to
140+
`:memory:` opens a separate, empty one); `memory` opens no connection at all;
141+
neither libSQL transport pools — a `file:` url runs that same local SQLite
142+
engine and a `libsql://` url is a remote request transport capped by
143+
`config.concurrency`. Every one of these used to be dropped in silence: an
144+
`app-crm` datasource asking for `max: 5` measurably ran on one connection
145+
([#5714](https://github.com/objectstack-ai/objectstack/issues/5714)), and a
146+
mongo datasource asking for `max: 20, idleTimeoutMillis: 30000` got the first
147+
and lost the second without a word
148+
([#7243](https://github.com/objectstack-ai/objectstack/issues/7243)) — the
149+
half-honoured case, which is the hardest to notice. The fix is always to
150+
delete the rejected keys; they were reaching nothing either way.
134151
- **TLS certificates** — the `ssl` block on the datasource (`enabled`,
135152
`rejectUnauthorized`, `ca`, `cert`, `key`). Inside `config`, `ssl` is the
136153
on/off boolean shorthand.

0 commit comments

Comments
 (0)