Skip to content

ETLPipeline.retry is a third retry-policy vocabulary that #4661 的收敛没有覆盖到 #4962

Description

@xuyushun441-sys

发现于 #4001 批 12(automation/etl.zod.ts 的 strict 收紧),不在该 PR 范围内,按 Prime Directive #10 单独记录。

事实

#4661 把重试策略收敛成一份声明shared/retry-policy.zod.ts,因为 automation/control-flow.zod.tssystem/job.zod.ts同一个导出名RetryPolicy 发布了两个不同的形状(#4411 陷阱)。收敛后的词表是:

maxRetries // 初次尝试之后的重试次数,default 0(#4661 起 opt-in)
backoffMs // 首次重试前的基础延迟
backoffMultiplier // 指数退避倍数
maxRetryDelayMs // 单次退避延迟上限
jitter // 抖动
retryDelayMs // 墓碑(retiredKey)→ backoffMs

ETLPipelineSchema.retry(packages/spec/src/automation/etl.zod.ts)是同一个概念的第三份编码,而 #4661 没有碰它:

retry: z.object({maxAttempts: z.number().int().min(0).default(3).describe('Max retry attempts'),backoffMs: z.number().int().min(0).default(60000).describe('Backoff in milliseconds'),}).optional()

三处分歧,都是真实的:

  1. 次数键拼写不同 —— maxAttempts vs 收敛后的 maxRetries。语义相同(初次之后的重试数),只有拼写不同。
  2. 默认值方向相反 —— 这里 default(3),收敛后的策略 default(0)spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 的理由写得很明确:重试会重放上一次尝试已经产生的副作用,所以"不写 = 不重试"必须是默认,隐式重试是最难在测试里抓到、在生产里最贵的失败模式,而 LLM 写的 metadata 恰恰是"没写出来的键"藏身的地方。ETL pipeline 的 retry 目前正好是被 spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 判为错误的那个方向。
  3. 少三个键 —— 没有 backoffMultiplier / maxRetryDelayMs / jitter,所以 ETL 的退避是平的、无上限的、无抖动的。一条每天凌晨 2 点跑、失败后固定 60s 重试 3 次的仓库管道,正是 thundering-herd 的典型形状。

之所以 #4661 漏掉它:那次收敛是按导出名冲突(#4411 / #4535 C8)驱动的,而 ETL 的 retry 是一个匿名内联 block,没有导出名,所以不在那次的雷达上。这是同一类债务的另一种外观 —— 一个概念三种词表 —— 只是不表现为 dual-source。

为什么现在只是记录,不是顺手改

etl.zod.ts 在本仓库/objectui/cloud 三仓零 importer、零 parse 点(与 #4738 删掉 L1 sync.zod.ts 时测到的形状相同,区别在于 ETL 不是任何东西的第二份声明,是 SYNC_ARCHITECTURE.md 的 L2 唯一一层,sync-retirement.test.ts 还显式 pin 了 ETLPipelineSchema 必须存活)。所以这里没有"运行时行为"可谈,改动纯粹是可授权契约的形状问题 —— 而契约形状是 needs-decision 级别的取舍,不该塞进一个 strictness 批次里顺手做掉。

批 12 做的是把这个分歧变吵而不是变对:retry block 收紧成 strict 之后,maxRetries 走 alias 提示改写成 maxAttempts,retryDelayMs / backoffMultiplier / maxRetryDelayMs / jitter 各自带一条 guidance,明说"这是 shared/retry-policy.zod.ts 上有、ETL 上故意没有的键,不是笔误"。在本 issue 有结论之前,那四条 guidance 就是这份分歧的可见形式。

三个选项

A. 让 ETLPipeline.retry 直接引用 shared/RetryPolicySchema

  • 长期正确性:最好。一个概念一份声明,spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 已经付过这笔收敛的代价并写下了理由;ETL 是唯一没跟上的那个。shared/retry-policy.zod.ts 的 module JSDoc 里"不进 shared/index.ts、由各 domain barrel 再导出以保住 def key"的做法现成可用。
  • 让 AI 写的 metadata 不易出错:最好。作者在 job / try_catch / ETL 三个地方看到同一套键和同一个默认语义,不再需要记住"哪个面上叫什么"。
  • 成本:maxAttemptsmaxRetries 是 breaking 的可授权键改名,需要 ADR-0087 D2 conversion + 墓碑 + changeset 迁移文案;默认值 3 → 0 是行为改变,但此处零 parse 点,所以没有已部署的 stack 会变(这一点和 spec 双源清账 C8:RetryPolicy / RetryPolicySchema(./automation ≠ ./system)—— 2 条 #4661 当时必须写 retry-policy-converged conversion 的处境不同,便宜得多)。
  • 副作用:automation/ETLPipeline:retryauthorable-surface.json 的展开会变宽(2 键 → 5 键)。

B. 只统一拼写,不引用共享声明(maxAttemptsmaxRetries,默认值改 0,保持内联两键)。

  • 长期正确性:差。这是"看起来收敛了"的补丁 —— 词表对上了,声明还是三份,下一次给重试策略加键时 ETL 依然会被漏掉,和这次被漏掉的机制一模一样。付了 breaking 改名的代价却没买到"一份声明"这个唯一值钱的东西。
  • 让 AI 写的 metadata 不易出错:中等。拼写一致确实有帮助,但 backoffMultiplier / jitter 在别处能写、在这里不能写的坑原样保留。

C. 什么都不做,保留批 12 的 guidance。

  • 长期正确性:差,但是诚实的差。分歧被写进了拒绝信息里,不再是静默的;债务可见、可检索、可排期。
  • 让 AI 写的 metadata 不易出错:中等偏下。作者会拿到明确的改写指示,但只在他已经写错之后;结构上并没有阻止他写错。
  • 成本:零。

建议

A,理由在两个轴上一致:它是 #4661 已经论证过的那个方向(一个概念一份声明),而且此处零 parse 点意味着 A 的迁移成本在整个仓库里此刻最低 —— 越晚做越贵,因为一旦真有 ETL 引擎落地,默认值 3 → 0 就从"改一份 schema"变成"改所有已部署管道的行为"。B 花掉了 A 的全部 breaking 预算却留下了 A 要解决的问题,不推荐。

需要维护者裁决的是 A 里的默认值:maxRetries 跟随收敛后的 0(与 #4661 的 opt-in 论证一致),还是为 ETL 保留 3(与现状一致但重新引入"隐式重试")。我的读法是跟随 0 —— #4661 的论证不依赖于是哪个 domain。

/cc #4001#4661#4535

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions