Skip to content

finding(service-automation): engine.ts 的 loadSuspendedRun 把驱动错误插进 warn 的 message —— 与 #5912 同文件、同一次失败的另一半,且这条会被 boot 缓冲丢弃 #6230

Description

@hotlong

做 #5912(PR #6228)时在同一个文件扫到的旁生发现,但在另一个方法、另一条分支上,且 #5912 的分诊评论(2026-08-06 16:57Z)已把那一单范围明确钉死在 :3017 一处、⛔ 不扩面,因此不在那一单范围内,按 Prime Directive #10 / objectstack#4949 单开。

现象

packages/services/service-automation/src/engine.ts:2931(origin/main 1549605f6;⚠️ 行号会漂,以内容定位),loadSuspendedRun —— 也就是 loadSuspendedRunStrict 的降级版读取器:

private async loadSuspendedRun(runId: string): Promise<SuspendedRun | null> {
    try {
        return await this.loadSuspendedRunStrict(runId);
    } catch (err) {
        this.logger.warn(
            `[automation] failed to load suspended run '${runId}' from durable store: ${(err as Error).message}`,
        );
        return null;
    }
}

(err as Error).message 来自 loadSuspendedRunStrict 底下的数据源驱动,和 #5912 是同一个 thrown 值 —— 我们不控制它有几行。它被插进 logger.warn 的 message。

与 #5912 的关系:同一次失败的另一半

两者读的是同一个 loadSuspendedRunStrict。一次「resume 时存储不可达」会同时走这两条:

所以 PR #6228 落地后,这条路径上 stderr 那条已经干净,stdout 这条仍会被换行切碎。PR #6228 的测试正是靠「只抓 stderr」把待验接缝与这条 warn 隔开的(见该 PR 测试文件里 resumeAgainstUnreadableStore 的注释)。

危害机制:比 #5912 那条更重一档

同族基础危害相同 —— ObjectLogger.write() 一次调用只加一个「时间戳 + 级别」记录头,message 里的换行把一条记录变成多个物理行,后几行无级别无时间戳,grep WARN 只捞到不含事实的那一行。

但多一条:ObjectLogger 把 debug / info / warn 路由到 stdout,而 serve 的 boot-quiet 窗口只包了 process.stdout.write,其 BootLogCapture.offer() 仅在 classifyBootLogLine 找得到级别头时才保留该物理行。这条是 warn ⇒ 落在那个缓冲的过滤面上,无头续行是被直接丢弃而不只是被误读。

而且它在 boot 期真实可达:plugin.ts 的 start()(:939)调 rearmSuspendedWaitTimers,后者对 overdue 运行调 engine.resume(run.runId)(builtin/wait-node.ts:378),resume() 的 gate 就走到这个降级版读取器。

对照:#5912 那条是 error 走 stderr,不经该缓冲,危害是被误读;这条是被误读 + boot 期被丢弃。这正是 thrown-cause-diagnostics.ts 模块 docblock 里 warn / error 下游不同的那段所描述的形态,也是 #5661 第 2、3 条与 cloud#971 的形态。

级别本身是对的,不要顺手改

warn 在这里是正确的:这是一个刻意的降级读取器,注释写明它服务于「只需要 best-effort 答案」的顺带读取方(gate 查询、screen 取数),真正需要区分「存储挂了」与「运行没了」的 resumeInternal 用的是严格版。按 #4632 的判据,这是功能性降级而非耐久性降级,不应上调到 error —— 上调才是 #4632 明确警告的镜像错误。本单只谈 message 拼接。

可达性(为什么是 finding 而不是事故报告)

与 #5575 / #5636 / #5661 / #5737 / #5912 同样的理由:今天库内的驱动错误均为单行,所以这是 finding。第一个包装多行 SDK 错误的驱动撞上 —— 数据库驱动的多行错误(Postgres 的 detail: / hint: 续行、better-sqlite3 包装器)在生态里很常见,#5737 与 #5912 的实测都是照这个形状造的。

修法(同族既定模板,零新词汇)

与前六次完全一致,复用同包 thrown-cause-diagnostics.ts 的 describeThrownForLog:message 保持单行自足,cause 走 meta。按 Logger 契约(packages/spec/src/contracts/logger.ts)warn(message, meta?) 是第二参(不是 error 的第三参 —— warn 没有 Error 槽)。新增字段名不得含 key / token / secret / password 子串(#5573);直接用 helper 自己的 error / issues 即可,无需新字段。

顺带值得判断(不是阻塞项):message 里可否同时补上这条降级的后果(返回 null,调用方会当作「没有这个挂起运行」)—— 目前文本只说「读失败」,没说读失败被翻译成了什么。倾向补,但属于同一处改动的自然范围。

关联

#5912 / PR #6228(本发现的来源,同文件另一方法)、#5737 / PR #5911、#5661、#5636 / PR #5662、#5575 / PR #5639、#5048 / PR #5572、#5660(同在 engine.ts 的 registerDegradedConnector,已闭)、#5573、#4632、#4420、cloud#971。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions