diff --git a/docs/SKILL-INDEX.md b/docs/SKILL-INDEX.md index 6a90166..f49afa2 100644 --- a/docs/SKILL-INDEX.md +++ b/docs/SKILL-INDEX.md @@ -31,6 +31,7 @@ - [`addon-test-acceptance-and-first-blocker-guide.md`](addon-test-acceptance-and-first-blocker-guide.md) — 成功语义分层、first blocker 分层、validation-only gate 身份固定、现场冻结 - [`addon-test-probe-classification-guide.md`](addon-test-probe-classification-guide.md) — 探针失败分到 `route_api` / `_` / `empty_output` / `parse_empty` / `runtime_mismatch` / `real_*_mismatch` 等正确层 +- [`addon-test-dg-helper-completeness-guide.md`](addon-test-dg-helper-completeness-guide.md) — 多步骤异步操作的 test helper 必须用 multi-gate 和 unfakeable observable invariant,避免单一状态字符串在中间态提前返回成功 - [`addon-bounded-eventual-convergence-guide.md`](addon-bounded-eventual-convergence-guide.md) — 异步收敛系统的状态判定必须 bounded retry,禁止单次 snapshot 当结论 *(also relevant in: 设计 / 开发新 addon — addon 启动 / rejoin / reconfigure 后的判定面)* - [`addon-evidence-discipline-guide.md`](addon-evidence-discipline-guide.md) — 对自己产出的结论也做 bounded retry:N=1→"average" / 间接旁证→"系统性证伪" / 动机假设→narrative inflation 三类反模式 - [`addon-design-contract-review-during-xp-guide.md`](addon-design-contract-review-during-xp-guide.md) — XP 模式 review 阶段的 "design-contract challenge" checklist:8 类常见设计契约级缺陷(静默 fallback / 非空字段未强制 / 同 commit state 不连续 / sentinel 值传错误 / 条件清理状态枚举不穷尽 / NotFound 短路写入 / terminating vs absent 不区分 / 运算符优先级陷阱),每类含 review 模式 + 修法 @@ -58,6 +59,7 @@ 写测试基础设施 / shell helper / 跨平台 portability: - [`addon-test-runner-portability-guide.md`](addon-test-runner-portability-guide.md) — macOS bash 3.2 + `set -euo pipefail` 下 7 个常见兼容坑(空数组、env-default 时机、`local x=$(cmd)` 等) +- [`addon-test-runner-cadence-discipline-guide.md`](addon-test-runner-cadence-discipline-guide.md) — 长时 runner 操作期间的固定节奏汇报纪律;cadence 是操作者义务,触发器必须独立于 runner 进程 ### 跨场景方法论(reusable framework) diff --git a/docs/addon-test-dg-helper-completeness-guide.md b/docs/addon-test-dg-helper-completeness-guide.md new file mode 100644 index 0000000..15d91d7 --- /dev/null +++ b/docs/addon-test-dg-helper-completeness-guide.md @@ -0,0 +1,270 @@ +# Addon Test Helper Completeness — 多步骤异步操作的 multi-gate 验证,避免中间状态伪造 false positive + +> **Audience**: addon dev / test / TL +> **Status**: stable +> **Applies to**: any KB addon +> **Applies to KB version**: any (methodology, version-agnostic) +> **Affected by version skew**: no + +属于:方法论主题文档(不绑定单一引擎)。Oracle/DG 具体案例见文末附录。 + +## 1. 这篇要解决的问题 + +轮询等待某个操作"成功"的 test helper,其质量上限由它对"成功"的定义决定。当被测操作是**多步骤、异步执行**的,系统往往会在中间某一步完成时就发出一个"看起来像最终成功"的信号——而此时整个操作远未结束。 + +只检查单一终态信号的 helper 会发生 false positive:在系统仍处于不完整中间状态时就返回成功。下游 assertion 随即在错误的系统状态上执行,产生虚假的测试失败(更糟糕的是:虚假的测试通过)。 + +具体的失败模式:你写了一个检查 `"SUCCESS"` 字符串的 helper。被测操作实际上由三个顺序的异步步骤组成。步骤 1 单独完成后就产生 `Configuration Status: SUCCESS`——但 standby 成员尚未加入。你的 helper 立即返回成功。下一条 assertion 查询 standby,拿到的是无效数据。 + +这篇文档解决的问题就是:**如何设计 multi-gate helper,让它在操作真正完整完成前不会提前返回**。 + +## 2. 为什么中间状态看起来像最终状态 + +多步骤异步操作的典型结构: + +1. 多个子任务独立完成,无原子事务保证 +2. 每个子任务完成时立即更新系统状态 +3. "终态成功信号"由步骤 1(或某个早期步骤)写出,而不是最后一步 + +这使得 `STATUS = SUCCESS` 成为**必要条件,但不是充分条件**。 + +不同引擎中出现同一模式的例子(引擎无关的泛化规律): + +- **Redis Sentinel**:`SENTINEL sentinels mymaster` 返回的 sentinel 数量小于 quorum,但 `INFO` 命令已返回 `role:master` +- **Galera**:`wsrep_cluster_size=1`(bootstrap 阶段,单节点认为自己已形成集群) +- **MySQL 半同步复制**:`information_schema.PLUGINS` 显示插件已加载,但 `rpl_semi_sync_master_status = OFF`(尚无已连接的 slave) +- **etcd**:单节点 quorum 已达成,但 peer endpoints 还未 join + +规律相同:**一个早期步骤产生了外观与最终状态相同或相似的信号**。Oracle/DG 的具体时序见文末附录。 + +## 3. Multi-gate helper 设计 + +Multi-gate helper 的工作原理:检查**多个独立条件**,所有条件必须同时满足,才宣告成功。 + +> **gate 必须串行 AND,不能短路 OR。** + +每个 gate 必须顺序检查。任何一个 gate 失败,整个 poll cycle 重新开始——不允许跳过失败的 gate 向后推进。"所有 gate 通过"≠"gate 3 通过,即使 gate 2 失败了"。必须在**同一个 poll 迭代内**所有 gate 都通过,才返回成功。 + +为什么是串行 AND 而非 OR?因为中间状态可以通过早期 gate,而在后续 gate 失败。gate 1(SUCCESS 字符串)单独通过不够;只有全部 gate 同时通过,才能证明系统处于完整的最终状态。 + +Multi-gate helper 的结构: + +``` +poll loop: + sample system state (单次快照) + gate 1: 检查条件 A → 失败 → sleep, continue(重新开始 poll cycle) + gate 2: 检查条件 B → 失败 → sleep, continue + gate 3: 检查条件 C → 失败 → sleep, continue + ... + gate N: 检查条件 N → 失败 → sleep, continue + 所有 gate 通过 → 返回成功 +超时 → 返回失败 +``` + +关键约束:gate 1 失败后,不进行 gate 2-N 的检查,直接 sleep 并重试;gate 2 失败后,不进行 gate 3-N 的检查,直接 sleep 并重试。中间不允许有"部分通过"的中间结果被保留到下一次迭代。 + +## 4. Gate 设计原则 + +### §4a — Unfakeable Feature 原则(所有其他原则的根基) + +> **一个好的 gate 必须校验一个中间状态无法伪造的状态特征(observable invariant)。** + +Observable invariant 是系统的一个属性,满足: + +- 只有在产生它的具体步骤真正完成后,才会变为 true +- 任何早期步骤无法作为副作用产生它 +- 可以直接观测(不依赖推理) + +**什么是 unfakeable(observable invariant)**: + +| 特征类型 | 示例 | 为什么 unfakeable | +|---|---|---| +| 成员计数 = 预期值 | `member_count >= expected_replicas` | 单 primary 无法产生 count=2 | +| 特定角色标签存在 | 输出中存在 `Physical standby database` 行 | CREATE CONFIGURATION 阶段 primary 不会产生这行 | +| 数值在阈值内 | `apply lag < 30s` | 刚加入的 standby 正在追赶 redo,lag 会很高 | +| 特定状态标志 | `Fast-Start Failover: ENABLED` | ENABLE FAST_START FAILOVER 命令执行后才出现 | + +**什么是 fakeable(不能作为单一 gate)**: + +| 信号 | 为什么 fakeable | +|---|---| +| `grep "SUCCESS"` | 中间状态(单 primary CREATE 完成)也返回 SUCCESS | +| exit code 0 | 工具内部 ENABLE 可能失败,但仍 exit 0 | +| "查询返回非空" | 任何时刻都可能非空 | +| `STATUS = OK` 等通用状态字符串 | 任意早期步骤均可写出这些字段 | + +**判断方法**:问自己"中间状态(尚未完成 step N)能产生这个特征吗?"。如果能 → 不是 unfakeable feature,不能单独作为 gate。 + +### §4b — Gate 排序 + +- 越稳定、越快速可验证的 gate 放前面——快速排除明显未完成状态,减少重试代价 +- 越容易被中间状态伪造的特征放后面——让它无法单独"通关" +- 不可伪造的计数 / 角色标签 / 数值类 gate 放最关键位置(通常是 gate 2/3/4) + +注意:fakeable gate(如状态字符串检查)可以保留为 gate 1,但它是 necessary 而非 sufficient——后续必须有 unfakeable gate 补位。 + +### §4c — 向后兼容 + +新 gate 通过可选参数激活,默认行为不变。已有调用者无需修改。 + +Pattern:`wait_for_x cluster [timeout] [strict_param_1] [strict_param_2]` + +- 无 strict params → gate 1 only(旧行为) +- 有 strict params → 全部 gate 激活 + +这使得已有测试用例无需感知 helper 的变化,新场景按需启用更严格的校验。 + +### §4d — 容错 gate + +部分 gate 允许在取值失败时降级(skip 而非 fail)。适用于: + +- 被测指标在操作早期可能不可查(SQL 层尚未就绪) +- 网络抖动导致单次查询失败不代表系统状态变化 + +**降级 gate 不能是 unfakeable feature gate**——关键验证不能被降级掉。降级时必须记录 skip 原因(便于事后 log 分析)。 + +## 5. 验证纪律:dry-run + fresh install 缺一不可 + +Fix 实现后,必须同时跑两种验证: + +**1. Dry-run on existing cluster**(验证无 false-negative) + +- 对一个已经处于完整健康状态的集群运行修改后的 helper +- 预期:helper 应该立即通过所有 gate,不应该因为新 gate 把健康集群判成 unhealthy +- 证明:fix 不破坏向后兼容,不会让已完成的集群变红 + +**2. Fresh install**(验证 fix 真正 blocks race window) + +- 从零开始安装集群,在操作进行中触发 helper +- 预期:helper 在 race window(操作未完成时)应该 fail,在操作完成后应该 pass +- **黄金证据形式**:helper 在 race window 内记录 `gate1=fail elapsed=Xs`,然后在下一次 poll 所有 gate 通过 + +这是"fix 真的等了那 X 秒"的直接证据。只有 dry-run 而没有 fresh install,无法证明 fix 真正阻断了 race window——因为已完成的集群上 race window 根本不会出现。 + +## 6. 泛化适用性 + +本文方法论不绑定 Oracle/DG。以下三个特征同时满足的场景,均适用本文的 multi-gate 设计: + +1. **操作分多个异步步骤**:每步独立完成,没有原子事务 +2. **中间步骤产生局部完成信号**:该信号外观与最终信号相同或相似 +3. **helper 依赖 poll + 信号比对**:轮询直到信号出现即返回 + +这覆盖了绝大多数分布式系统运维操作的验证场景:复制拓扑建立、集群成员变更、配置参数热加载、备份 / 恢复完成判定等。 + +## 7. 反模式表 + +| 反模式 | 描述 | 后果 | 正确做法 | +|---|---|---|---| +| 只检查状态字符串 | `grep "SUCCESS"` 即返回 | 中间状态伪造成功,下游 assertion 基于错误状态运行 | 识别 unfakeable feature,增加 member count / role label / metric gate | +| gate 短路 OR | 任意一个 gate 通过即认为成功 | 早期步骤完成的 gate 通过,掩盖了后续步骤未完成 | gate 串行 AND,所有 gate 在同一 poll 迭代内全部通过 | +| 只做 dry-run 验证 | 在已完成的集群上测试 helper fix,没有 fresh install | 无法证明 fix 在 race window 内真正阻断,可能仍有漏洞 | dry-run + fresh install 双验证;缺一不可 | +| 把 observable invariant 降级为 optional gate | 将 unfakeable feature(如 member count)设为可选 / 可 skip | race window 中关键验证被跳过,false-positive 回归 | unfakeable feature gate 不能被降级;只有取值失败的辅助 gate 可以降级 | + +## 8. 自检清单(写 test helper 前过一遍) + +写一个轮询等待操作完成的 test helper 之前,先自问: + +1. **我是否画出了被测操作的完整步骤时序图?** 每步会产生哪些可观测信号? +2. **每个 gate 是否校验了一个 unfakeable feature?** 中间状态能产生这个特征吗? +3. **我的 gate 是串行 AND 吗?** 任何一个失败是否都会重试整个 poll cycle,而不是跳过继续? +4. **新 gate 是否通过可选参数激活?** 已有调用者是否需要修改? +5. **fix 后是否同时跑了 dry-run + fresh install?** 两种验证缺一不可。 + +## 9. 与其他方法论文档的关系 + +`addon-bounded-eventual-convergence-guide.md` 解决的是:对外部异步系统做单次 snapshot 判定会踩中间态,修复模板是 bounded retry。 + +本文解决的是:即使已经做了 bounded retry,**poll 的终止条件本身不够强**——只检查单一信号,而系统在中间状态也能发出该信号。Multi-gate 是对 bounded retry 的补强:不只是"retry 够多次",还要"每次 retry 检查足够多的维度"。 + +两篇结合:**对异步收敛状态,既要有足够的等待时间(bounded retry),也要有足够强的终止条件(multi-gate)**。 + +## 10. 给后续 addon 工程师的固化要求 + +1. **先画被测操作的步骤时序图**,识别每个步骤会产生哪些可观测信号——这是 gate 设计的前提 +2. **每个 gate 必须校验一个 unfakeable feature(observable invariant)**——中间状态无法产生的特征 +3. **只校验字符串 match / exit code 0 的 helper 必须审查**是否存在能在中间状态伪造这些信号的场景 +4. **gate 串行 AND,不能短路 OR**——所有 gate 必须在同一 poll 迭代内同时通过 +5. **新 gate 通过可选参数激活**,不破坏已有调用者 +6. **fix 后必须同时跑 dry-run(无 false-negative)+ fresh install(真正 blocks race window)** +7. **黄金证据形式**:fresh install 时 helper 在 race window 内记录 fail,下一 poll 通过——这是"fix 等了那些秒"的直接证据 + +## 11. 案例附录 + +### Case:Oracle Data Guard broker setup — Bug #26 + +#### setup_dg_broker.sh 三会话时序(精确时间戳,来源:chaos-replication-run5-FAIL-20260430.log) + +``` +2026-04-30T11:31:20Z Session 1: CREATE CONFIGURATION + ENABLE CONFIGURATION + → SHOW CONFIGURATION: Configuration Status: SUCCESS + (仅 ORCLCDB_0 Primary,无 standby,FSFO DISABLED) + +2026-04-30T11:31:28Z Session 2: ADD DATABASE ORCLCDB_1 (8 秒后) + +2026-04-30T11:31:50Z Session 3: ENABLE FAST_START FAILOVER (再 22 秒后) +``` + +Race window:session 1 结束后 → session 2 开始前(约 8 秒)。 + +#### Bug #26 触发(Run 5 log,第 74–91 行) + +``` +# log line 74: +[INFO] Waiting for Data Guard broker SUCCESS on ora-ch-81949 ... +# log line 75-87: old helper (gate 1 only) — sees single-node SUCCESS, returns immediately +[INFO] Data Guard configuration shows SUCCESS + Members: + ORCLCDB_0 - Primary database +Fast-Start Failover: DISABLED +Configuration Status: +SUCCESS (status updated 7 seconds ago) +[PASS] C00: DG configuration SUCCESS (DG broker SUCCESS) +# log line 91: standby not yet a member → SQL returns text instead of count +[FAIL] C00: baseline row exists (DG sync verified) (pod=1) + (expected='1', got='SELECT COUNT(*) FROM john_chaos_rows WHERE id=1 AND val=''chaos''') +``` + +(来源:`oracle/evidence/chaos-replication-run5-FAIL-20260430.log`,第 74–91 行) + +旧 helper 在 session 1 完成后的 8 秒 race window 内拿到 `SUCCESS` 字符串,立即返回。此时 standby 尚未加入 DG 配置,下一步对 standby pod 的 SQL 查询拿到的是原始 SQL 文本而非行数,导致 assertion 失败。 + +#### Fix — 5-gate wait_for_dg_healthy(commit ab7cb76) + +来源:`kubeblocks-tests/oracle/lib/common.sh`,第 391–504 行 + +| Gate | 条件 | Observable Invariant? | 说明 | +|---|---|---|---| +| 1 | `Configuration Status: SUCCESS` | ❌ fakeable(单 primary 也返回) | Necessary but not sufficient;保留为快速前置检查 | +| 2 | member count ≥ expected_replicas | ✅ | standby 未加入时 count=1,无法伪造 count=2 | +| 3 | `Physical standby database` 存在 | ✅ | CREATE CONFIGURATION 阶段 primary 不产生这行 | +| 4 | `Fast-Start Failover: ENABLED` | ✅ | session 3 执行前不出现 | +| 5 | apply lag < 30s on each standby | ✅ | 追赶中 lag 会高,刚加入时无法满足 | + +Gate 1 是 necessary 但 not sufficient(fakeable);Gates 2–5 是 unfakeable features。全部串行 AND 通过才返回成功。 + +#### 黄金证据(Run 6 fresh install,第 74–99 行) + +``` +# log line 74: +[INFO] Waiting for Data Guard broker SUCCESS on ora-ch-562 (expected_replicas=2, mode=replication) ... +# log line 75: fix blocks the race window — gate 1 fail at 33s +[INFO] [wait_for_dg_healthy] elapsed=33s gate1=fail last_output=Connected to "ORCLCDB_0" + Configuration - ora-ch-562 + Members: + ORCLCDB_0 - Primary +# (next poll) all 5 gates pass: +[INFO] Data Guard configuration shows SUCCESS + Members: + ORCLCDB_0 - Primary database + ORCLCDB_1 - (*) Physical standby database +Fast-Start Failover: ENABLED +Configuration Status: +SUCCESS (status updated 3 seconds ago) +[PASS] C00: DG configuration SUCCESS (DG broker SUCCESS) +[PASS] C00: baseline row exists (DG sync verified) (pod=0) +[PASS] C00: baseline row exists (DG sync verified) (pod=1) +``` + +(来源:`oracle/evidence/chaos-replication-run6-PASS-20260430.log`,第 74–99 行) + +Fix 在 race window 内(elapsed=33s)记录了 `gate1=fail`,然后在下一次 poll 所有 5 个 gate 通过。这是"fix 真的等了那 33 秒"的直接证据。 diff --git a/docs/addon-test-runner-cadence-discipline-guide.md b/docs/addon-test-runner-cadence-discipline-guide.md new file mode 100644 index 0000000..7b1d7cc --- /dev/null +++ b/docs/addon-test-runner-cadence-discipline-guide.md @@ -0,0 +1,249 @@ +# Addon 测试 Runner Cadence 纪律指南 — Cadence 是操作者的义务,不是 Runner 的义务 + +> **Audience**: addon test engineer / agent / TL +> **Status**: stable +> **Applies to**: any KB addon long-running test operation +> **Applies to KB version**: any (methodology, version-agnostic) +> **Affected by version skew**: no + +属于:方法论主题文档(不绑定单一引擎)。 + +## 1. 这篇要解决的问题 + +长时间运行的测试操作(chaos 轮次、集群安装、备份恢复、vscale 等)往往要跑 20-60 分钟。在这段时间里,操作者(测试工程师或 agent)和 reviewer(如 TL / review owner)之间有一个信息不对称:**操作者看着日志,reviewer 什么都看不到**。 + +如果操作者只在开始时发一条"起跑"消息,然后等 runner 结束再汇报,reviewer 面临的处境是: + +> "这个 runner 到底是在正常跑,还是已经卡死,还是已经崩了?我不知道。" + +**Cadence** 就是用来填这个信息缺口的:操作者以固定间隔主动向 reviewer 汇报 runner 状态。没有 cadence,reviewer 无法区分"正常运行"和"卡死/崩溃",只能靠猜测或主动询问——这是信任债务的直接来源。 + +本文的触发案例:Oracle addon chaos 测试 Run 5(2026-04-30 19:19→20:17),操作者在"起跑"之后沉默 58 分钟,reviewer 被迫发出明确催促才得知 runner 状态。 + +## 2. 什么是 Cadence + +**Cadence** 是操作者在长时间运行操作期间,以固定间隔向 reviewer 主动发送的状态汇报。 + +关键要素: + +- **发起方**:操作者(主动),不是 reviewer(被动等待) +- **频率**:固定间隔,默认 5 分钟,不是"有进展才汇报" +- **内容**:当前 runner 状态快照(pod ready 数、phase、活跃操作、有无异常) +- **方向**:即使没有新进展,也要发;"仍在运行"本身就是有效状态 + +Cadence 不是 reviewer 的要求——它是操作者对 reviewer 的服务义务。reviewer 不应该需要开口问,才能知道 runner 活着。 + +## 3. 5 分钟是硬约束,不是建议 + +cadence 间隔默认为 **5 分钟**,这是硬约束,不是软性指南。 + +原因: + +**超过 cadence 间隔的沉默,对 reviewer 等价于"runner 状态未知"。** + +reviewer 没有办法从沉默中区分: +- runner 正常运行,操作者只是没说 +- runner 卡在某个步骤,操作者没察觉 +- runner 崩溃,操作者自己也不知道 + +从 reviewer 的视角,这三种情况的信息是完全相同的——都是沉默。 + +**reviewer 没有 benefit of doubt**。在 cadence 间隔过期之后,reviewer 有充分理由认为 runner 状态不明,并且有理由中断等待来询问。这不是 reviewer 苛刻,而是操作者未履行义务。 + +超过 cadence 间隔不发 ping,是操作者主动放弃了告知 reviewer 的责任。 + +## 4. Cadence 的责任归属与常见误区 + +### §4a — Cadence 是操作者的义务,不是 runner 的义务 + +这是最核心的一点,也是最常被混淆的一点。 + +**角色区分**: +- **runner**(chaos 脚本、测试脚本、DBCA 安装脚本)是**被观察的对象** +- **操作者**(测试工程师、agent)是**观察者** +- **reviewer**(TL / review owner)在等**操作者**的汇报,不是在等 runner 自己输出 + +cadence 是操作者对 reviewer 的汇报义务。它和 runner 有没有新输出无关,和 runner 有没有完成当前步骤无关。 + +**操作者是独立于 runner 而存在的角色**。操作者的工作是:监控 runner、读日志、形成状态判断、向 reviewer 汇报。这个工作不依赖 runner 先完成某件事。 + +### §4b — 三类常见误区,均不构成 cadence 豁免 + +**误区 1:"我在等 runner,等它结束再汇报。"** + +错误之处:等 runner 和发 cadence ping 是两件完全独立的事情。你可以一边等 runner 运行,一边每 5 分钟读一次日志汇报当前状态。它们不互斥。 + +reviewer 等待的是**你**的汇报,不是 runner 的完成信号。runner 还没结束,并不妨碍你汇报"runner 仍在运行,当前 pod 3/4,已跑 12 分钟"。 + +**误区 2:"runner 没有新输出,没东西可说。"** + +错误之处:"没有新进展"本身就是一种状态,汇报它是有价值的。 + +一条"仍在运行,pod 3/4 维持 8 分钟,无报错"的 ping,对 reviewer 的价值远大于沉默。沉默传递的信息是"未知";no-progress ping 传递的信息是"可控的等待"。 + +**误区 3:"我没有新进展可以汇报。"** + +错误之处:cadence ping 的目的不是"有新发现才汇报",而是"让 reviewer 知道你还在"。"目前无新进展"就是你要汇报的内容。 + +## 5. 里程碑主动 Ping + +除了固定间隔 cadence,关键里程碑的发生应该立即触发 ping,不等下一个 cadence tick。 + +**里程碑示例**(不限于此): +- 集群 phase 变为 Running +- DG broker setup 成功 +- 每一轮 chaos round 完成(PASS 或 FAIL) +- SQL ready / 主库角色确认 +- backup 开始 / 完成 +- switchover 发生 +- 任何 ERROR 或意外状态出现 + +**判断标准**:如果你正在记录一条日志,而这条日志是你会记录在 work-log 里的——那么同时向 reviewer ping 一条。如果这个事件值得记录,它就值得 ping。 + +里程碑 ping 和 cadence ping 叠加,不互相替代。里程碑在两个 cadence tick 之间发生,立即 ping,不等下一个 tick。 + +## 6. Cadence 触发器的设计原则:与 Runner 进程解耦 + +这是实现层面最关键的一点:**cadence 触发器必须与 runner 进程解耦**。 + +原因:如果 cadence 触发器依赖 runner 进程,runner 一旦卡住(比如卡在 20 分钟的 DBCA 等待、卡在网络 I/O、卡在 shell 循环),cadence 也同时卡住。这等于没有 cadence。 + +**正确设计**:cadence 触发器是一个独立的机制,不论 runner 是否活跃、是否阻塞,都能按时触发。 + +平台无关的可行方案(任选其一或组合): +- **定时提醒**(例如 `slock reminder schedule --recurring 5m`),锚定到任务消息,触发时由操作者读日志汇报 +- **Cron / 定时任务**,独立进程,读 log 文件并发送状态 +- **IM bot 定时消息**,提醒操作者主动发 ping +- 任何其他机制,只要满足"runner 卡住时 cadence 仍然能触发" + +**错误设计**:把 cadence 逻辑嵌进 runner 脚本(比如在 chaos 脚本内部每 5 分钟调用一次汇报函数)。原因:脚本 hang 住时,嵌入的 cadence 逻辑也 hang 住,cadence 失效。 + +核心原则:**cadence 触发器与 runner 进程解耦**。cadence 必须能在 runner 卡死时继续工作。 + +## 7. 打破 Cadence 的后果 + +打破 cadence(超过间隔未 ping)的实际代价: + +1. **reviewer 无法区分正常运行与卡死/崩溃**,必须主动询问才能获得本应主动提供的信息 +2. **信任债务**:reviewer 下次会更早催促,因为无法依赖操作者的主动汇报 +3. **"我在后台跑"不是理由**:reviewer 看不到后台,操作者的工作就是把后台可见化 +4. **cadence 断档需要显式补救**:如果因为某种原因出现了沉默间隔,重新开始时必须发"恢复汇报"消息,补充这段时间的状态,而不是假装断档没发生 + +打破 cadence 一次,reviewer 会怀疑后续每次 ping 的可靠性。这不是个人信任问题,而是流程债务:操作者未能维护的纪律,需要 reviewer 用额外的主动询问来补偿。 + +## 8. 给后续 addon 工程师的固化要求 + +1. **cadence 是操作者的义务,不是 runner 的义务**——runner 没有新输出,操作者仍然要 ping +2. **沉默超过 cadence 间隔 = 状态未知 = 等价于 runner 已失败**——reviewer 无 benefit of doubt +3. **no-progress ping 仍然有效**——"仍在运行,pod 3/4 维持 8min"是合法 ping,不是废话 +4. **里程碑事件立即 ping,不等下一个 tick**——关键状态变化不能被 cadence 间隔延迟 +5. **cadence 触发器必须独立于 runner 进程运行**——runner hang 时 cadence 不能一起 hang +6. **log-only 不等于 ping**——把状态写进日志文件不能替代向 reviewer 发可见消息 +7. **打破 cadence 后必须发显式恢复消息**——"我回来了,这段时间发生了 X、Y、Z",不能无声续跑 + +## 9. 反模式表 + +| 反模式 | 描述 | 正确做法 | +|---|---|---| +| "等 runner 结束再汇报" | runner 运行期间操作者完全静默,以为等结束再一次性汇报即可 | 设置独立 cadence 定时器,每 5min 主动读日志汇报当前状态 | +| "没新进展不 ping" | 认为"没进展 = 不需要汇报",沉默对 reviewer 传递有效信息 | no-progress ping 仍然有效("仍在运行,pod 3/4 维持 8min") | +| "把 cadence 逻辑嵌入 runner 脚本" | 在测试脚本内部定期汇报,runner hang 时 cadence 也 hang | cadence 触发器独立于 runner 进程,runner 卡死时 cadence 仍能工作 | +| "milestone 消化完再 ping" | 关键事件发生后等到下一个 cadence tick 才汇报 | 里程碑立即触发 ping,不等下一个 tick | + +## 10. 自检清单(启动长时间操作前逐项确认) + +启动任何预计超过 5 分钟的操作之前: + +1. **我是否设置了独立于 runner 的 cadence 触发器?** 触发器在 runner 卡住时还能工作吗? +2. **cadence 间隔是否 ≤5 分钟?** 不是 10 分钟,不是"大概每隔一会儿"。 +3. **我是否梳理了本次操作的关键里程碑?** 每个里程碑完成后是否会立即 ping,不等 cadence tick? +4. **如果 runner 卡在某步 20 分钟,我的 cadence 触发器还能按时触发吗?** 如果答案是否,重新设计触发机制。 + +## 11. 案例附录 + +### 反面案例 — Run 5(2026-04-30 19:19→20:17,58 分钟沉默) + +**时间线**: + +| 时刻 | 事件 | +|---|---| +| 19:18 | Run 5 启动(fresh cluster install,chaos 套件) | +| 19:19 | 操作者发出最后一条 ping:"Run 5 起跑" | +| 19:19–20:17 | **58 分钟完全静默**——操作者在等 runner,未设置独立 cadence 触发器 | +| 20:17 | Reviewer 主动发出催促:"19:19 起跑后 58min 无 ping. 立即报告: PID/log/cluster status/sentinel/死了?" | + +**问题分析**: + +操作者的心理模型是"等 runner 结束再一次性汇报"。但 reviewer 的心理模型是"操作者应该每 5 分钟让我知道 runner 活着"。这两个模型在 58 分钟的沉默中完全错位。 + +reviewer 在 20:17 之前完全无法区分: +- runner 正常跑了 58 分钟,操作者只是忘了 ping +- runner 在某步卡死,操作者没察觉 +- runner 崩溃,操作者也不知道 + +**直接后果**:reviewer 必须打断自己的工作来询问本应主动提供的信息;操作者的汇报可靠性下降;下次 reviewer 会更早催促。 + +**根因**:没有独立于 runner 的 cadence 触发器。操作者在等 runner,cadence 没有独立的触发机制。 + +--- + +### 正面案例 — Run 6(2026-04-30 20:30→20:58,7 次 cadence ping,全部在间隔内) + +**时间线**: + +| 时刻 | 类型 | 内容摘要 | +|---|---|---| +| 20:30 | 起跑 | Run 6 启动,设置独立 5min cadence 提醒 | +| 20:35 | cadence ping 1 | oracle-0 4/4 Running,oracle-1 3/4(RMAN active),无报错 | +| 20:41 | cadence ping 2 | oracle-0 4/4,oracle-1 4/4,phase Creating,DG broker 初始化中 | +| ~20:44 | 里程碑 ping | phase=Running(立即 ping,不等下一个 tick) | +| ~20:47 | 里程碑 ping | C00 DG SUCCESS 验证完成(5 项证据:broker status / lag / role / SCN / ping) | +| 20:50–20:55 | 每轮 ping | C01 r1-r5 各轮完成后立即汇报(PASS/FAIL + 简要证据) | +| 20:58 | 最终汇报 | Run 6 完成,整体 PASS,附汇总表 | + +**7 次 cadence ping,reviewer 全程未需要主动询问状态。** + +**关键差异**: + +1. **独立触发器**:操作者在起跑时设置了独立的 5 分钟提醒,提醒触发时操作者主动读日志汇报,与 runner 进程解耦 +2. **no-progress ping 合理**:20:35 的 ping 没有重大新进展(runner 仍在运行),但它让 reviewer 知道"runner 活着,状态可控" +3. **里程碑不等 tick**:20:44 phase=Running 立即 ping,没有等到下一个 5min tick +4. **reviewer 零询问**:整个 28 分钟操作,reviewer 从未需要主动询问,因为状态始终可见 + +**bash 参考(仅供附录)**: + +如果在 bash 环境中手动管理 cadence,可以在 runner 启动后,用后台独立进程发送提醒,而不是在 runner 脚本内部嵌入: + +```bash +# 独立 cadence 提醒进程(与 runner 解耦) +( + while true; do + sleep 300 + echo "[CADENCE] 5min tick: 请读日志汇报当前 runner 状态" + done +) & +CADENCE_PID=$! + +# 启动 runner +bash chaos_run.sh > /tmp/chaos.log 2>&1 +RUNNER_RC=$? + +# runner 结束后停掉 cadence 提醒 +kill $CADENCE_PID 2>/dev/null + +echo "runner exit code: $RUNNER_RC" +``` + +注意:上面的 cadence loop 在 runner hang 时仍会按时触发提醒——这是解耦的核心价值。提醒触发时,操作者读 `/tmp/chaos.log` 末尾,形成状态判断,向 reviewer 发 ping。 + +如果使用 slock,可以通过 `slock reminder schedule --recurring 5m` 锚定到任务消息,效果相同。 + +--- + +### 与其他文档的关系 + +- [`addon-test-acceptance-and-first-blocker-guide.md`](addon-test-acceptance-and-first-blocker-guide.md) — 测试验收标准与 first blocker 归层 +- [`addon-evidence-discipline-guide.md`](addon-evidence-discipline-guide.md) — 汇报时证据强度与结论的匹配规则 +- [`addon-test-environment-gate-hygiene-guide.md`](addon-test-environment-gate-hygiene-guide.md) — 起跑前环境就绪门控 + +本文与 evidence-discipline-guide 互补:evidence guide 管"汇报什么内容"(证据强度),本文管"什么时候汇报"(cadence 纪律)。两者都是操作者对 reviewer 的服务义务的组成部分。