diff --git a/docs/SKILL-INDEX.md b/docs/SKILL-INDEX.md index 9576711..b4569dc 100644 --- a/docs/SKILL-INDEX.md +++ b/docs/SKILL-INDEX.md @@ -30,6 +30,7 @@ - [`addon-pvc-rebind-via-workload-intent-guide.md`](addon-pvc-rebind-via-workload-intent-guide.md) — 当一条 OpsRequest 需要把同名 PVC 从一块 PV 改绑到另一块(rebuild / restore-into-place / PV migration),用 Workload CR annotation 把意图交给 Workload 控制器(唯一写者),并按 intent-before-release + helper PV `claimRef` prebind 顺序交接所有权,避免 OpsRequest 控制器、Workload 控制器、动态 provisioner 三方抢同名 PVC 所有权造成 `PersistentVolume "" not found`、helper PV 消失或绑错 PV - [`addon-fake-server-protocol-validation-guide.md`](addon-fake-server-protocol-validation-guide.md) — FakeSentinel / FakeMySQL / fake mongos 等 fake protocol server 的 golden real-server 对照验收方法:raw protocol request、协议感知 reader、命令矩阵分类、known delta 记录、client-consumed fields 覆盖证明;附 Redis Sentinel RESP 案例 - [`addon-engine-image-entrypoint-env-name-collision-guide.md`](addon-engine-image-entrypoint-env-name-collision-guide.md) — chart cmpd 定义的 env name 与 engine docker image 内 docker-entrypoint.sh reserved env name 撞名时, image entrypoint 会静默执行副作用 (典型: CREATE USER 无 IF NOT EXISTS 写 binlog → secondary replay 撞 1396)。识别信号 / grep entrypoint script 排查 / rename chart env 修法 (KB_ prefix 推荐) / 验证收敛 6-gate / N=1 GREEN ≠ release-ready。附 MariaDB Addon 2026-05-13 案例 (mariadb:11.4 entrypoint grep + chart diff + 6-gate verify) 进 Case Appendix +- [`addon-postready-bounded-timeout-failure-classification-guide.md`](addon-postready-bounded-timeout-failure-classification-guide.md) — addon DataProtection ActionSet `spec.restore.postReady` 长操作(`CREATE STANDBY TENANT` / secondary 重建 / `RESTORE DATABASE` 第二阶段)必须外层 `timeout -k 1 N` + bounded retry deadline + 5 层失败分类(ENV_PRE_FAIL / RUNNER_INVALID / CONTROL_PLANE_STALL / ENGINE_ERROR / ENGINE_CONVERGE_TIMEOUT)+ trap EXIT INT TERM;caller budget > 内层 step bounded retry > 单步 timeout 的预算层级;patch-gate 4 指标仅证修复在位、不证 PITR / 功能 PASS / acceptance / release-ready;附 OceanBase enterprise addon `oceanbase-physical-backup` postReady 改造后 6 样本无 hang 案例(边界明写"不外推") ### 2. 写新 smoke / chaos 测试 diff --git a/docs/addon-postready-bounded-timeout-failure-classification-guide.md b/docs/addon-postready-bounded-timeout-failure-classification-guide.md new file mode 100644 index 0000000..13a113c --- /dev/null +++ b/docs/addon-postready-bounded-timeout-failure-classification-guide.md @@ -0,0 +1,239 @@ +# Addon ActionSet `postReady` — Long-Running Engine Hooks Need Bounded Timeout + Failure Classification + +> **Audience**: addon dev / test / TL +> **Status**: stable methodology +> **Applies to**: any KB addon ActionSet whose `spec.restore.postReady` (or equivalent post-restore reconciliation hook) issues a multi-step engine command that can legitimately take minutes but might also hang indefinitely (e.g. `CREATE STANDBY TENANT`, `RESTORE DATABASE` second-stage, `attach replication`, schema warm-up, role re-elect, full-table verify). +> **Applies to KB version**: any (methodology). + +属于:方法论主题文档(不绑定单一引擎)。 + +## 1. 这篇要解决的问题 + +addon 在 KubeBlocks DataProtection `ActionSet.spec.restore.postReady` 里经常会跑「长操作」——把 restore 出来的 base + archive 在引擎层 finalize 成可用副本。这类操作的两个共同形态: + +1. **正常时长跨度大**——secondary 重建 / standby tenant 创建 / 索引重建可能 30s 到 30min 不等,取决于数据量与硬件。 +2. **故障态没有自显式失败**——卡在 wait-for-something(lock、journal、catalog 改变、协调线程)就一直 Running 下去,引擎本身不超时退出。 + +**反模式**:postReady 里写一个无限循环或没有外层 timeout 的命令,让它"自然结束"。一旦碰上故障态: + +- DataProtection controller 看到 Restore CR phase=`Running` 一直不变,没办法分类。 +- 测试 runner / OpsRequest 等到自己的 budget 超时才 fail,错过现场。 +- 没有可分类的 first-blocker 信号——只能写"hang"。 + +**目标**:postReady 必须有 **bounded timeout + 失败分类**,让 hang 在 budget 内变成可读 reason,让 controller / runner / 测试都能据此决定下一步。 + +> **边界 framing**:本 doc 只讲 ActionSet `postReady` 这一类 hook(以及形态类似的 `preBackup` / `preReady` 重 SQL hook)。`backupData` 主备份阶段、`prepareData` 单纯文件拷贝、lifecycle action 短探针(roleProbe / readinessProbe)的 timeout 设计另开主题。 + +## 2. 硬规则 + +### 规则 A — 任何长操作必须有外层 `timeout`(bash level),budget 与单步预算分离 + +```bash +# 反例:无外层 timeout,命令自己跑多久都不退 +mysql --defaults-file="$cnf" -e "CREATE STANDBY TENANT IF NOT EXISTS ..." +``` + +```bash +# OK:外层 timeout 锁住单条客户端调用 ≤ N 秒 +timeout -k 1 60 mysql --defaults-file="$cnf" -e "CREATE STANDBY TENANT IF NOT EXISTS ..." +``` + +- `60` 是单步预算上限。**单步预算 ≠ postReady 总 budget**:postReady 总 budget 在测试侧(如 `TIMEOUT_RESTORE=1800`);单步 timeout 是 hook 内每一条 SQL 自己的上限,防止一条 SQL 永久阻塞 hook 整体。 +- `-k 1` 软超时后 1 秒再发 KILL,避免僵进程。 +- `timeout` 退出 rc=124 / 137 时一定写到日志,让上层分类得到信号。 + +### 规则 B — 多步骤等待(poll)必须用 bounded retry,不允许 `while true` + +postReady 里常见模式:先发命令,再 poll 引擎元数据等收敛。规则: + +```bash +# 反例:while true 等收敛,没退路 +while true; do + status=$(query_status) + [ "$status" = "NORMAL" ] && break + sleep 5 +done +``` + +```bash +# OK:bounded retry 给上限,给失败 reason +deadline=$(( SECONDS + 600 )) # 10 min 上限 +status="" +while [ $SECONDS -lt $deadline ]; do + status=$(timeout -k 1 15 query_status 2>/dev/null || true) + if [ "$status" = "NORMAL" ]; then + break + fi + sleep 5 +done + +if [ "$status" != "NORMAL" ]; then + echo "ERROR: postReady standby_create not NORMAL after $((SECONDS-deadline+600))s, last_status=${status:-}" + exit 1 +fi +``` + +- 单次 `query_status` 也包 `timeout`(规则 A)。 +- deadline 到时**必须**有可读的失败 reason 输出:写到 stderr / pod log,包含**最后一次观测到的状态值**和**总等待时长**。无 reason 的 exit 1 在控制面看到的只是 `phase=Failed`,没有分类依据。 + +### 规则 C — 失败必须分到 5 层之一,由命令行能区分 + +每个长操作的失败出口至少能映射到下面 5 类(参考 `addon-test-acceptance-and-first-blocker-guide.md`): + +| 层 | 触发条件 | 退出形式 | +|---|---|---| +| env / pre-condition | client 都连不上 / DNS 不可解析 / 鉴权失败 / 必备 resource 缺失 | exit 70 + "ENV_PRE_FAIL: " | +| runner / 口径 | 脚本本身参数错(手写错 user / 错 db / 错 host) | exit 71 + "RUNNER_INVALID: " | +| 控制面 / control-plane | OpsRequest / Restore CR / Workload CR 在 K8s 控制面层异常(GC、reconcile 卡) | exit 72 + "CONTROL_PLANE_STALL: " | +| 引擎产品 | 引擎报硬错(OB error code、MySQL 1xxx error) | exit 73 + "ENGINE_ERROR: " | +| 引擎 hang(最后兜底) | 上面 4 类都没匹配,等到 deadline 仍未收敛 | exit 74 + "ENGINE_CONVERGE_TIMEOUT: last_status= waited=" | + +写 `exit` 时直接退出码不同 + reason 字符串规整,下游 DataProtection controller、test runner、closeout 报告就可以按 grep 分类,不是猜。 + +### 规则 D — 兜底 trap:postReady 任何退出都要清掉临时凭据文件 / 副作用 + +参见 `addon-mysql-credential-hygiene-no-argv-guide.md` §2 规则 B: + +```bash +trap 'rm -rf "${tmp_dir}"' EXIT INT TERM +``` + +`SIGTERM` 路径不挂 trap 的话,kubelet 给 pod 发终止信号时临时密码文件就留下了。 + +### 规则 E — postReady 之上的 controller / runner budget 必须独立可观察 + +addon 写 postReady 时**不**假设 caller 一定给了 budget。测试侧 / DataProtection controller 必须独立设置一个 outer budget(如 `TIMEOUT_RESTORE=1800`),二者关系: + +``` +caller budget (e.g. TIMEOUT_RESTORE=1800) + > sum of internal step bounded retries + > each timeout(60s) wrapping one mysql/obclient call +``` + +外层 > 内层每一层。任何一层穿透到外层,分类信号已经丢失。 + +## 3. 测试侧验收(patch-gate) + +加新 postReady 或改 postReady 后,pre-run gate 维护: + +``` +postReady_has_outer_timeout= # grep -cE 'timeout[ -]' +postReady_no_while_true_unbounded= # grep -cE 'while true' == 0 +postReady_failure_reason_strings= # 至少出现 ENV_PRE_FAIL / RUNNER_INVALID / CONTROL_PLANE_STALL / ENGINE_ERROR / ENGINE_CONVERGE_TIMEOUT 中 ≥1 个 token +postReady_trap_cleanup= # grep -cE '^[[:space:]]*trap ' +``` + +`patch-gate` 通过只能说明上述 4 条**在 live ActionSet 里**;**不**能写"产品 / addon / KB 已被证明稳定"。closeout 必须独立列 readback / cleanup / runtime 三类硬事实。 + +## 4. PR 评审 checklist + +新或改 postReady 时按这 6 条扫: + +1. 每条客户端调用是否包 `timeout -k 1 N`? +2. 是否有 `while true` 无 break / 无 deadline 的 loop?grep `'while true'` 应 0 命中。 +3. 失败出口是否带 reason 字符串(ENGINE_CONVERGE_TIMEOUT / CONTROL_PLANE_STALL / ENGINE_ERROR / RUNNER_INVALID / ENV_PRE_FAIL)?至少一个分类 token 命中。 +4. 退出码是否按 §2 规则 C 用了不同数字(70/71/72/73/74),不是统一 `exit 1`? +5. `trap 'rm -rf ${tmp_dir}' EXIT INT TERM` 是否覆盖 3 信号(避免 SIGTERM 不触发 cleanup)? +6. closeout 模板里有没有把 patch-gate 通过当 PITR / 功能 PASS 证据?(不该当) + +任一条不过,PR 不合并。 + +## 5. 反例 vs 正例 + +### 反例 1(无外层 timeout) + +```bash +mysql --defaults-file="$cnf" -e "CREATE STANDBY TENANT std FROM PRIMARY ..." +``` + +`CREATE STANDBY TENANT` 在引擎里卡 wait-for-something → 此 hook Block,DataProtection controller 看到 Restore phase=Running 一直不变。 + +### 正例 + +```bash +set -euo pipefail +tmp_dir="$(mktemp -d /tmp/addon-pr-XXXXXX)"; chmod 700 "$tmp_dir" +trap 'rm -rf "${tmp_dir}"' EXIT INT TERM +umask 077 +cnf="${tmp_dir}/client.cnf" +cat > "$cnf" </tmp/cs.err; then + rc=$? + if [ $rc -eq 124 ] || [ $rc -eq 137 ]; then + echo "ERROR: ENGINE_CONVERGE_TIMEOUT step=CREATE_STANDBY_TENANT waited=60s" + exit 74 + fi + # engine 报错(OB code / SQLSTATE) + echo "ERROR: ENGINE_ERROR step=CREATE_STANDBY_TENANT rc=$rc stderr=$(cat /tmp/cs.err)" + exit 73 +fi + +# bounded retry 等收敛 +deadline=$(( SECONDS + 600 )) +last_status="" +while [ $SECONDS -lt $deadline ]; do + last_status=$(timeout -k 1 15 mysql --defaults-file="$cnf" -BN -e \ + "SELECT status FROM oceanbase.DBA_OB_TENANTS WHERE tenant_name='std'" 2>/dev/null || true) + [ "$last_status" = "NORMAL" ] && break + sleep 5 +done +if [ "$last_status" != "NORMAL" ]; then + echo "ERROR: ENGINE_CONVERGE_TIMEOUT step=standby_tenant_normalize last_status=${last_status:-} waited=$((SECONDS - (deadline-600)))s" + exit 74 +fi +``` + +### 反例 2(`while true` 无退路) + +```bash +while true; do + status=$(query_status) + [ "$status" = "NORMAL" ] && break + sleep 5 +done +``` + +修:见正例。 + +### 反例 3(统一 exit 1,无分类) + +```bash +echo "ERROR something failed" +exit 1 +``` + +修:见 §2 规则 C。 + +## 6. 与其他 skill 的关系 + +- `addon-mysql-credential-hygiene-no-argv-guide.md` — 本 doc §2 规则 D 对临时凭据 trap 引用其规则 B;外层 `timeout` 与凭据卫生是两条正交规则。 +- `addon-bounded-eventual-convergence-guide.md` — 本 doc §2 规则 B 是它在 postReady 场景的特化;bounded retry 与 single-snapshot 反模式同源。 +- `addon-test-acceptance-and-first-blocker-guide.md` — §2 规则 C 的 5 层分类直接取自该 doc;postReady 的失败必须能 map 到那 5 层。 +- `addon-evidence-discipline-guide.md` — patch-gate 三指标只能证明"修复在位",不能写"产品 / addon / release-ready"。本 doc §3 与 §4 第 6 条复用其结论。 + +## Appendix A — OceanBase enterprise addon 案例(仅证据,不外推) + +### Case A.1 — `oceanbase-physical-backup` ActionSet postReady `CREATE STANDBY TENANT` 修复 + +OceanBase 早期版本(在某个 closeout 标为 `postreadyfix2`)出过一次 postReady hang:`CREATE STANDBY TENANT` 卡在 `CREATING_STANDBY`,进程在 `CREATE STANDBY TENANT`,超出测试 runner outer budget 1800s 仍未退;closeout 标 `FIRST_BLOCKER:PITR_postReady_standby_create_hang`。 + +按 §2 改造后(加 outer `timeout` + bounded retry deadline + 5 层失败分类): +- live postReady sha `b74912857084451adbf707166a82bd8f55efce5ca74f3b2ce964c65451fc9925` +- patch-gate:`postReady_has_outer_timeout=true / postReady_no_while_true_unbounded=true / postReady_failure_reason_strings≥1 / postReady_trap_cleanup=1` +- 后续在两条独立运行路径(Mac+port-forward 与 idc4 host-runner+in-cluster DNS)上共累计 6 个 PITR runtime 样本,**无**一例再现 `standby_create_hang`。 + +边界声明:6 样本无 hang **不**等于「hang 永远不再现」;只支持「在该 6 样本中,postReady 改造后没有进入卡死的可观察窗口」。**不**作为 PITR 验收 / addon acceptance / release-ready / cross-env 稳定的证据。 + +### Case A.2 — 失败分类落地 + +OceanBase enterprise addon `oceanbase-physical-backup` postReady 在改造后用 `ENGINE_CONVERGE_TIMEOUT step=standby_tenant_normalize last_status=<...> waited=<...>s` 这条标准 reason;DataProtection controller 把它附在 Restore CR `status.conditions`,runner / closeout 报告直接 grep 这个 token 做 first-blocker 分类,不再写"hang"无分类口径。 + +附录到此为止。后续若在其它引擎或其它 postReady 路径上有实测数据,请另起 appendix;**不**回写到上方方法论正文。