feat(runtime): /ready reports 503 when a data driver stops answering (#3756) - #3765
Merged
Merged
Conversation
…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
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…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
marked this pull request as ready for review
July 28, 2026 02:48
This was referenced Jul 28, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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-admin的testConnection消费;没有任何探针路径调用它,而且ObjectQL的drivers是 private、没有 getter —— 就算探针想问也没地方问。改动
ObjectQL.checkDriversHealth({ timeoutMs })—— 新的公开聚合健康检查,返回DriverHealth[]。两个设计点: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 启动 ——连续 5 次快速探测稳定在 503 / 9-11ms。两个测试服务器均已自行关闭。
文档
deployment/self-hosting.mdx的探针表补上了两者的语义差异(为什么/health不该查数据库、/ready该查),并加了一条 Callout 指向 #3759:driver 不会自己重连,所以 k8s 场景靠 readiness 摘副本 + 重启策略兜底,无编排器的部署需要自己配一个基于/api/v1/ready的 supervisor。一并开的相邻 issue
object.datasource绑定的 datasource 连不上只降级成 warning(DatasourceConnectionService层的同一形状,需要同步改 ADR-0062 D5,没有夹带进这个 PR)Generated by Claude Code