feat(datasource): a datasource that is down is visible, and says why when queried (#3827, #3828) - #3836
Merged
os-zhuang merged 1 commit intoJul 28, 2026
Conversation
…when queried (#3827, #3828) #3816 made an explicitly-bound datasource that cannot connect refuse the boot. Two gaps survived it, both in the cases that still boot — a policy denial, an `autoConnect` datasource, or any failure waved through with OS_ALLOW_DRIVER_CONNECT_FAILURE: * It was invisible. `DatasourceSummary.status` was the literal 'unvalidated' for every row — the contract declared three states, the implementation ever emitted one — so a dead datasource looked exactly like a healthy-untested one. `checkDriversHealth()` could not help either: it iterates REGISTERED drivers, and one that never connected was never registered, so it is absent from the probe rather than unhealthy. The diagnostic procedure was "restart the server and re-read the boot logs". * The query-time error said nothing. `getDriver()` answered four situations with one sentence — refused by policy, failed under the escape hatch, a misspelled name, `active: false`. Only the third is an authoring bug, so the others sent the reader hunting for a typo that does not exist. Both come from one root: `connect()` produced a `ConnectResult` per attempt and every caller threw it away. - Retain the last verdict per datasource, with a coarse `availability` (available / blocked / failed / unattempted). `getConnectionState`, `listConnectionStates`; `disconnect()` drops it so a removed pool stops explaining itself. - `DatasourceSummary.status` tells the truth: ok | error | blocked | unvalidated, plus an operator-facing `statusReason`. `blocked` is new and deliberate — a policy denial is a decision, not a fault. Surfaced in the admin list and in the summary returned from create/update, so a "Save" whose pool failed to open is no longer presented as success. - New `DatasourceUnavailableError` (`ERR_DATASOURCE_UNAVAILABLE`, HTTP 503) from `@objectstack/objectql`, thrown when the connection layer recorded WHY a declared datasource has no driver. An undeclared name keeps the original message. 503 over 400/500: nothing about the request is wrong, and it may clear. - Privileged/public split for the reason. The error NEVER carries the underlying cause — connect failures contain hosts, ports and DSNs; a policy's `reason` is written for operators. Those stay in logs and the admin-gated list. `DatasourceConnectDecision` gains an opt-in `publicReason`, the only string that reaches an end user. - Readiness stays ungated on this: an optional datasource being down must not pull an otherwise-working replica out of the load balancer. Also lands the #3826 drift guard and corrects ADR-0062's status. The ADR claimed D1 implemented; only the construction half converged — `default` is still registered as a `driver.*` kernel service and connected by `ObjectQLEngine.init()`, with its own failure verdict, pool teardown and no connect policy. The blocker is an input-shape mismatch, not ordering: `connect()` takes a definition and BUILDS the driver, while `default` arrives pre-built, and routing it through the service would make ObjectQLPlugin's boot depend on an optional higher-layer service. Until that is designed, `degraded-boot-parity.test.ts` pins both paths to the same operator-visible contract so a change to one that forgets the other fails CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TQVM3A9Yd6N2eZS8ZcdnMk
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 34 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 28, 2026 09:16
os-zhuang
deleted the
claude/explicit-datasource-connection-fallback-5b83d0
branch
July 28, 2026 09:16
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 #3827. Closes #3828. Partially addresses #3826 (see below — #3826 停留在 open)。
问题
#3816 让「被显式绑定的 datasource 连不上」拒绝启动。但仍然能起来的那几类 —— policy 拒绝、
autoConnect、以及操作者用OS_ALLOW_DRIVER_CONNECT_FAILURE放行的失败 —— 留下了两个洞:DatasourceSummary.status对每一行都填死'unvalidated'(契约声明三态,实现只发一态),所以一个死掉的 datasource 和一个健康但没测过的完全无法区分。checkDriversHealth()也帮不上:它遍历已注册的 driver,而从未连上的 datasource 从未被注册,所以它是不出现而不是「报 unhealthy」。排查手段只剩「重启服务器再读日志」。getDriver()用同一句话回答四种情况:policy 拒绝、逃生阀放行的连接失败、名字拼错、active: false。只有第三种是作者的 bug,另外三种把人引去找一个不存在的拼写错误。两者同源:
connect()每次都产出了ConnectResult,而每个调用方都把它扔了。改动
availability(available/blocked/failed/unattempted)。新增getConnectionState()/listConnectionStates();disconnect()会清掉,免得一个已删除的 pool 还在解释自己。DatasourceSummary.status说真话:ok|error|blocked|unvalidated,外加面向运维的statusReason。blocked是新增且刻意区分的 —— policy 拒绝是「决定」不是「故障」,不会自己好。除列表外,create/update 返回的 summary 也走同一路径,所以一次 pool 没开成的「保存」不再被呈现为成功。ERR_DATASOURCE_UNAVAILABLE(HTTP 503):新的DatasourceUnavailableError,在连接层记录过原因时抛出;没记录过的名字保持原句(那确实就是拼错了)。选 503 而非 400/500:请求本身没问题,而且这个状态可能自行恢复。两个待定项,按保守解落地
你之前让我继续,我按下面两条推进:
①
reason的可见性 —— 默认不外露,opt-in 才外露。 查询期错误绝不携带底层 cause:连接失败的文本里routinely 有 host / port / DSN,policy 的reason是写给运维的。两者都留在日志和 admin 列表(本来就有权限门)。DatasourceConnectDecision新增 opt-in 的publicReason,是唯一会到达终端用户的字符串:纯增量,cloud 侧不改也能编译、行为不变(不给
publicReason就只说类别)。② 503,用
code识别。 走rest-server.ts既有的mapDataError分支,放在 catch-all 之前,不然会退化成通用 400。另外一个我自己做的判断:readiness 不受影响。
/ready仍只反映已注册 driver 的健康度。autoConnect语义是「能连就连」,把它变成整机下线开关会让一个可选副本拖垮整个 replica。可见性由 admin 列表和查询期报错承担。#3826:只完成了守卫部分,issue 保持 open
核对代码后,我原来的说法需要修正:构造那一半已经收敛,没收敛的是 connect + 失败判决。真正的阻塞点也不是启动顺序(kernel 的 init-all→start-all 已经保证连接服务在
ObjectQLPlugin.start()时存在),而是:connect()接收的是 datasource 定义并构建 driver,而default是以已构建实例的形式作为driver.*kernel service 到达的 —— 没有 "adopt 一个现成 driver" 的入口;default走连接服务,会使ObjectQLPlugin的启动依赖一个上层的可选 service(service-datasource),为每个 app 都需要的那一个 driver 反转分层。这是设计决策,不是机械搬迁,而且 ADR §Risk 标注它是风险最高的一步 —— 我不打算在这个 PR 里草率做掉。已把结论写进 ADR-0062(顺带把 header 里 "D1–D8 implemented" 更正为 D1 partially),并落一个真正有用的东西:
packages/runtime/src/degraded-boot-parity.test.ts,把两条路径钉在同一份运维可见契约上(默认 fail-fast、OS_ALLOW_DRIVER_CONNECT_FAILURE解析完全一致、DEGRADED BOOT上 stderr)。#3741 → #3758 就是漏改一边,代价是三个月和第二份 bug report;下次由 CI 来发现。验证
service-datasource— 120 passed(+54:判决留存、privileged/public 分离、publicReason不跨次泄漏、fail-fast 抛出时仍记录、disconnect 清理、status 映射四态)objectql— 1144 passed(+16:四种情况的报错分流、不含 cause 的断言、checkDriversHealth结构性看不见未注册 datasource)rest— 410 passed(+2:503 映射、不退化成 400)runtime— 693 passed(+30:parity 守卫 12 例;端到端 policy 拒绝后查询报错含publicReason、不含运维 reason;admin 列表显示blocked+statusReason)pnpm build全量通过;service-datasourcetypecheck、examples typecheck、ESLint、changeset 门禁本地全绿顺带修正了
DEGRADED BOOTbanner —— 它原本声称查询会报"Datasource 'x' is not registered",这话在本 PR 之后不再成立。Migration
增量为主。
DatasourceSummary.status多了'blocked',穷举 switch 的消费方需要补一个分支;原本'ok'/'error'的含义不变,原本被报成'unvalidated'的行现在报真实状态。连接层记录过的 datasource,查询期错误从裸Error变为DatasourceUnavailableError(状态码从原先的兜底值变 503);按旧文案is not registered匹配的代码,在「名字未声明」这唯一一种它本来就准确的情况下仍然有效。Generated by Claude Code