fix(datasource): an explicitly-bound datasource that cannot connect refuses the boot (#3758) - #3816
Merged
os-zhuang merged 1 commit intoJul 28, 2026
Conversation
…efuses the boot (#3758) `DatasourceConnectionService.handleFailure()` fail-fasted only for an `external` datasource with `validation.onMismatch: 'fail'`; everything else degraded to one `warn` line — including the case the D2 auto-connect gate itself identifies as having no fallback path: a datasource that objects bind to explicitly via `object.datasource`. Those objects never fall through to the `default` driver; `engine.getDriver` throws `Datasource 'x' is not registered` for them. An app declaring `datasource: 'analytics'` with 20 objects bound, booted against a wrong `ANALYTICS_URL`, therefore started clean and exited zero, then failed every read and write of those objects with an error that reads nothing like "the analytics database is unreachable" — and the rest of the app kept working, which made it harder to locate than a total outage. Same decision #3741/#3751 fixed one layer up in `ObjectQLEngine.init()`; this boundary was still drawn in the old place. - Fail-fast is keyed on "no fallback path", not on `onMismatch` alone: at the `declared-auto` trigger a connect failure aborts the boot when the datasource is external + `onMismatch:'fail'` OR when >=1 object binds to it explicitly. `autoConnect: true` with nothing bound stays lenient; runtime-admin and rehydration triggers are unchanged and still always degrade. - Every failure mode counts, not just an unreachable socket: an unresolvable `external.credentialsRef` and an unsupported `driver` leave the bound objects exactly as dead. A `DatasourceConnectPolicy` denial is NOT a failure and stays metadata-only — a host refusing egress is deciding, not failing. - The error names the bound objects (up to 10, then `+N more`) alongside the underlying cause; `connectDeclared()` attempts every gated datasource before throwing an aggregate, so one boot reports all of them. - The escape hatch is shared with the engine guard, `OS_ALLOW_DRIVER_CONNECT_FAILURE=1` — one operator intent, one flag. It now covers `onMismatch:'fail'` too, which previously had no opt-out. When set, boot continues and a DEGRADED BOOT banner goes to stderr as well as the logger. `emitDegradedBootBanner` moved to `@objectstack/types` so both call sites share one implementation; `@objectstack/objectql` re-exports it unchanged. ADR-0062 D5 amended with the new criterion, the shared flag, and the policy-denial boundary; deployment + federation docs updated to match. 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): 31 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 08:22
os-zhuang
deleted the
claude/explicit-datasource-connection-fallback-5b83d0
branch
July 28, 2026 08:22
This was referenced Jul 28, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
…when queried (#3827, #3828) (#3836) #3816 made an explicitly-bound datasource that cannot connect refuse the boot. Two gaps survived it, in the cases that still boot — a policy denial, an `autoConnect` datasource, or a failure waved through with OS_ALLOW_DRIVER_CONNECT_FAILURE: the datasource was invisible (`DatasourceSummary.status` was a hardcoded 'unvalidated'; `checkDriversHealth()` cannot see a driver that was never registered), and the query-time error said only "is not registered" for four different situations. Both from one root: `connect()` produced a ConnectResult per attempt and every caller threw it away. - Retain the last verdict per datasource (available / blocked / failed / unattempted); `getConnectionState`, `listConnectionStates`. - `DatasourceSummary.status`: ok | error | blocked | unvalidated, + statusReason. - New DatasourceUnavailableError (ERR_DATASOURCE_UNAVAILABLE, HTTP 503) naming which situation it is. An undeclared name keeps the original message. - Privileged/public split: the error never carries the underlying cause; DatasourceConnectDecision gains an opt-in `publicReason` for tenant-facing text. - Readiness stays ungated on this. Also lands the #3826 drift guard (degraded-boot-parity.test.ts) and corrects ADR-0062's status: D1 is partially implemented, not complete.
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 #3758.
问题
DatasourceConnectionService.handleFailure()的 fail-fast 只覆盖「external +validation.onMismatch: 'fail'」,其余一律降级成一条warn——包括同文件 D2 gate 自己注释里点名「没有任何回落路径」的那一类:被object.datasource显式绑定的 datasource。这些对象不会落到defaultdriver,engine.getDriver对它们直接抛Datasource 'x' is not registered。于是一个声明
datasource: 'analytics'并绑了 20 个对象的 app,在ANALYTICS_URL配错时:进程正常启动、退出码为 0,然后那 20 个对象的每一次读写都失败,报的错跟「analytics 库连不上」相距很远;而其余部分工作正常,反而比整体宕机更难定位。这跟 #3741 / #3751 在ObjectQLEngine.init()那一层修掉的是同一个决定,只是这一层的边界还画在旧位置。改动
判据从
onMismatch换成「有没有回落路径」。declared-auto(启动)触发下,连接失败在两种情况下终止启动:external+validation.onMismatch: 'fail'object.datasource显式绑定autoConnect: true且无对象绑定runtime-admin创建/更新与启动期 rehydration 行为不变,始终降级:一次 UI 操作不能弄挂正在跑的服务器。覆盖全部失败原因,而不只是连不上。 无法解析的
external.credentialsRef(D3)和不受支持的driver,对绑定对象来说一样是死的,走同一判决。DatasourceConnectPolicy拒绝不算失败,仍然停在 metadata-only —— 多租户 host 按套餐挡 egress 是在「决定」,不是在「失败」,把它变成 fail-fast 会让共享 runtime 上所有租户一起起不来。错误信息直接列出绑定对象(最多 10 个,其余记
+N more),连同底层原因一起给出。connectDeclared()会先尝试完所有通过 gate 的 datasource 再抛聚合错误,一次启动报全部问题,而不是修一个重启一次(与ObjectQLEngine.init()的DriverConnectError同形)。逃生阀与引擎层共用
OS_ALLOW_DRIVER_CONNECT_FAILURE=1—— 操作者意图相同,两个旗标只会保证有一个被漏设。它现在也覆盖 (a),而 (a) 此前完全没有 opt-out。设置后继续启动,并把DEGRADED BOOTbanner 同时写到 stderr(os serve的 boot-quiet 会吞掉 stdout)。emitDegradedBootBanner移到@objectstack/types,两个调用点共用一份实现;@objectstack/objectql原样再导出,公开 API 不变。文档
content/docs/data-modeling/external-datasources.mdx新增「When auto-connect fails」小节;drivers.mdx/environment-variables.mdx/production-readiness.mdx同步旗标覆盖范围。examples/app-showcase的showcase_external注释原本声称onMismatch: 'warn'能挡住 boot 被弄挂——那句话现在只对 schema drift 成立,已改写为准确表述。验证
packages/services/service-datasource— 106 passed(新增 13 个用例:managed + 绑定对象 fail-fast、消息内容与截断、credential/unsupported-driver 同判决、autoConnect与 runtime-admin 保持宽容、旗标 opt-in、stderr banner、falsy 值仍 fail-fast)packages/runtime— 663 passed(新增 2 个端到端用例:kernel.bootstrap()因绑定 datasource 连不上而拒绝启动并点名依赖对象;设了旗标则降级启动)packages/objectql— 1128 passed;packages/types— 26 passedpnpm build全量通过;service-datasourcetypecheck 通过Migration
配置正确的部署无需改动。此前静默带着一个已死的、被显式绑定的 datasource 启动的部署,现在会启动失败,并给出 datasource、原因和依赖它的对象;修配置即可。若确实要在明知每个相关请求都会失败的前提下继续启动,设
OS_ALLOW_DRIVER_CONNECT_FAILURE=1。Generated by Claude Code