Skip to content

fix(driver-sql): bound a connection attempt at 10s, correct the "no reconnection" claim (#3769) - #3781

Merged
os-zhuang merged 1 commit into
mainfrom
claude/objectql-driver-connection-error-jwicgq
Jul 28, 2026
Merged

fix(driver-sql): bound a connection attempt at 10s, correct the "no reconnection" claim (#3769)#3781
os-zhuang merged 1 commit into
mainfrom
claude/objectql-driver-connection-error-jwicgq

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

部分关闭 #3769,并订正 #3751 / #3765 里我基于错误前提写下的声明。#3759 已按 not_planned 关闭(前提被实测推翻)。

一、我之前的声明是错的

#3751#3765 里我写了多处"driver 不会重连" —— there is no lazy reconnectionNOT retried and NOT reconnectedstays disconnected for the process lifetime实测下来两个驱动都会自行恢复。

driver-mongodb(真 mongod,杀掉后原端口+原 dbpath 重启,同一个 driver 实例,我们不调用任何 reconnect):

[2] SIGKILL       create FAILED MongoServerSelectionError (3001ms)
[3] 原端口重启
[4] recovery-1    create -> OK (13ms)      ← 第一次尝试就成功

driver-sql(TCP 层判定"池是否被永久毒死"):

while DOWN  : connect ECONNREFUSED
while UP    : Connection terminated unexpectedly   ← 错误随服务端实时状态变化
DOWN again  : connect ECONNREFUSED

每次获取都在开新连接。配置侧印证:storage-driver.tspool.min = 0,不驻留空闲连接。

我当初为什么错了: grep 自己的源码没看到 reconnect 实现,就推断"不会恢复"。但恢复行为在底层客户端库里 —— mongodb 官方 driver 的拓扑监控、knex/tarn 的按需获取。"我们没写这段代码" ≠ "这个行为不会发生"。这是方法论错误,记在 #3759 的关闭说明里。

边界: mongo 那个是真 mongod,结论硬;sql 那个没有真 postgres,证明的是"持续建新连接",不是"一次真实 failover 完整恢复"。

二、启动期 fail-fast 不变 —— 但理由变了

不是"连接再也回不来",而是启动序列不会重跑。错过 init() 的 driver 同时错过了 syncRegisteredSchemas(),所以即使数据库随后恢复,那些对象也可能压根没有表。DEGRADED BOOT banner 现在说的是这个,而不是一句关于死连接的假话。

订正了 4 处:DriverConnectError 消息、banner、resolveAllowDriverConnectFailure() 文档、drivers/self-hosting 两个文档页。

三、底下的真缺陷:连接尝试没有我们自己设的上限

SqlDriver 把配置原样透传给 knex,于是面对接受 TCP 但不完成握手的端点(过载实例、半开防火墙、failover 中途的 LB),每个查询等满 tarn 默认的 30 秒,然后报 Timeout acquiring a connection. The pool is probably full —— 池并没有满,运维会被引去调池子大小而不是查网络。pool.max: 5 下几个这样的查询就把池占满,后续请求开始排队,那时候"pool is full"才变成真的,但那是继发症状。

实测四种配置(真 knex/pg + 黑洞监听器):

today (no bound)              30024ms  Knex: Timeout acquiring a connection…
acquireConnectionTimeout 3s    3003ms  Knex: Timeout acquiring a connection…
pool.createTimeoutMillis 3s    3004ms  Knex: Timeout acquiring a connection…
connectionTimeoutMillis 3s     3006ms  timeout expired                        ← 唯一准确的诊断

SqlDriver 现在默认 pool.createTimeoutMillis = 10s,与 driver-mongodb 既有的 connectTimeoutMS ?? 10_000 对齐,两个驱动在同一点放弃。宿主自己设了就不动它。

没在本 PR 做的: 消息准确性需要 dialect 专属的 connectionTimeoutMillis(上表最后一行),而它会把 connection 从字符串改成对象,导致 serve.ts 启动横幅的 URL 显示退化成 (unknown)。留在 #3769,并已在代码注释里写明,免得下一个人以为已经解决。

验证

  • 新增 sql-driver-connect-bound.test.ts(5 例):默认值生效 / 不覆盖宿主的 min&max / 宿主显式设置被尊重 / 真的限住了对黑洞端点的查询 / sqlite 正常连接不受干扰。
  • 更新 engine-driver-connect-failfast.test.ts 的 banner 断言 —— 从 NOT reconnected 改为 SKIPPED FOR GOOD(持久后果是被跳过的 schema sync,不是死连接)。
  • pnpm build 71/71 绿。受影响的包全绿:types 26、driver-sql 330、objectql 1126、runtime 661、cli 667。

关于本地全量 pnpm test,如实说明: 这台机器上它现在不稳定,干净的 main 也一样(131/132,dogfood 挂)。抓到的唯一真实失败是 cloud-connection 的 2 个用例 30s 超时,而那个文件自己的注释就写着 "cold import can eat several seconds on a fresh CI runner";turbo 随即取消其余在跑任务,所以每次"失败集合"都不同。4 核并行下的竞争,与本改动无关 —— 但这一条我没法在本地完全排除,以 CI 为准。

关联


Generated by Claude Code

…econnection" claim (#3769, #3759)

Two related corrections, both from measuring what #3741/#3751/#3765 asserted.

## The claim was wrong

#3751 and #3765 shipped several statements that drivers never reconnect —
"there is no lazy reconnection", "NOT retried and NOT reconnected", "stays
disconnected for the process lifetime". Measured, both drivers recover:

- driver-mongodb: killing a real mongod and restarting it on the same port, the
  SAME driver instance served the next write successfully (13ms) with no
  reconnect call from us — the official driver's topology monitor handles it.
- driver-sql: a knex/pg pool is not poisoned by an outage. Its error tracks live
  server state (ECONNREFUSED while down → a handshake error once a listener is
  back → ECONNREFUSED again), i.e. every acquire opens a fresh connection.
  storage-driver.ts also sets pool.min = 0, so no stale idle connections.

The original reasoning grepped this repo for `reconnect`, found nothing, and
concluded recovery does not happen — but the recovery lives in the client
libraries, not in our code. "We didn't write it" is not "it doesn't happen".

## Fail-fast at boot is unchanged — the reason is different

It is not that the connection can never return; it is that the BOOT SEQUENCE
never re-runs. A driver that missed init() also missed syncRegisteredSchemas(),
so its tables can simply not exist even after the database comes back. The
DEGRADED BOOT banner now says that instead of claiming a dead connection.

## The real defect underneath

SqlDriver passed its config to knex untouched, so an endpoint that accepts TCP
but never completes the handshake (overloaded instance, half-open firewall, LB
mid-failover) made every query wait out tarn's 30s default, then fail with
"Timeout acquiring a connection. The pool is probably full" — pointing an
operator at pool sizing instead of the network. With a small pool.max a few such
queries saturate the pool and everything else queues behind them.

SqlDriver now defaults pool.createTimeoutMillis to 10s, matching
driver-mongodb's existing `connectTimeoutMS ?? 10_000`. A host that sets its own
createTimeoutMillis is left alone.

Measured (black-holing listener, real knex/pg):

  today (no bound)              30024ms  Knex: Timeout acquiring a connection…
  acquireConnectionTimeout 3s    3003ms  Knex: Timeout acquiring a connection…
  pool.createTimeoutMillis 3s    3004ms  Knex: Timeout acquiring a connection…
  connectionTimeoutMillis 3s     3006ms  timeout expired

Message accuracy needs the dialect-specific knob (last row), which changes the
shape of `connection` and would regress serve.ts's startup banner — left to
#3769.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9VBCNvg9Zixrf1A9Rnwdd
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 4:18am

Request Review

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/objectql, @objectstack/driver-sql, @objectstack/types.

18 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/driver-sql, @objectstack/types)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/driver-sql)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/driver-sql)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 05:20
@os-zhuang
os-zhuang merged commit 32d3800 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/objectql-driver-connection-error-jwicgq branch July 28, 2026 05:20
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…r message (#3769) (#3791)

#3781 bounded a connection attempt at 10s via pool.createTimeoutMillis, which stopped the 30s hang but kept knex's own wording: "Timeout acquiring a connection. The pool is probably full". The pool is not full — the server never completed the handshake — so that message sends an operator to tune pool.max while the network is what is broken.

SqlDriver now also sets the dialect's own connect timeout, which fails with a message that names what happened:

  pg / postgres / postgresql / cockroachdb → connectionTimeoutMillis → "timeout expired"
  mysql / mysql2                           → connectTimeout          → "connect ETIMEDOUT"

Carrying the timeout requires `connection` to be an object, so a URL string moves into the client's URL slot (connectionString for pg, uri for mysql2). Measured against a black-holing listener: both forms still reach the URL's own host/port and still honour ?sslmode=require. SQLite is untouched — a file open has no handshake to time out. A function-valued connection is left alone.

The two bounds are deliberately unequal. They race and knex wins a tie, so equal values let the pool timeout fire first and the accurate message is never seen — caught by the black-hole test, which now asserts the wording rather than merely that something threw. The dialect timeout is the effective bound at 10s; the pool timeout is a strictly looser backstop, 10s → 15s.

driver.config keeps the shape the author passed — the rewrite applies only to what knex receives. serve.ts's startup banner and createDatabase() both depend on that; a test pins it. createDatabase()'s own admin connection now gets the same bound.

Also documents the two defaults in drivers.mdx, which had claimed the constructor takes a Knex.Config "verbatim".
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

Development

Successfully merging this pull request may close these issues.

2 participants