Skip to content

feat(runtime): /ready reports 503 when a data driver stops answering (#3756) - #3765

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

feat(runtime): /ready reports 503 when a data driver stops answering (#3756)#3765
os-zhuang merged 2 commits into
mainfrom
claude/objectql-driver-connection-error-jwicgq

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #3756#3741 / #3751 修的是"启动期连不上",这个 PR 修的是运行期掉线 —— 同一个病的另一面。

问题

/health 恒返回 {status:'ok'}/ready 只看 kernel state 是不是 running,而那个标志在 bootstrap 完成的一刻置位、此后不再复查。两个探针都不碰数据层。

数据库在启动之后不可用(重启、failover、网络策略变更、连接池耗尽、凭据轮换)→ 两个探针照样绿 → LB 继续往这个副本打流量,而它每个请求都 500,编排器也看不出任何异常。

driver 的 checkHealth() 早就存在且很便宜(SELECT 1 / db.command({ping:1})),但只被 datasource-admintestConnection 消费;没有任何探针路径调用它,而且 ObjectQLdrivers 是 private、没有 getter —— 就算探针想问也没地方问。

改动

ObjectQL.checkDriversHealth({ timeoutMs }) —— 新的公开聚合健康检查,返回 DriverHealth[]。两个设计点:

  • 每个探测独立结算且有超时上限(默认 2s)。 checkHealth() 自己吞异常返回 false,但在一个死掉的连接池上它根本不返回 —— knex 的 SELECT 1 会一直等到 acquireConnectionTimeout(默认 60s)。一个挂住的探针和一个撒谎的探针一样没用。
  • 没实现 checkHealth() 的 driver 判为健康。 没有探针 ≠ 有故障;因为一个可选方法从来没实现过就把整个副本摘掉是错的。

GET /ready —— kernel 在跑但有 driver 挂了时返回 503,并列出是哪个 driver。结果 memo 约 1 秒:k8s 默认几秒探测一次 × 每副本,不能每次都打一个 DB 往返。

GET /health 保持什么都不检查,并在代码里写明了为什么 —— 这是我认为这个 PR 最容易被后人改错的地方:liveness 失败 → 编排器重启 pod,而重启修不好宕掉的数据库,只会让所有副本在整个故障窗口里陷入重启风暴,还顺带杀掉本可以正常完成的 in-flight 请求。摘流量(readiness)才是有用的那个失败模式。

readiness 检查 fail-open。 没有 data engine(lite kernel、edge、metadata-only host)、engine 版本旧没有这个方法、或者探测本身抛错 —— 一律判为 ready,跟这个检查存在之前完全一致。readiness 决定这个副本收不收流量,一个不确定的答案绝不能把一个正常的部署整个黑洞掉。只有 driver 明确报告自己不健康才摘副本。

验证

  • packages/objectql/src/engine-driver-health.test.ts(7 例):全健康 / 返回 false / 抛错 / 挂住的探测被超时掐掉而不是干等 / 一个挂住的 driver 不会掩盖另一个健康的 / 无 checkHealth() 判健康 / 空 driver 集。

  • packages/runtime/src/http-dispatcher.ready.test.ts(+6 例):driver 全健康→200;driver 挂了→503 且 body 里列出 driver 名;memo 生效(连续 3 次探测只调用 1 次 checkDriversHealth);探测抛错→仍 200;旧 engine→仍 200;以及 /health 在 driver 挂掉时保持 200 且完全没调用 checkDriversHealth

  • pnpm build + pnpm test 全绿(132/132 tasks)。

  • 端到端:用 feat(objectql)!: refuse to boot when a data driver fails to connect (#3741) #3751 的逃生阀 OS_ALLOW_DRIVER_CONNECT_FAILURE=1 让 CRM example 带着一个连不上的 postgres 启动 ——

    health = 200
    ready  = 503  (12ms)
    {"success":false,"error":{"message":"Data driver unavailable","code":503,
     "details":{"state":"running","drivers":["com.objectstack.driver.sql"]}}}
    

    连续 5 次快速探测稳定在 503 / 9-11ms。两个测试服务器均已自行关闭。

文档

deployment/self-hosting.mdx 的探针表补上了两者的语义差异(为什么 /health 不该查数据库、/ready 该查),并加了一条 Callout 指向 #3759:driver 不会自己重连,所以 k8s 场景靠 readiness 摘副本 + 重启策略兜底,无编排器的部署需要自己配一个基于 /api/v1/ready 的 supervisor。

一并开的相邻 issue


Generated by Claude Code

…3756)

`/health` returned `{status:'ok'}` unconditionally and `/ready` only checked
whether the kernel state was `running` — a flag set once when bootstrap finishes
and never revisited. Neither probe touched the data layer. A database that went
away AFTER boot (restart, failover, network policy change, pool exhausted,
credentials rotated) left both green: the load balancer kept routing to a replica
failing 100% of its requests, and the orchestrator saw nothing wrong. The
driver's `checkHealth()` already existed and was cheap (`SELECT 1` /
`db.command({ping:1})`) but was only consumed by datasource-admin's
`testConnection` — no probe path called it, and `ObjectQL` exposed no way to ask
(`drivers` is private, no accessor).

This is the runtime-side half of #3741, which fixed only the boot-time version.

- New `ObjectQL.checkDriversHealth({ timeoutMs })` returns a `DriverHealth[]`
  verdict. Probes settle independently and are bounded (default 2s):
  `checkHealth()` swallows its own errors, but on a dead knex pool it does not
  return at all, waiting out `acquireConnectionTimeout` (60s default) — a probe
  that hangs is as useless as one that lies. A driver with no `checkHealth()` is
  reported healthy; absence of a probe is not evidence of failure.
- `GET /ready` returns 503 naming the failing drivers when the kernel runs but a
  driver is down, memoized ~1s so k8s' few-second polling is not one DB
  round-trip per probe per replica.
- `GET /health` deliberately still checks nothing, and now says why: a failing
  liveness probe restarts the pod, which cannot fix an unreachable database but
  would put every replica into a restart storm for the length of the outage.

The readiness check fails OPEN — no data engine, an older engine, or a throwing
probe all read as ready, exactly as before. Readiness gates whether a replica
gets any traffic, so an inconclusive answer must not black-hole a working
deployment; only a positive unhealthy verdict takes the replica out.

Verified end-to-end: booting the CRM example against an unreachable postgres via
OS_ALLOW_DRIVER_CONNECT_FAILURE=1 gives `/health` 200 and `/ready` 503 with
`drivers: ["com.objectstack.driver.sql"]`, stable across rapid repeat probes.

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 2:44am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/runtime.

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

  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • 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, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/index.mdx (via @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/objectql, @objectstack/runtime)
  • 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.

…berately does not

The drift check flagged protocol/kernel/lifecycle.mdx — its endpoint table said
/ready means 'kernel is running and ready to accept requests', which #3765 makes
incomplete. Records the liveness-vs-readiness split and the fail-open rule, and
notes that PluginHealthMonitor covers plugins rather than driver connections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T9VBCNvg9Zixrf1A9Rnwdd
@os-zhuang
os-zhuang marked this pull request as ready for review July 28, 2026 02:48
@os-zhuang
os-zhuang merged commit 8e08bc3 into main Jul 28, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/objectql-driver-connection-error-jwicgq branch July 28, 2026 03:00
os-zhuang added a commit that referenced this pull request Jul 28, 2026
…econnection" claim (#3769) (#3781)

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

**The claim was wrong.** #3751 and #3765 shipped several statements that drivers never reconnect. Measured, both recover: killing a real mongod and restarting it on the same port, the SAME driver instance served the next write in 13ms with no reconnect call from us (the official driver's topology monitor); and a knex/pg pool is not poisoned by an outage — its error tracks live server state, i.e. every acquire opens a fresh connection. 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.

**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.

**The real defect underneath.** SqlDriver passed its config to knex untouched, so an endpoint that accepts TCP but never completes the handshake 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. SqlDriver now defaults pool.createTimeoutMillis to 10s, matching driver-mongodb's existing connectTimeoutMS. A host that sets its own value is left alone.

Message accuracy needs the dialect-specific knob (pg's connectionTimeoutMillis), which changes the shape of `connection` and would regress serve.ts's startup banner — left open in #3769.
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.

/ready 探针看不见数据库:driver 运行期掉线,k8s 不摘流量也不重启,而每个请求 500

2 participants