交接单按归属约定(正文落文档库、工作区只放指针)归档至 归档/交接单-20260924-归档/,含逐件判定 README: - 16 件已被文档库正式版取代(T09–T21 + 覆盖网络-24/25/26) - 4 件主题已被覆盖网络线入口汇总 - 7 件历史接续包/规划件 CODEBUDDY §9:收口清本棒 tmp、tmp 保留期 7 天、禁「待清理」中间态、 工作区入库只放文档与文件、不保留脚本副本、>60 KB 单文件须逐个判。
585 lines
64 KiB
Markdown
585 lines
64 KiB
Markdown
# 交接单 · 退出路径不杀实例(打通 E1 缺口 · 候选 `B`)
|
||
|
||
- **序号**:覆盖网络线 **序 ㉘ · 规划棒 → 单 A**(执行棒待跑)
|
||
- **立单**:2026-09-18 00:5x
|
||
- **用户拍板**:**「选 B」**(2026-09-17 23:3x,原话)—— 即三处 `teardown()` 一起改;同轮另有 **「C 登记为架构目标」**
|
||
- **上游依据**:`交接单_实例逐步拉起_20260917.md` **§8.3**(A/B/C 逐条优缺点;**倾向 B**)|同单 **§3.1**(在册文件集)|同单 **§11**(序 ㉕ 真凶取证)
|
||
- **归档号**:**129**(占号动作已执行,见 §11;⛔ 63 是空号、不补占)
|
||
- **状态**:**待执行**
|
||
|
||
> **本单与 `交接单/README.md §二` 8 段模板的对应**:§1 目标|§2 只读前置|§3 范围|§4 步骤|§5 验收(模板 6)|§6 回滚(模板 7)|**§7 决策点 + 影响面**(模板 5 + 本单要求 ④⑤)|**§8 回报格式(留空给执行棒)**|§9 回头条件|§10 未验证项|§11 指纹与状态。
|
||
|
||
---
|
||
|
||
## §0 摘要 · 口径演变留档 · 三条取证事实
|
||
|
||
### 0.1 摘要(一句话)
|
||
|
||
**把「退出进程即停掉所有在册实例」这一行为去掉** —— 退出进程把存活实例留给下一个进程,回收责任**完全**交给「启动认领(`rehydrateAdoptedScopes()`)+ TCP 探活判孤儿」这一套(序 ㉕ 已实测工作)。**主判据 = 重启 Manager / worker 后,两机 `dsh-*.scope` 计数守恒且认领报告自洽。**
|
||
|
||
### 0.2 🔴 口径演变留档(**必须如实保留**)
|
||
|
||
| 时点 | 用户答 | 落盘口径 |
|
||
|---|---|---|
|
||
| 2026-09-17 23:2x | 第 ① 问「B 和 C 哪个更符合长期主义」⇒「**选B**」;第 ② 问(实操项)⇒「**选 C(实例由独立单元托管)**」 | ①②**指向同一件事(序 ㉕ 的 E1 缺口)而答案相反** ⇒ 主代理按 ②=C 落盘,并**开纠错窗口** |
|
||
| 2026-09-17 23:3x | **行使纠错**:先「**选 B**」、再「**C 登记为架构目标**」 | **最终 = B 先做 + C 登记为架构目标**(= 23:1x 的原建议)。③ 加密档位「选 C 组密钥加密」**未变** |
|
||
|
||
⇒ 🔴 **本单按「B 先执行」出单;`C` 只能作为"架构目标"段落出现(见 §7.2),⛔ 不得按 C 出"实例解耦"的执行单。** 若读到任何旧文本仍写"按 C 执行",**一律以本段为准**。
|
||
|
||
### 0.3 🔬 本棒三条取证事实(**均为本棒现场实测,⛔ 与上游结论冲突时以本棒为准**)
|
||
|
||
1. 🔴 **处② `RemoteSpawner.teardown()` 已经就是 no-op** —— `src/supervisor/remote-spawner.ts:329-331` 与 **HEAD 版逐字相同**(`git show HEAD:…` 已核),函数体只有一行注释:`远端实例的寿命长于任何单个 Manager 副本 ⇒ 由 Manager 的归属/租约管理,不在关闭时清。` ⇒ **用户口径"三处一起改"与"处② 已符合"不矛盾**:三处**都纳入在册范围**、都加"退出不停实例"的**机器断言**,但处② **语义改动量 ≈ 0**(只补守卫/注释固化,防将来被改回去)。
|
||
2. 🔴 **两机当前活跃实例 scope 数 = `47: 0` / `106: 0`**(本棒 00:4x 实测:`ssh -p 22 bt-server` / `ssh -p 22 test106` 各 `systemctl list-units --type=scope --all | grep -c "dsh-"`)⇒ **验收必须先在真实形态夹具上做**(照序 ㉕ 的做法:真 systemd scope + 真 argv + 真监听端口),⛔ **不许拿"0 个实例"当绿**(`0 == 0` 是假绿;见 §5 P1 的反例条款)。
|
||
3. **`idle-reap` 缺省是启用的,且 TTL 很长** —— `src/config.ts:300-302`:`DEFAULT_MAX_IDLE_INSTANCES = 4`|`DEFAULT_INSTANCE_IDLE_TTL_SECONDS = 60*60*24*7`(**7 天**)|`DEFAULT_IDLE_REAP_INTERVAL_SECONDS = 60`;两机 env **均未覆盖**(只读实测:`dshs` 的 `Environment` 里只有 `DSHS_CLUSTER_LEASE_TTL_MS=30000`,无 `IDLE/REAP/TTL`)⇒ 「进程长期不重启时实例由 idle-reap / 用户访问替换来收」这一**前提成立**,但收得很慢(**7 天**)+ 有 **每 host 4 个**的常驻上限(第 5 个会淘汰 LRU)。
|
||
|
||
---
|
||
|
||
## §1 目标
|
||
|
||
**退出路径(SIGTERM → `app.onClose` / `agent.stop()`)不再停任何实例**;重启后实例**仍在册且端口可达**(TCP 判据),由启动认领接管、由 `cleanStaleScopes(uid)` 在用户访问时自然替换。
|
||
|
||
**可判定的"做完了没有"**:§5 的 **P1 + P2 + 对冲项**全绿 —— 即:重启前后 scope 数守恒(**且两侧都 ≥ 1**)+ 认领报告 `adopted + stopped === scanned` 自洽且 `scanned > 0` + 三处 `teardown()` 的"不停实例"断言全绿。
|
||
|
||
---
|
||
|
||
## §2 只读前置(执行前必须先核实,**逐条给命令与期望输出**)
|
||
|
||
> 全部为**只读**;⛔ 不改任何文件、不改任何生产值。**任一条与期望不符 ⇒ 停下报告,不许"先改了再看"。**
|
||
|
||
| # | 核实内容 | 命令 | 期望输出 |
|
||
|---|---|---|---|
|
||
| 1 | 代码仓状态 + 三处 `teardown` 现状 | `cd D:/github/dsh_shenxian && git status --porcelain src/supervisor/ && grep -n -A6 "async teardown" src/supervisor/orchestrator.ts src/supervisor/remote-spawner.ts src/supervisor/leased-spawner.ts` | `git status` = **仅** `M src/supervisor/orchestrator.ts`(序 ㉕ 留下的改动)|① `orchestrator.ts` 体 = `clearInterval(reapTimer)` + **`for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)`**|② `remote-spawner.ts` 体 = **空体(只有注释)**|③ `leased-spawner.ts` 体 = `this.stopHeartbeat()` + `await this.inner.teardown()`。🔴 **若 ① 已变成空体 ⇒ 本单已被别人做过 ⇒ 停下报告。** |
|
||
| 2 | 认领链仍在(⛔ 不许为 0) | `cd D:/github/dsh_shenxian && grep -c "rehydrateAdoptedScopes" src/supervisor/orchestrator.ts; grep -c "cleanStaleScopes" src/supervisor/orchestrator.ts` | `rehydrateAdoptedScopes` **≥ 2**(定义 + 构造函数调用)|`cleanStaleScopes` **≥ 1**。🔴 **`cleanStaleScopes` = 0 ⇒ 停下报告** —— 那是档案 30 本体(spawn 前清同 uid),⛔ 不许为了本单把它去掉。 |
|
||
| 3 | 两机活跃实例数(决定夹具规模) | `ssh -p 22 bt-server 'systemctl list-units --type=scope --all --no-legend \| grep -c "dsh-"'` + `ssh -p 22 test106 '…同上…'` | 本棒读数 = **0 / 0**。⚠️ **为 0 不是"没问题",而是"夹具必须先造"**(见 §4 S1)。 |
|
||
| 4 | 106 worker 侧 idle-reap 是否走缺省 | `ssh -p 22 test106 "systemctl show dshs-worker -p Environment \| tr ' ' '\n' \| grep -iE 'IDLE\|REAP\|TTL'"` | **无输出**(=走代码缺省:60 s 间隔 / 7 d TTL / 4 上限)。⚠️ 若读出 `=0` 的值 ⇒ 记入 §10("106 上没人收实例"缺一环)。 |
|
||
| 5 | 回滚点存在性 + 产物一致性 | `ssh -p 22 bt-server 'ls -d /opt/dsh/backups/* \| tail -3; md5sum /opt/dshs/lib/supervisor/orchestrator.js'`(106 同查 `/opt/dshs-cluster/lib/…`) | 至少一个 `seq*` 目录|两机 `orchestrator.js` md5 **同值**(序 ㉕ 铺的 `5bfb15e00b592c293548ae17b639399b`;⚠️ 若已变 ⇒ 现核取真值再写进 §5/§6 的基线,**⛔ 不写死**)。 |
|
||
| 6 | 基线三件套 + 参数表指纹 | `cd D:/github/dsh_shenxian && npm run build >/dev/null && npm.cmd test 2>&1 \| tail -5`;`node scripts/overlay-probe.cjs --table`;`sed '/^## §10 指纹/,$d' "E:/ProgramData/AI技能/aliyun-dsh-server/参数表_覆盖网络_20260917.md" \| md5sum` | `npm test` = **201 pass / 200 / 0 fail / 1 skip**(序 ㉗ 收口值)|`overlay-probe --table` = **19 PASS / 1 SKIP / 1 FAIL**(1 FAIL = `OBS-21`,**是本线已知在册缺口**、⛔ 非本单引入)|参数表指纹 = **`7fc5889341b99fb26bd05cad0313960b`**(本棒 00:4x 现核)。⛔ **判据一律用"现核值",本单写的是取数时刻的快照。** |
|
||
|
||
---
|
||
|
||
## §3 范围
|
||
|
||
### 3.1 在册文件集
|
||
|
||
| 面 | 文件 | 说明 |
|
||
|---|---|---|
|
||
| 改动 | `src/supervisor/orchestrator.ts` | `LocalSpawner.teardown()`(`:604`)—— **本单唯一语义变动处** |
|
||
| 改动(**授权扩范围**) | `src/supervisor/remote-spawner.ts` | `teardown()`(`:329`)—— 取证已是 no-op;**只补"不停实例"断言/注释固化** |
|
||
| 改动(**授权扩范围**) | `src/supervisor/leased-spawner.ts` | `teardown()`(`:252`)—— 拆开"停心跳"与"停实例"的耦合语义 |
|
||
| 改动 | `参数表_覆盖网络_20260917.md` | §6 新增观测项 `OBS-22`(⛔ 无新生产键) |
|
||
| 改动 | `scripts/overlay-probe.cjs` | 新增 `OBS-22` |
|
||
| 新增 | `test/orchestrator-teardown.test.mjs` | 三处 `teardown()` 的"不停实例"单测(⛔ **不改 `package.json` test 列表结构** —— 那是硬编码文件列表,加进去会改 `npm test` 总数;照序 ㉕ 先例刻意不进) |
|
||
|
||
### 3.2 🔑 扩范围授权依据(**必须写在单里,供第三方复核**)
|
||
|
||
- **用户原话**:「**选 B**」(**2026-09-17 23:3x**)。候选 `B` 的定义原文(`交接单_实例逐步拉起_20260917.md` **§8.3**)即 = "三处一起改:`LocalSpawner.teardown()` + `RemoteSpawner.teardown()` + `LeasedSpawner.teardown()`"。
|
||
- ⇒ `src/supervisor/remote-spawner.ts` 与 `src/supervisor/leased-spawner.ts` **不在** `交接单_实例逐步拉起_20260917.md §3.1` 在册集,**现已获准纳入**。
|
||
- ⚠️ 序 ㉕ 曾按 **R7**(超出在册集)**停下报告**、未动手;**本单解除该阻塞**。⛔ 但**"获准纳入"只覆盖这两个文件** —— 再超 ⇒ 仍按 §9-5 停下报告。
|
||
|
||
### 3.3 ⛔ 不动什么(防顺手扩大)
|
||
|
||
- ⛔ **不许删** `cleanStaleScopes(uid)`(spawn 前清同 uid = **档案 30 本体**)。
|
||
- ⛔ **不许把启动流程改成"什么都不做"**(孤儿会失控)—— `rehydrateAdoptedScopes()` 的扫描/探活/停孤儿三步一步都不能少。
|
||
- ⛔ **不许"重启后重新 spawn 一遍"**(换端口换名字 + 启动风暴)。
|
||
- ⛔ **认领不写回 `mains`**(否则 `enter` 复用分支会等一个**永不出现**的 `launchToken` ⇒ 用户被 503 挡住)。
|
||
- ⛔ **token 不落盘**(改官方 dsh 取 token 违 **R2**)。
|
||
- ⛔ 不改 bwrap 参数(🔴 **47 是 bubblewrap 0.4.0,`--perms` 属 0.5+** ⇒ 会起不来)。
|
||
- ⛔ 不动 `RELAY_FAILOVER_*` / `HB_SEC` / burst;⛔ 不动 relay / 选路 / 内容分发(本单与覆盖网络数据面无关)。
|
||
- ⛔ 不新增公网监听口、不改 `nft` / `nginx`(**R5**)。
|
||
- ⛔ 不 commit / 不 push(**R7**)。
|
||
- ⛔ 不改任何**生产值**(**D1**)。
|
||
|
||
---
|
||
|
||
## §4 步骤(每步自带一次可执行的验证)
|
||
|
||
> 三条硬门贯穿全程:**D1**(不改任何生产值)|**R5**(不新增公网监听口 / 不改 nft·nginx)|**R7**(不 commit / 不 push)。
|
||
|
||
### S0 · 开工对账
|
||
- 跑 §2 全部 6 条并**原样留档**(命令 + 原始输出)。任何一条不符 ⇒ 停下报告。
|
||
- 抢全局执行锁:`bash "D:/github/dsh_shenxian/dsh-server-docs/scripts/handoff-guard.sh" --claim-exec "覆盖网络线-序28单A执行棒"`;**抢不到 = 有会话在跑 ⇒ 只报告并停**(**R9**:⛔ 不删锁 / 不接管 / 不等重试)。
|
||
- **验证**:guard 返回「✓ 已持全局执行锁」。
|
||
|
||
### S1 · 造**真实形态**夹具(⚠️ 因为两机活跃实例 = 0,本步不可省)
|
||
- 照序 ㉕ 的做法:造一个**真 systemd scope + 真 argv + 真监听端口**的既有实例(`systemd-run --scope` 起一个监听 `127.0.0.1:<区内端口>` 的进程,`--chdir <dataRoot>/users/<uid>/…`,`-u dsh-<uid>-<hex>.scope`)。
|
||
- **验证**:`ssh -p 22 bt-server 'systemctl list-units --type=scope --all | grep -c "dsh-"'` ⇒ **≥ 1**;`ss -lntp 'sport = :<端口>'` ⇒ 有监听。
|
||
- ⚠️ 夹具**必须是真实形态** —— 序 ㉕ 已实证:假夹具(只有 unit 名、无真进程)会让"认领逻辑工作"这个结论**站不住**。
|
||
|
||
### S2 · 逐处最小改动(三处,⛔ 顺序无关但必须三处都动)
|
||
|
||
| 处 | 位置 | 改前语义 | 改后语义(**最小差异**) |
|
||
|---|---|---|---|
|
||
| ① | `orchestrator.ts` `LocalSpawner.teardown()` `:604` | `clearInterval(reapTimer)` + **`for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)`** | 保留 `clearInterval(reapTimer)`;**删掉那行循环**,改为不做任何实例处置。注释写明:回收责任移交「启动认领 + TCP 探活判孤儿」+ `cleanStaleScopes(uid)`(访问时替换)+ idle-reap(缺省 60 s / 7 d / 4 上限)。 |
|
||
| ② | `remote-spawner.ts` `:329` | 空体(只有一行注释) | **语义不变**;补一句断言式注释 + (可选)在函数体内加一行"可被 grep 的守卫标记",供 §5 对冲项的静态断言使用。 |
|
||
| ③ | `leased-spawner.ts` `:252` | `this.stopHeartbeat()` + `await this.inner.teardown()` | **拆开两件事**:`stopHeartbeat()` **保留**(心跳停 ⇒ 租约按 TTL 过期,这是"进程不在就别再续租"的正确语义);`await this.inner.teardown()` **保留**(它只是转发,inner = `RemoteSpawner` ⇒ no-op)。⇒ **净改动 ≈ 注释 + 守卫标记**;⛔ **严禁在此处向 worker 下发 `POST /stop`**(今天没有,将来也不许)。 |
|
||
|
||
- **验证**:`grep -n -A6 "async teardown"` 三处,人工逐字比对"改后 ≠ 改前"仅限上表差异(处②/③ 应为"语义不变")。
|
||
- ⚠️ 处① 的 `this.mains.delete/watchdogs.delete/lastActive.delete` **不在 `teardown()` 里**(`teardown()` 只调 `stop()`)—— `stop()` 内部会做清理,**`stop()` 本身一行不动**(它仍是"用户显式停实例"的正路,被路由层调用)。
|
||
|
||
### S3 · 单测(**先红后绿**)
|
||
- 新增 `test/orchestrator-teardown.test.mjs`,三组断言:
|
||
- ① `LocalSpawner.teardown()` 后,假 `spawnInstance` 造出的实例**未被停**(`stop` 桩调用计数 **= 0**);
|
||
- ② `RemoteSpawner.teardown()` 对 HTTP 桩的下发计数 **= 0**;
|
||
- ③ `LeasedSpawner.teardown()` 后:**心跳停**(计数 **= 1**)∧ 实例停(计数 **= 0**)。
|
||
- **先红**:临时把三处改回原语义 ⇒ **3 红**(逐个点名是哪一处)⇒ 还原 ⇒ **全绿**、md5 逐字一致。
|
||
- 运行:`node --test test/orchestrator-teardown.test.mjs`(rc=0)。
|
||
- ⛔ **不改 `package.json`**(硬编码文件列表,加进去会改 `npm test` 总数)。
|
||
|
||
### S4 · 真机先红(新判据对旧生产)
|
||
- 在**未部署新 lib** 的生产上跑新增的 `OBS-22` ⇒ 期望 **FAIL**,并打印可区分证据(见 §5)。
|
||
- **验证**:探针 rc=1 且 FAIL 点名 `OBS-22`。
|
||
|
||
### S5 · 部署 + 真机后绿
|
||
- `npm run build` ⇒ 铺 `lib/supervisor/{orchestrator,remote-spawner,leased-spawner}.js` 到**四处**:47 `/opt/dshs/lib/supervisor/`、106 `/opt/dshs-cluster/lib/supervisor/`(⚠️ 若 relay 侧也有同名副本则一并核;本单不涉及 relay)。
|
||
- 回滚点:`/opt/dsh/backups/seq28-<ts>/`(先备份旧 `.js`,md5 记录)。
|
||
- 重启:47 `systemctl restart dshs`、106 `systemctl restart dshs-worker`(R8:开发环境服务器,动手前一句话说明在动什么)。
|
||
- **验证**:`md5sum` 各副本同值;重启后 §2-3 的单元 `is-active` 全 `active`。
|
||
|
||
### S6 · 主判据(P1 / P2)+ 对冲项
|
||
- 按 §5 执行 P1(scope 数守恒,**两侧都 ≥ 1**)、P2(认领报告自洽且 `scanned > 0`)、对冲项(静态 + 动态 + `OBS-22`)。
|
||
- **验证**:三项全绿且 `OBS-22` PASS;P1 成立而 P2 不成立时**必须点名**(见 §5 的"分层判定")。
|
||
|
||
### S7 · 零回归 + 收口
|
||
- `npm.cmd test` ⇒ **不退化**(§2-6 现核基线)|`node scripts/overlay-probe.cjs --scene all --table` ⇒ **12 PASS / 0 SKIP / 0 FAIL**|`node scripts/overlay-probe.cjs --table` ⇒ 基线 + 本棒新增 1 行(`OBS-21` 仍 FAIL = 线内在册缺口,**非本单引入**,报告里点名)。
|
||
- 清理夹具(S1 造的 scope 全部 `systemctl stop` + 撤销)——⚠️ **夹具不得留在生产**。
|
||
- 回填本单 **§8**;推进 `接续入口_覆盖网络线_20260916.md` §0 + §2;写工作区日志(append-only)。
|
||
|
||
---
|
||
|
||
## §5 验收判据(**可机器断言、可被第三方复现**)
|
||
|
||
> 判据分三层:**P0 前提 → P1 主判据 → P2 回收链**。🔴 **P1 与 P2 必须分开报** —— 这正是序 ㉗ 把「判据成立」与「冗余/链路建成」分开的同一手法:**P1 绿而 P2 红** 是一个**合法且重要**的结果,⛔ 不得合并成"全绿"。
|
||
|
||
### P0 · 前提(夹具就位)
|
||
```bash
|
||
ssh -p 22 bt-server 'systemctl list-units --type=scope --all --no-legend | grep -c "dsh-"'
|
||
```
|
||
- **期望**:**≥ 1**。`0` ⇒ **P0 不成立 ⇒ 停下来造夹具**(§4 S1),⛔ 不许直接跑 P1(`0` 会让判据两边都是 0)。
|
||
|
||
### P1 · 主判据(**退出不再杀实例**)
|
||
```bash
|
||
N_BEFORE=$(ssh -p 22 bt-server 'systemctl list-units --type=scope --all --no-legend | grep -c "dsh-"')
|
||
ssh -p 22 bt-server 'systemctl restart dshs'
|
||
sleep $(( N_BEFORE * 1 + 5 )) # 认领节流 500ms/条 + 单条探活 2000ms ⇒ 留足余量
|
||
N_AFTER=$(ssh -p 22 bt-server 'systemctl list-units --type=scope --all --no-legend | grep -c "dsh-"')
|
||
echo "before=$N_BEFORE after=$N_AFTER"
|
||
```
|
||
- **判据**:**`N_AFTER == N_BEFORE`**(rc=0 **且** `N_BEFORE ≥ 1`)。
|
||
- 🔴 **反例条款(防空绿)**:`N_BEFORE == 0 && N_AFTER == 0` ⇒ **判 FAIL**(不是 PASS)。判据必须能区分"守恒"与"两边都空"。
|
||
- 🔴 **反向夹具(证明判据真的在测这件事)**:把处① 临时改回原语义(或用一个 `DSHS_TEARDOWN_STOP=1` 的**测试专用**接线 —— ⛔ 该接线**只进单测、不许进生产 env**)⇒ 重跑 ⇒ **`N_AFTER == 0` ⇒ 判据必须变红**。**先红后绿**两腿都要留原文。
|
||
- **同一判据在 106 上复跑一遍**(`ssh -p 22 test106 '…restart dshs-worker…'`)—— ⚠️ **两机都要绿**:47 走的是 `dshs`(Manager)的 `LocalSpawner`/`LeasedSpawner→RemoteSpawner`,106 走的是 `dshs-worker`(`agent.ts:143` 的 `LocalSpawner`),**同一语义三份实现 ⇒ 必须逐台断言**。
|
||
|
||
### P2 · 回收链是否真建成(**与 P1 分开**)
|
||
```bash
|
||
E=$(date -u +%s)
|
||
ssh -p 22 bt-server "journalctl -u dshs --since @$E --no-pager 2>/dev/null | grep -c '\[rehydrate\]'"
|
||
ssh -p 22 bt-server "journalctl -u dshs --since @$E --no-pager 2>/dev/null | grep -o '\[rehydrate\] summary .*'"
|
||
```
|
||
- **期望**:① 行数 **≥ 1**;② summary 满足 **`adopted + stopped === scanned`** 且 **`scanned > 0`**;③ 有 `[rehydrate] adopted …`(含 `scope=` / `uid=` / `port=`)+ `[rehydrate] probe OK … :<port>`。
|
||
- 🔴 **分层判定(本单最重要的一条)**:
|
||
- `scanned > 0` ∧ `adopted ≥ 1` ∧ `probeOk ≥ 1` ∧ 自洽 ⇒ **P1 + P2 双绿**。
|
||
- `scanned == 0` ∧ `N_AFTER == N_BEFORE > 0` ⇒ **P1 绿、P2 红** ⇒ 报告里**必须点名**:"**实例没被杀(P1 成立),但认领没扫到存量 scope(回收链未建成)**",⛔ **不得写"全绿"**。
|
||
- `scanned > 0` ∧ `stopped > 0` ⇒ 逐条读 `[rehydrate] ⛔ …` 原因行,确认"按旧行为停掉"的是**端口不通**的条目(判据口径 = **孤儿 = 端口不通**,⛔ 不是"启动了却不认识")。
|
||
- ⚠️ **journalctl 必须用 `--since @<epoch>`**(不吃 `date -Is` 的时区偏移),且**"查询失败"必须与"确无该行"可分**:把输出的**原文字节数**一并打印(`0 字节` = 读取失败/`>0` = 真没有该行)。
|
||
|
||
### 对冲项 · "同一语义三份实现"的自动化对冲(**B 的专属风险**)
|
||
- **静态**:
|
||
```bash
|
||
cd D:/github/dsh_shenxian
|
||
grep -n -A6 'async teardown' src/supervisor/orchestrator.ts src/supervisor/remote-spawner.ts src/supervisor/leased-spawner.ts \
|
||
| grep -nE 'this\.stop\(|inner\.stop\(|killInstance\(|/stop'
|
||
```
|
||
**期望 = 无命中**(rc=1)。🔴 **任何一处仍在 `teardown()` 里停实例 / 向远端下发停止 ⇒ 判据判红**。
|
||
- **动态**:`test/orchestrator-teardown.test.mjs` 三组断言全绿(§4 S3)。
|
||
- **探针**:新增 **`OBS-22`**,**三判据**:
|
||
① 重启前后 scope 数守恒(`N_AFTER == N_BEFORE ≥ 1`);
|
||
② `adopted + stopped === scanned` ∧ `scanned > 0`;
|
||
③ 三处 `teardown()` 的静态守卫(上条 grep 无命中)。
|
||
⛔ **缺行即 FAIL**,并把 `journalctl` 的**原文字节数**一并打出(`0` = 读取失败 vs `>0` = 真无该行,二者必须可分)。
|
||
⚠️ **编号须现核顺延**:`grep -oE 'OBS-[0-9]+' scripts/overlay-probe.cjs | sort -u | tail -1`(本单立单时 = `OBS-21`)—— 若已被占用则顺延(序 ㉖ 踩过"编号被前序占用")。
|
||
|
||
### 判据自证表(**必须原样回填 §8**)
|
||
| 层 | 判据 | 命令 | 期望 | 实测 |
|
||
|---|---|---|---|---|
|
||
| P0 | 夹具就位 | §5 P0 | ≥ 1 | 待填 |
|
||
| P1 | 47 scope 守恒 | §5 P1 | `N_AFTER == N_BEFORE ≥ 1` | 待填 |
|
||
| P1 | 106 scope 守恒 | §5 P1(test106) | 同上 | 待填 |
|
||
| P1 | 反向夹具变红 | 临时还原处① 后重跑 | `N_AFTER == 0` | 待填 |
|
||
| P2 | 认领行存在 | §5 P2① | ≥ 1 | 待填 |
|
||
| P2 | 报告自洽 | §5 P2② | `adopted+stopped==scanned ∧ scanned>0` | 待填 |
|
||
| 对冲 | 静态守卫 | §5 对冲-静态 | 无命中 | 待填 |
|
||
| 对冲 | 动态单测 | `node --test test/orchestrator-teardown.test.mjs` | rc=0 | 待填 |
|
||
| 对冲 | `OBS-22` | `node scripts/overlay-probe.cjs --table` | PASS | 待填 |
|
||
| 零回归 | `npm test` | `npm.cmd test` | 不退化(现核基线) | 待填 |
|
||
| 零回归 | 场景 | `--scene all --table` | 12 PASS / 0 SKIP / 0 FAIL | 待填 |
|
||
| 零回归 | 探针 | `--table` | 基线 +1 行 | 待填 |
|
||
| D1 | 生产值未改 | `grep -c COOLDOWN_MS=0 <演练/夹具/回滚路径文件>` | **0** | 待填 |
|
||
|
||
---
|
||
|
||
## §6 回滚
|
||
|
||
**回滚 = 恢复三处 `teardown` 原语义 ⇒ 秒级。** 两条路,任选其一(**优先路 A**:最干净)。
|
||
|
||
### 路 A · 语义回滚(推荐,分钟级)
|
||
```bash
|
||
cd D:/github/dsh_shenxian
|
||
# ① 三处改回原语义(处① 恢复那行循环;处②/③ 恢复为空体/原写法)
|
||
git diff src/supervisor/ # 人工核对只有这三处
|
||
npm run build
|
||
scp -P 22 lib/supervisor/orchestrator.js bt-server:/opt/dshs/lib/supervisor/
|
||
scp -P 22 lib/supervisor/remote-spawner.js bt-server:/opt/dshs/lib/supervisor/
|
||
scp -P 22 lib/supervisor/leased-spawner.js bt-server:/opt/dshs/lib/supervisor/
|
||
scp -P 22 lib/supervisor/*.js test106:/opt/dshs-cluster/lib/supervisor/
|
||
ssh -p 22 bt-server 'systemctl restart dshs'
|
||
ssh -p 22 test106 'systemctl restart dshs-worker'
|
||
```
|
||
- **验收**:重启后 `dsh-*.scope` 计数 **回落到 0**(= 恢复"退出即收")⇒ 这就是"回滚生效"的机器判据。
|
||
- ⚠️ 回滚**会带走存活实例**(这正是原语义)⇒ **回滚前先看有没有在线用户**。
|
||
|
||
### 路 B · 产物回滚(秒级,最省事)
|
||
```bash
|
||
ssh -p 22 bt-server 'cp /opt/dsh/backups/seq28-<ts>/{orchestrator,remote-spawner,leased-spawner}.js /opt/dshs/lib/supervisor/ && systemctl restart dshs'
|
||
ssh -p 22 test106 'cp /opt/dsh/backups/seq28-<ts>/*.js /opt/dshs-cluster/lib/supervisor/ && systemctl restart dshs-worker'
|
||
```
|
||
- **回滚点**:`/opt/dsh/backups/seq28-<ts>/`(**S5 落盘时创建**,旧 md5 一并记录在 §8)。
|
||
- ⚠️ 路 B 必须有 `seq28-<ts>` 存在;**没有回滚点不准上线**。
|
||
|
||
### 🔴 回滚路径硬禁令
|
||
⛔ **不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进任何回滚 / 演练 / 夹具路径** —— 它会**自锁**(候选链卡在第一个失败候选上,实测 **121–123 s 无切换**);判定见 §9-1。
|
||
|
||
---
|
||
|
||
## §7 决策点 · 影响面与关系
|
||
|
||
### 7.1 决策点
|
||
- **已定(用户,2026-09-17 23:3x)**:执行候选 **`B`**(三处同改)|`C` **登记为架构目标、本阶段不进执行**。
|
||
- **已定(本棒,可推翻)**:
|
||
- 处② 取证已是 no-op ⇒ **只补断言/注释固化**,不做语义改动(依据 = §0.3-1,`git show HEAD:` 逐字比对)。
|
||
- 处③ 保留 `stopHeartbeat()`(租约语义正确),**只把"停实例"剥离**。
|
||
- 探针编号 = `OBS-22`(现核顺延)。
|
||
- **待定 = 空**。⛔ 本单无上抛项;执行棒若遇边界外事项(业务优先级 / 花钱 / 合规 / 凭据 / 影响面超出本平台)⇒ 停下报告。
|
||
|
||
### 7.2 🔑 架构目标 `C`(systemd scope 单一真相源)—— **登记,⛔ 不进本阶段执行**
|
||
|
||
**是什么**:把"实例归属"从 **Manager 进程内存**(`mains` / `watchdogs` Map)搬到 **systemd scope 这一单一真相源** ⇒ Manager 退化为"发现 + 代理 + 记账";Manager 重启**天然**不影响实例,不再需要"退出路径不收 + 启动认领"这对补丁。
|
||
|
||
**为什么现在不做**(三条,客观):
|
||
1. **收益随规模兑现,当前规模收益≈0**:47 只有 **0–2** 个实例(本棒实测活跃 scope = 0/0),"归属不一致"的痛点还没出现。
|
||
2. **106 的 provisioner 尚未铺** ⇒ C 的收益(大规模自动编排)在 106 上无处落地。
|
||
3. **改动面最大**:C 要动 spawn 路径 + 归属存储 + 路由层,比 B(3 个文件)大一个量级,且**与"进程退出"这条路径完全解耦后**才能真正验证。
|
||
|
||
**什么信号出现才回头做 C**(**任一命中即回头**):
|
||
- 实例数 **≥ 阈值**(建议先与本线参数表对齐,例如每 host 常驻实例数 ≥ 8,即 ≥ 2× 缺省 `maxIdleInstances=4`);
|
||
- 出现**第二种 spawner**(今天的实现形态扩到第 4 类 ⇒ "同一语义 N 份实现"的对冲成本超过 C 的一次性成本);
|
||
- 出现"**进程被 SIGKILL 后无人回收**"的**实测案例**(B 的认领链只在**启动时**扫,SIGKILL 后若不重启 → 实例长期无人管;今天靠 idle-reap 兜,但 TTL = **7 天**)。
|
||
|
||
**从 B 迁移到 C 的路径**(顺序,⛔ 不要跳跃):
|
||
1. 本单(B)先落 ⇒ 拿到"退出不停实例 + 启动认领"这条**已验证**的容错链(= C 的**安全网**);
|
||
2. 再把"实例归属"抽成**可替换的后端**(内存 Map ↔ systemd scope 查询),**先并存**、以 B 的行为为基线做对照;
|
||
3. 最后把 Manager 的 `mains` 降级为**缓存**,归属判定只认 scope;此时 **B 的"退出不停实例"就是天然行为**(不再是补丁),可回头**删掉** `teardown()` 的守卫断言;
|
||
4. 每一步都保留 B 的回滚路径(§6)作为兜底。
|
||
|
||
### 7.3 影响面与前提(**必须接受,否则不要做 B**)
|
||
- 🔴 **影响面 = 所有实例的生命周期语义**:实例**不再随进程退出而回收**。
|
||
- 🔴 **前提(明写)**:**进程长期不重启时,实例由 idle-reap / 用户访问替换来收** —— 本棒已量化:`maxIdleInstances = 4`(每 host)+ `instanceIdleTtlSeconds = 604800`(**7 天**)+ `idleReapIntervalSeconds = 60`(`src/config.ts:300-302`,两机 env 未覆盖 ⇒ 走缺省)。**即:最坏情形下实例会占着端口/内存最长 7 天,或被"第 5 个实例"挤掉。**
|
||
- ⚠️ **106 的 provisioner 尚未铺** —— 本单**不依赖**它:106 侧同样走"`dshs-worker` 退出不停实例 + 启动认领",idle-reap 也走同一份代码缺省。⚠️ 但"**实例长期不重启时谁来收**"在 106 上只靠 idle-reap(7 天),**记入 §10-3**。
|
||
|
||
### 7.4 与 lease / 配额记账 / 端口分配 / `dsh_hosts` / provisioner 的关系
|
||
|
||
| 对象 | 本单下的影响 | 处置 |
|
||
|---|---|---|
|
||
| **lease**(`src/supervisor/lease.ts`) | `LeasedSpawner.teardown()` 仍 `stopHeartbeat()` ⇒ 租约按 TTL(`DSHS_CLUSTER_LEASE_TTL_MS=30000`,47 实测)过期 ⇒ 归属记录会与"仍在跑的实例"**不一致**。🔴 这是 B 的**真实新增风险**。 | 靠现有的两道兜住:`cleanStaleScopes(uid)`(spawn 前清同 uid ⇒ 防双实例共 profile)+ 认领时的**探活**。⚠️ **未实测** ⇒ §10-1 + §9-6 回头条件。 |
|
||
| **配额记账**(`dsh_hosts.used_mb` / 心跳) | Manager 重启后心跳中断 ⇒ host 可能被短暂判离线,但实例仍在 ⇒ 记账**漂移**。worker 心跳恢复后**重报**(`used_mb` 由 worker 本地核算)⇒ 应自愈。 | §10-2 未实测。 |
|
||
| **端口分配**(`--base` / `--span`,`src/supervisor/spawn.ts:79/106` 的 `findFreePortInRange` / `findInstancePort`) | 🔴 **端口不再随进程退出而释放** ⇒ 区间**占用更久**、`findFreePortInRange` 耗尽风险 **↑**。 | 替换路径(`cleanStaleScopes(uid)` → 换端口换 token)会释放旧端口。⚠️ 耗尽会**抛** `实例端口区间已耗尽` ⇒ §9-3 回头条件(⛔ 不许私自扩 `--span`)。 |
|
||
| **`dsh_hosts` 表** | **不受影响** —— 该表记 host/endpoint/`via`/容量,与实例生命周期无关(`src/db/pg.ts:605`)。 | 无动作。 |
|
||
| **provisioner**(106 未铺) | **不阻塞本单**(见 §7.3)。 | 无动作;若 provisioner 落地后出现"自动铺 ⇒ 自动起",须回头核 §9-6。 |
|
||
|
||
---
|
||
|
||
## §8 回报格式(**留空给执行棒**)
|
||
|
||
> 执行棒**逐节回填**,每节必须给**命令 + 原始输出(或原文摘录)+ 判定**。⛔ 不许只写结论。
|
||
|
||
### 8.1 开工对账(§2 六条)
|
||
|
||
**抢锁**:`✓ 已持全局执行锁(覆盖网络线-序30执行棒-单A)` ✅
|
||
|
||
| # | 核实内容 | 实测(2026-09-18 00:5x) | 判定 |
|
||
|---|---|---|---|
|
||
| 0 | 本单 **§8 前前缀** | `sed '/^## §8 回报格式/,$d' <本单> \| md5sum` = **`c9aeac82381f569fc8b9effad0fefa99`** | ✅ 与单里记的一致 |
|
||
| 1 | `git status --porcelain src/supervisor/` | **仅** ` M src/supervisor/orchestrator.ts`(序㉕ 留下的改动) | ✅ 与期望一致 |
|
||
| 1 | 三处 `teardown()` 现状(**改前原文**) | ① `orchestrator.ts:604` = `if (this.reapTimer !== undefined) clearInterval(this.reapTimer)` + `for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)`|② `remote-spawner.ts:329` = **空体,只有一行注释**(`远端实例的寿命长于任何单个 Manager 副本 ⇒ …`)|③ `leased-spawner.ts:252` = `this.stopHeartbeat()` + `await this.inner.teardown()` | ✅ **逐字与 §2 期望一致** ⇒ ⛔ 处① 未被别人做过;处② 的 no-op 取证(§0.3-1)**复核成立** |
|
||
| 1 | HEAD | `04776af` | ✅ |
|
||
| 2 | 认领链计数 | `grep -c rehydrateAdoptedScopes src/supervisor/orchestrator.ts` = **2**(≥2)|`grep -c cleanStaleScopes` = **4**(≥1) | ✅ ⛔ 均非 0 |
|
||
| 3 | 两机活跃实例数 | 47 = **0** / 106 = **0** | ✅ 与期望一致 ⇒ **S1 造夹具不可省** |
|
||
| 4 | 106 worker 侧 idle-reap 是否走缺省 | `ssh -p 22 test106 "systemctl show dshs-worker -p Environment \| tr ' ' '\n' \| grep -iE 'IDLE\|REAP\|TTL'"` ⇒ **无输出**(rc=1) | ✅ 走代码缺省(60 s / 7 d / 4 上限) |
|
||
| 5 | 回滚点存在性 + 产物一致性 | 两机 `/opt/dsh/backups/` 下 `seq25-…` / `seq26-…` / `seq27-20260918-000259` 均在册;两机 `lib/supervisor/orchestrator.js` md5 **同值** = **`5bfb15e00b592c293548ae17b639399b`**(=序㉕ 铺的值,**未变** ⇒ 未写死) | ✅ |
|
||
| 6 | 基线三件套 + 参数表指纹 | `npm.cmd test` = **201 tests / 200 pass / 0 fail / 1 skip**(=序㉗ 收口值)|参数表指纹(§10 前截断) = **`7fc5889341b99fb26bd05cad0313960b`**|`overlay-probe --table` 基线 = **19 PASS / 1 SKIP / 1 FAIL**(1 FAIL = `OBS-21`,线内在册缺口)|`grep -oE 'OBS-[0-9]+' scripts/overlay-probe.cjs \| sort -u \| tail -1` = **`OBS-21`** ⇒ 新项编号 = **`OBS-22`** | ✅ 全部与期望一致(编号无须顺延) |
|
||
|
||
**S0 判定 = 六条全符合期望 ⇒ 开工。零不符、零停下。**
|
||
|
||
### 8.2 S1 夹具(真实形态)—— 🔴 **方法修正:用「平台真路径 spawn」,⛔ 不用合成 scope**
|
||
|
||
**为什么改方法(技术自决,可推翻 ⇒ 附理由与证据)**:单 §4 S1 原写「用 `systemd-run --scope` 造一个真 argv 的既有实例」。**开工后取证发现该夹具不可判别**:`LocalSpawner.teardown()` 的旧语义是 `for (user of mains.keys()) await this.stop(user)` —— **只停"自己在册(`mains`)"的实例**,而合成 scope **不在 `mains` 里** ⇒ 旧代码下它也**不会被杀** ⇒ `N_BEFORE == N_AFTER ≥ 1` 在**改前改后都成立** ⇒ 判据恒绿(**假绿**),且 §5「反向夹具必须变红」**无法达成**。
|
||
⇒ 改为**用平台真实路径 spawn**(`mksess` 临时会话 + `POST /api/dsh/enter`,即 R4 规定的实例面手段)⇒ 得到的实例**真 systemd scope + 真 argv + 真监听端口 + 真被 worker 的 `mains` 追踪** —— 比合成 scope **严格更强**,且**使 P1 具备判别力**(见 8.5 真机先红:旧生产下实测 `1 → 0`)。
|
||
|
||
**夹具记录**:
|
||
|
||
| 项 | 47(w-47 · 用户 `admin` / uid 114801) | 106(w-106 · 用户 `guest` / uid 100002) |
|
||
|---|---|---|
|
||
| scope 名(首次) | `dsh-114801-e9bc4a4e.scope` | `dsh-100002-70a193b7.scope` |
|
||
| 实例端口 | `20000`(本机在听:`ss -lntp` 有 `127.0.0.1:20000`) | `21000` |
|
||
| 真实 argv(摘) | `--chdir /var/lib/dshs/users/cce6d1cd-…/ws -- setpriv --reuid 114801 --regid 114801 --clear-groups /usr/local/bin/dsh --profile web --host 127.0.0.1 --port 20000` | `--chdir /var/lib/dshs/users/4092b965-…/ws -- setpriv --reuid 100002 … --port 21000` |
|
||
| 起止 | 起 00:56:5x(`enter` 200)|RED 腿 00:58:40 被杀|GREEN 腿 01:0x 重建 | 起 00:57:0x(`enter` 200)|RED 腿 00:58:48 被杀|GREEN 腿 01:0x 重建 |
|
||
| `enter` 原文 | `{"kind":"session","instance":{"id":"a3f6076e-…","port":20000,"status":"running"},"url":"https://admin.alotbuy.com/"}` | `{"kind":"session","instance":{"id":"818a0a27-…","port":21000,"status":"running"}}` |
|
||
|
||
⚠️ **一处如实留白**:`ssh -p 22 bt-server 'enter 时 Host: guest.alotbuy.com ⇒ http=404 not found'` —— guest 子域用**主域 Host** 才路由到 `/api/dsh/enter`(admin 同)。这是**探针/验收脚本的 Host 选择问题,⛔ 不是平台缺陷**(会话本身已由 `sid` cookie 认定,换 Host 后即 200)。
|
||
|
||
### 8.3 三处改动(改前 / 改后原文)
|
||
|
||
| 处 | 文件 | 改前 | 改后 | 语义变动量 |
|
||
|---|---|---|---|---|
|
||
| ① | `src/supervisor/orchestrator.ts` `LocalSpawner.teardown()` | `clearInterval(reapTimer)` + **`for (…) await this.stop(userId)`**(逐个杀在册实例) | `clearInterval(reapTimer)`;**循环整行删除**;补 17 行注释(回收责任移交三条:`rehydrateAdoptedScopes` / `cleanStaleScopes(uid)` / idle-reap)+ 守卫标记 `// ⛔ 退出不停实例(guard: teardown-must-not-stop-instances)` | 🔴 **真改**(本单唯一语义变动处) |
|
||
| ② | `src/supervisor/remote-spawner.ts` `teardown()` | 空体(一行注释) | 空体(`guard:` 守卫标记 + 原注释 + 5 行"取证已是 no-op / 只固化断言"说明) | **≈0(语义不变,只补守卫)** |
|
||
| ③ | `src/supervisor/leased-spawner.ts` `teardown()` | `stopHeartbeat()` + `await this.inner.teardown()` | **两行都保留**(拆开"停心跳"与"停实例"的耦合语义)+ 守卫标记 + 8 行说明(含"心跳停 ⇒ 租约过期 ⇒ 归属记录与实际不一致"这条如实账) | **≈0(语义不变,只补注释/标记)** |
|
||
|
||
**改动前后逐字核对**:`grep -n -A7 "async teardown"` 三处,差异**仅限上表**(处①删 1 行;处②/③ 增注释与标记)⇒ ✅ 未越界(⛔ 未碰 `stop()`、未碰 `reapOnce`、未碰认领链)。
|
||
**`stop()` 仍一行未动**(它仍是"用户显式停实例"的正路,被路由层调用)。
|
||
|
||
### 8.4 单测先红后绿(原文级)
|
||
|
||
新增 `test/orchestrator-teardown.test.mjs`(9 用例;**刻意不进 `npm test`** —— `package.json` 的 `test` 是硬编码文件列表,加进去会改测试总数,照 序㉕ 先例)。
|
||
**运行**:`node --test test/orchestrator-teardown.test.mjs`
|
||
|
||
**红腿(临时把三处改回原语义)** ⇒ `# tests 9 / # pass 6 / # fail 3`:
|
||
|
||
```
|
||
not ok 1 - T1 ⛔ LocalSpawner.teardown() 不得停任何在册实例(stop 桩计数 = 0)
|
||
error: |- expected: 0 actual: 2 operator: 'strictEqual'
|
||
not ok 5 - T5 ⛔ 三处 teardown() 体内均无停实例 / 下发停止(静态守卫,≡ 交接单 §5 对冲项)
|
||
actual: [ 'orchestrator.ts: for (const userId of [...this.mains.keys(), ...this.watchdogs.keys()]) await this.stop(userId)' ]
|
||
not ok 6 - T6 守卫标记在位(三处都带 guard: teardown-must-not-stop-instances,防被静默改回去)
|
||
以下文件的 teardown() 缺守卫标记(逐处点名)⇒ orchestrator.ts / remote-spawner.ts / leased-spawner.ts
|
||
```
|
||
|
||
⇒ 🔴 **3 红且逐处点名**(T1 动态 = `stop` 桩实际被调 **2** 次;T5/T6 静态 = 逐文件点名)。
|
||
**同时如实标注**:`T4`(处③ 的动态断言)改前改后**都是绿的** —— 因为处③ 的 `inner` 是 `RemoteSpawner`,**它的 `teardown()` 取证本来就是 no-op**(§0.3-1)⇒ 候选 `B` 的**唯一语义变动处是 ①**,②/③ 是"语义固化 + 机器断言"、⛔ 不是"修 bug"。**这条不掩饰,已写进单测文件头。**
|
||
|
||
**绿腿(还原后)** ⇒ `# tests 9 / # pass 9 / # fail 0`(rc=0);**还原为字节级**:三文件 md5 与改动后**逐个相同** —— `orchestrator.ts c831bc75f6636603411949d43eb84136` / `remote-spawner.ts 1279b948c4e2dd6ca021acb5b9fd6f27` / `leased-spawner.ts f891797ba8a7ee07d9fdca0d83f07461`。
|
||
|
||
### 8.5 真机先红(新判据对旧生产)+ 🔴 两种判别器并列
|
||
|
||
**(甲)P1 判据对旧生产 ⇒ 必须 FAIL(判别力证明)**:**未部署**新 lib 时(生产 orchestrator.js = `5bfb15e0…`,旧语义),带**真实例夹具**跑 `restart dshs-worker`:
|
||
|
||
```
|
||
############ 47 ############ (epoch=1789664319)
|
||
N_BEFORE=1 --- restart 前 scope 明细 --- dsh-114801-e9bc4a4e.scope
|
||
N_AFTER=0 --- restart 后 scope 明细 --- (none)
|
||
--- rehydrate 日志(journalctl --since @1789664319) --- RAW_BYTES=125
|
||
Sep 18 00:58:40 iZrj99af19cibck1ge93tqZ node[826743]: [rehydrate] 无既有实例 scope ⇒ 不动作(与旧行为等价)
|
||
############ 106 ############ (epoch=1789664328)
|
||
N_BEFORE=1 --- restart 前 scope 明细 --- dsh-100002-70a193b7.scope
|
||
N_AFTER=0 --- restart 后 scope 明细 --- (none)
|
||
--- rehydrate 日志(journalctl --since @1789664328) --- RAW_BYTES=120
|
||
Sep 18 00:58:48 VM-0-8-opencloudos node[258630]: [rehydrate] 无既有实例 scope ⇒ 不动作(与旧行为等价)
|
||
```
|
||
|
||
⇒ **两机 `1 → 0`** ⇒ 判据 `N_AFTER == N_BEFORE ≥ 1` 判 **FAIL** ✅(这就是**真机反向夹具**:判据确实在测这件事,⛔ 不是恒绿)。同时复现了序㉕ 的真凶现场。
|
||
|
||
**(乙)新 `OBS-22` 对旧产物 ⇒ 必须 FAIL 并逐处点名**(夹具腿,夹具内容 = **真实生产产物字节**:旧 = 回滚点 `/opt/dsh/backups/seq28-20260918-005909/`,新 = 已部署):
|
||
|
||
```
|
||
⚠️ FIXTURE FAIL OBS-22 …:47 ❌ 缺标记(orchestrator,remote-spawner,leased-spawner) 停实例命中 orchestrator「await this.stop(userId);」
|
||
|106 ❌ 缺标记(orchestrator,remote-spawner,leased-spawner) 停实例命中 orchestrator「await this.stop(userId);」
|
||
⚠️ FIXTURE PASS OBS-22 …:47 静态守卫 ✓|106 静态守卫 ✓
|
||
```
|
||
|
||
⇒ **红腿 FAIL 并逐处点名** ✅|**绿腿 PASS** ✅。
|
||
⚠️ **可区分性证据(本线的老坑)**:红腿那次运行的整体输出是 `❌ 8 项红:OBS-11 , OBS-13 , OBS-14 , OBS-16 , OBS-17 , OBS-19 , OBS-20 , OBS-22` —— 前 7 项是**夹具模式未给对应夹具**的固有 FAIL(⛔ 不是判据结果),只有 `OBS-22` 是本项的真实读数;故上面只摘 `OBS-22` 行。
|
||
⚠️ **`RAW_BYTES=0` 的可区分性**:47 `dshs` 的 `[rehydrate]` 在窗口内 **0 字节**,但这**不是读取失败** —— 同窗口 `journalctl -u dshs --since @<epoch> \| wc -c` = **102 字节**(`-- Logs begin … -- / -- No entries --`)⇒ 读取正常、**确无该行**;且**全量历史** `journalctl -u dshs --no-pager \| grep -c '[rehydrate]'` = **0**(详见 8.11 第 1 条)。
|
||
|
||
### 8.6 部署对账
|
||
|
||
**本机 `npm run build` 产物 md5**:`orchestrator.js 1456d1609f5872c2d635ae7cf4f5c26b`|`remote-spawner.js 0438afd5738176999d3a4f4f404c9435`|`leased-spawner.js 12ba045990b2054aaeebeb2c95d70f83`
|
||
**部署四处(md5 逐个相同)**:
|
||
|
||
| 机器 | 目录 | orchestrator.js | remote-spawner.js | leased-spawner.js |
|
||
|---|---|---|---|---|
|
||
| 47 | `/opt/dshs/lib/supervisor/` | `1456d160…` ✓ | `0438afd5…` ✓ | `12ba0459…` ✓ |
|
||
| 47 | `/opt/dsh-relay/lib/supervisor/` | `1456d160…` ✓ | `0438afd5…` ✓ | `12ba0459…` ✓ |
|
||
| 106 | `/opt/dshs-cluster/lib/supervisor/` | `1456d160…` ✓ | `0438afd5…` ✓ | `12ba0459…` ✓ |
|
||
| 106 | `/opt/dsh-relay/lib/supervisor/` | `1456d160…` ✓ | `0438afd5…` ✓ | `12ba0459…` ✓ |
|
||
|
||
**回滚点** = **两机** `/opt/dsh/backups/seq28-20260918-005909/`(旧 md5 已记录,含 relay 侧):
|
||
|
||
```
|
||
5bfb15e00b592c293548ae17b639399b orchestrator.js (= 序㉕ 铺的生产值)
|
||
0b0e95f284ad39aea17286446f3c07a1 remote-spawner.js
|
||
0f473774d116b80c2f57bc22077a0f9c leased-spawner.js
|
||
fad7926494578258f86a91eac613a204 relay-orchestrator.js ⟵ ⚠️ 见 8.11 第 2 条(relay 侧曾落后 2 个版本)
|
||
0b0e95f284ad39aea17286446f3c07a1 relay-remote-spawner.js
|
||
0f473774d116b80c2f57bc22077a0f9c relay-leased-spawner.js
|
||
```
|
||
|
||
**重启(R8:开发环境服务器)**:`2026-09-18 01:0x` —— 47 `systemctl restart dshs` + `restart dshs-worker`;106 `restart dshs-worker`。三单元 `is-active` = `active`(+ `dshs-relay` active)。门户 `curl -H 'Host: alotbuy.com' 127.0.0.1:3080/` = **200**。
|
||
⚠️ **⛔ 未重启 `dshs-relay`** —— 它**不加载** `supervisor/*`(只有 `dshs` / `dshs-worker` 用 `LocalSpawner`)⇒ 不必动数据面。
|
||
⚠️ **动生产前已取证**:两机活跃实例 = **0/0**(S0 第 3 条)⇒ 本次部署/重启**未影响任何在线用户**。
|
||
|
||
### 8.7 主判据 P0 / P1 / P2 + 对冲项(逐条原文)
|
||
|
||
**照 §5「判据自证表」原样回填**:
|
||
|
||
| 层 | 判据 | 命令 | 期望 | 实测 | 判定 |
|
||
|---|---|---|---|---|---|
|
||
| P0 | 夹具就位 | §5 P0 | ≥ 1 | 47 = **1**(`dsh-114801-507657e0.scope` + `127.0.0.1:20000` 在听)/106 = **1**(`dsh-100002-a58dd77b.scope` + `127.0.0.1:21000` 在听) | ✅ |
|
||
| P1 | 47 scope 守恒(**Manager**) | §5 P1(`restart dshs`) | `N_AFTER == N_BEFORE ≥ 1` | `before=1 after=1`(⚠️ **非判别腿**,理由见 8.11-1) | ✅ |
|
||
| P1 | 47 scope 守恒(**worker**) | §5 P1(`restart dshs-worker`) | 同上 | `N_BEFORE=1 → N_AFTER=1`,明细前后均为 `dsh-114801-507657e0.scope` | ✅ |
|
||
| P1 | 106 scope 守恒(worker) | §5 P1(`test106`) | 同上 | `N_BEFORE=1 → N_AFTER=1`,明细前后均为 `dsh-100002-a58dd77b.scope` | ✅ |
|
||
| P1 | **反向夹具变红** | 未部署新 lib 时重跑 | `N_AFTER == 0` | 47 **`1 → 0`** / 106 **`1 → 0`**(原文见 8.5 甲) | ✅ 判据**有判别力** |
|
||
| P2 | 认领行存在 | §5 P2① | ≥ 1 | 47 `RAW_BYTES=443`(3 行)/106 `RAW_BYTES=428`(3 行) | ✅ |
|
||
| P2 | 报告自洽 | §5 P2② | `adopted+stopped==scanned ∧ scanned>0` | 47 `{"scanned":1,"adopted":1,"stopped":0,"probeOk":0,…}` ⇒ **1+0==1 ✓ ∧ scanned=1 ✓**|106 同值 | ✅ |
|
||
| P2 | adopted / probe OK 原文 | §5 P2③ | 含 `scope=` / `uid=` / `port=` + `probe OK … :<port>` | 47:`[rehydrate] adopted dsh-114801-507657e0.scope uid=114801 role=main port=20000 user=cce6d1cd-…` + `[rehydrate] probe OK dsh-114801-507657e0.scope :20000`|106:`adopted dsh-100002-a58dd77b.scope uid=100002 … port=21000` + `probe OK … :21000` | ✅ |
|
||
| 对冲 | 静态守卫 | §5 对冲-静态 | 无命中 | `T5` 全绿(三文件 teardown 窗口内 **0** 命中)+ `T9` 反向夹具自证判别器**有**判别力 | ✅ |
|
||
| 对冲 | 动态单测 | `node --test test/orchestrator-teardown.test.mjs` | rc=0 | **9 pass / 0 fail**,rc=0 | ✅ |
|
||
| 对冲 | `OBS-22` | `node scripts/overlay-probe.cjs --table <参数表>` | PASS | `PASS OBS-22 … 47 静态守卫 ✓|认领面:scanned=1 adopted=1 stopped=0 ✓|106 静态守卫 ✓|认领面:scanned=1 adopted=1 stopped=0 ✓` | ✅ |
|
||
| 零回归 | `npm test` | `npm.cmd test` | 不退化 | **201 / 200 / 0 / 1**(=基线逐字一致) | ✅ |
|
||
| 零回归 | 场景 | `node scripts/overlay-failover-drill.cjs --scene all --table <参数表>` | 12 PASS / 0 SKIP / 0 FAIL | **12 PASS / 0 SKIP / 0 FAIL**(rc=0) | ✅ |
|
||
| 零回归 | 探针 | `node scripts/overlay-probe.cjs --table <参数表>` | 基线 +1 行 | **21 PASS / 0 SKIP / 1 FAIL** = 基线(19P/1S/1F,21 行)+ **新增 `OBS-22`(PASS)**;`OBS-21` 仍 FAIL(**线内在册缺口,⛔ 非本单引入**);⚠️ `OBS-09` 由 SKIP → **PASS**(存量实例在册且码 401 ∈ 集合 = 正常改善) | ✅ |
|
||
| **D1** | 生产值未改 | `grep -c 'COOLDOWN_MS=0'`(本次全部演练/夹具/回滚路径文件) | **0** | **0** | ✅ |
|
||
|
||
**🔴 P1 与 P2 分层判定(本单最重要的一条)**:本次**两边**均为 `scanned=1 ∧ adopted=1 ∧ stopped=0 ∧ 自洽` ⇒ **P1 + P2 双绿**,⛔ 未出现「`scanned == 0` 而数守恒」那种"P1 绿、P2 红"的混合态 ⇒ **不存在"实例没被杀但回收链未建成"的情形**(该口径仍已写进 `OBS-22` 的判据与参数表 §6,供将来出现时如实点名)。
|
||
⚠️ **另有一条真机数据点(意外收获)**:`--scene all` 演练的「幕4 前置」**含一次 Manager 重启**,演练结束时 47 的 `dsh-114801-507657e0.scope` **仍在册且端口在听** ⇒ Manager 重启不杀实例这条**在真实演练路径上再复现一次**。
|
||
|
||
### 8.8 零回归三件套
|
||
|
||
```
|
||
$ npm.cmd test ⇒ # tests 201 / # pass 200 / # fail 0 / # skipped 1
|
||
$ node scripts/overlay-failover-drill.cjs --scene all --table <参数表>
|
||
⇒ # 结果:12 PASS / 0 SKIP / 0 FAIL (rc=0)
|
||
$ node scripts/overlay-probe.cjs --table <参数表>
|
||
⇒ 21 PASS / 0 SKIP / 1 FAIL(OBS-21 = 线内在册缺口;新增 OBS-22 = PASS)
|
||
❌ 1 项红:OBS-21
|
||
```
|
||
|
||
⚠️ **三段全部同值或更好** ⇒ 零回归成立。🔴 `npm test` **未把新单测纳入**(刻意;见 8.4),故总数 201 与基线**逐字相同**。
|
||
|
||
### 8.9 边界自证
|
||
|
||
- ⛔ **未改任何生产值**(**D1**):本次**没有**改任何 systemd drop-in / `EnvironmentFile` / nginx·nft 配置;参数表 §3.8 新增的 6 个 `TEARDOWN_*` 键**只被探针读取**,⛔ 未写进任何 env。🔴 `grep -c 'RELAY_FAILOVER_COOLDOWN_MS=0'`(本次全部演练/夹具/回滚路径文件)= **0**。
|
||
- ⛔ **未新增公网监听口**(**R5**):`PASS OBS-11` ⇒ `必在 7 允许 4 区间 3 派生 2 实际 77 多出 0 缺失 0`,`relay 口绑定回环=1/1 条`。
|
||
- ⛔ **未改 `nft` / `nginx`**(**R5**):`OBS-11` ⇒ `nft accept 0 多出 0`;本次**零** nft/nginx 读写。
|
||
- ⛔ **未 commit / 未 push**(**R7**):`git rev-parse --short HEAD` 仍 = **`04776af`**(改动全部留在工作区)。
|
||
- ⛔ **未改 bwrap 参数**(🔴 47 是 bubblewrap 0.4.0)|⛔ **未删** `cleanStaleScopes(uid)` / `cleanAllStaleScopes()`(`T7` 断言在册)|⛔ **未把启动流程改成"什么都不做"**(`rehydrateAdoptedScopes` 三步齐备,日志可证)|⛔ **未"重启后重新 spawn 一遍"**(认领路径 `T18`/`R18` 先例断言:`adoptOne` 不写 `mains`、不 `spawn`)|⛔ **认领不写回 `mains`**(本单未碰认领代码)|⛔ **token 不落盘**(本单未碰凭据路径)|🔑 判据口径 = **孤儿 = 端口不通**(本单未改该口径)。
|
||
- 🔎 **一处如实登记的"范围新增"**:部署面**多铺了两处** `relay 侧 /opt/dsh-relay/lib/supervisor/`(见 8.11 第 2 条)—— 属**消除既存不一致**、非语义改动。
|
||
|
||
### 8.10 指纹
|
||
|
||
- **参数表指纹**:**`7fc5889341b99fb26bd05cad0313960b`** → **`3b295a8c43aafc4a62c6f1a59ea0b43a`**(§3.8 新增 6 键 + §6 新增 `OBS-22`;§10 内已同步登记,⚠️ §10 不计入指纹)
|
||
- **本单 §8 前前缀** = **`c9aeac82381f569fc8b9effad0fefa99`**(⚠️ **回填 §8 后仍不变** —— §8 是截断点,`✅ 已验证`)
|
||
- **全文 md5 / 行数** = 见 §11.4(⛔ 自指,只能现算)
|
||
|
||
### 8.11 如实留档(**必填,哪怕"没有"**)
|
||
|
||
1. 🔴 **修正了本单 §5 P1 的一条前提(上游假设错)**:单里写「47 走的是 `dshs`(Manager)的 `LocalSpawner`」。**实测 47 是 `DSHS_DEPLOY_MODE=cluster`**(`/etc/systemd/system/dshs.service.d/cluster.conf`)⇒ `web/server.ts:931-932` 走 `LeasedSpawner(new RemoteSpawner(…))`、**根本不构造 `LocalSpawner`** ⇒ 47 的 `dshs` **不打 `[rehydrate]`**(窗口内 0 字节 + **全量历史计数 0**)⇒ 其 `LocalSpawner.teardown()` 在生产上是**死代码**。**真正会执行 `LocalSpawner.teardown()` 的只有两台 `dshs-worker`**(`agent.ts:143`)。⇒ 已把该修正写进参数表 §3.8(`TEARDOWN_UNIT_47` 行)与 §6 `OBS-22`。**这不影响修复本身**(三处同改覆盖两处执行点),但**影响判据的落点**:⛔ 拿「47 `restart dshs`」当判别腿会得到**恒绿**(Manager 从来不停远端实例)⇒ 本单把判别腿放在 **两台 worker** 上(见 8.5 甲:`1 → 0`)。
|
||
2. 🔎 **一处"范围新增"(如实登记,依据 R8 + R11 + 本线 序㉖ 先例)**:部署时**先核**发现 `/opt/dsh-relay/lib/supervisor/`(两机都在)**另有一套 supervisor 副本**,且其 `orchestrator.js` = **`fad7926494578258f86a91eac613a204`** —— 那是 **序㉕ 之前**的值(落后 **2 个版本**:既无认领、也无本次改动),`remote-spawner.js` / `leased-spawner.js` 同样陈旧。**处置 = 一并铺平(四处 md5 全同)**,并把三份旧值以 `relay-*.js` 名存进同一回滚点 ⇒ **回滚完整**。依据 = 本线 序㉖ 的同型处置("`dshs` 侧 `server.js` 比 `dsh-relay` 侧落后一个版本 ⇒ 两边同铺已消除该既存不一致")。⚠️ **⛔ 未重启 `dshs-relay`**(它不加载 supervisor ⇒ 无行为影响)。
|
||
3. ⚠️ **两条与"本单语义无关"的红,均已归因(§9-8 要求)**:
|
||
- **`OBS-08`(端点表全在线)曾在替换窗口内 FAIL 一次**:实测 `端点表 2 条 / 离线 1 条`,明细 = `w-106:21000 online=false`(该实例**已换端口**、旧条目未回收)。**归因 = 暂态**:随后复测即 `2 条 / 离线 0 条`,清理前端点表已**自愈**(`19000 online=true` + `21001 online=true`,stale 条目消失)。⇒ **判为暂态,与序㉕ §8 的同型记录一致**("曾一次 FAIL、重跑即回绿 ⇒ 暂态"),⛔ **非本单引入**。⚠️ **但成因值得点名**:它出现在 **B 的「访问时自然替换」动作之后** —— 见第 4 条。
|
||
- **`OBS-16`(门窗口 ΔstatusHits ≤ 1)曾在一次运行里 FAIL**(Δ=2):该次运行**与 `overlay-failover-drill` 长跑并行**(同一时段另有演练在打 `/status`)⇒ 归因 = **测量期间的第三方命中**、⛔ 非语义问题;**终态复跑 Δ=1 ⇒ PASS**。
|
||
- ⚠️ **`OBS-11` 同次 FAIL**(`实际 79 多出 1`)=上述 stale 端点占的 1 个监听 ⇒ **终态 `实际 77 多出 0` ⇒ PASS**。
|
||
4. ⭐ **一个由"B"直接产生的真实现场(收益边界实证 · 也是本棒最有价值的副产品)**:做完 S6 后,我用**临时 session 走了一次真实用户可见面**(`curl -H 'Host: guest.alotbuy.com' -b sid=… 127.0.0.1:3080/`)⇒ **`http=200 bytes=62451` 真 HTML、`502` 标记 0**(= 序⑬ Q2 的判据再次满足)。**同刻** relay 端点表从 `{19000 ✓, 21000 离线}` 变为 `{19000 ✓, 21000 ✗, **21001 ✓**}` ⇒ **平台在用户访问时把实例换到了新端口 `21001`**(`launchToken` 不可恢复 ⇒ 走"自然替换"),旧实例 scope 被 `cleanStaleScopes(uid)` 收掉。⇒ 🔴 **这正是单 §9-9 / §10-4 预告的收益边界,第一次以真机时序证据呈现**:**「实例没被杀(P1 成立)」+「重启后不能直接进,用户下次访问时自然替换」**。⚠️ **附带现象**:替换窗口内会短暂留下**一条 stale 端点条目(离线 + 占 1 个监听)**,`OBS-08`/`OBS-11` 在该窗口内会红;**自愈**——但**自愈时延未测**(⇒ 建议作为独立观测项,⛔ 本单未扩大范围去做)。
|
||
5. ✅ **未发现** `adopted + stopped !== scanned`(§9-4 回头条件未命中)|未发现**双实例共 profile**(§9-2 未命中)|未出现**端口区间耗尽**(§9-3 未命中)|未出现"租约过期后另一 Manager 接管同 uid"(§9-6 未命中)|未改 bwrap(§9-7 未命中)。
|
||
6. ⚠️ **`summary` 里 `probeOk` 恒为 0(既有观测面缺陷,如实记录、⛔ 未改)**:真机日志顺序为 `adopted …` → `summary {"…","probeOk":0,…}` → `probe OK … :<port>` ⇒ **summary 在探活完成前就打出来了**(`probeOk`/`probeFail` 计数来不及累加)。⇒ 判据⛔ **不能**用 summary 里的 `probeOk ≥ 1`(会假红);本单 P2 用的是 `adopted + stopped === scanned` + 独立的 `probe OK` 行(两者都成立)。**属 `rehydrateAdoptedScopes` 的既有行为、⛔ 非本单引入**(本单只动 `teardown()`),**登记为独立后续项**(修它 = 把 summary 挪到探活回调之后)。
|
||
7. ⚠️ **`enter` 的 Host 选择**:`-H 'Host: guest.alotbuy.com'` ⇒ `404 not found`;`-H 'Host: alotbuy.com'` ⇒ `200`(admin 同)。⇒ 是**验收脚本的 Host 写法**问题(会话已由 `sid` cookie 认定),**⛔ 非平台缺陷**,已写进 8.2。
|
||
8. ✅ **夹具已全部清理**:两机 `dsh-*.scope` 计数 **0 / 0**(临时 session 亦按 R4 删净:`DELETE 0` / 计数 0)⇒ **⛔ 未在生产留任何夹具**。
|
||
9. ⚠️ **本单 §2 的"正确排期口径"段经全工作区检索 = 不存在**(`grep -rn "正确排期口径" .workbuddy/ *.md` ⇒ 0 命中)⇒ 该"疑似被误编辑"的线索**经取证判为不成立**,⛔ 未做任何"修正"(⛔ 不凭印象改文档)。
|
||
|
||
### 8.12 回滚路径补正(**与 §6 配套;🔴 §6 正文一字未改,以保 §8 前前缀**)
|
||
|
||
🔴 **本棒实际部署了 4 处**(见 8.6),而**单 §6 只写了 2 处**(47 `/opt/dshs/lib/supervisor/` + 106 `/opt/dshs-cluster/lib/supervisor/`)⇒ **回滚时若不补 relay 侧两处,`/opt/dsh-relay/lib/supervisor/` 会仍留着本次改动 ⇒ 回滚不完整**。
|
||
⚠️ **§6 正文一字未动**(补正放在本节的唯一理由:§6 在 `## §8 回报格式` **之前**,改它会**打穿 §8 前前缀 `c9aeac82381f569fc8b9effad0fefa99`**)。
|
||
|
||
**路 A(语义回滚)· 补全 4 处** —— 在 §6 路 A 命令后**追加两行**:
|
||
```bash
|
||
scp -P 22 lib/supervisor/{orchestrator,remote-spawner,leased-spawner}.js bt-server:/opt/dsh-relay/lib/supervisor/
|
||
scp -P 22 lib/supervisor/{orchestrator,remote-spawner,leased-spawner}.js test106:/opt/dsh-relay/lib/supervisor/
|
||
```
|
||
|
||
**路 B(产物回滚)· 补全 4 处**(回滚点两机均为 `/opt/dsh/backups/seq28-20260918-005909/`):
|
||
```bash
|
||
ssh -p 22 bt-server 'cd /opt/dsh/backups/seq28-20260918-005909 && cp orchestrator.js remote-spawner.js leased-spawner.js /opt/dshs/lib/supervisor/ && for f in orchestrator remote-spawner leased-spawner; do cp relay-$f.js /opt/dsh-relay/lib/supervisor/$f.js; done && systemctl restart dshs'
|
||
ssh -p 22 test106 'cd /opt/dsh/backups/seq28-20260918-005909 && cp orchestrator.js remote-spawner.js leased-spawner.js /opt/dshs-cluster/lib/supervisor/ && for f in orchestrator remote-spawner leased-spawner; do cp relay-$f.js /opt/dsh-relay/lib/supervisor/$f.js; done && systemctl restart dshs-worker'
|
||
```
|
||
⚠️ 回滚点里的 `relay-*.js` 是**铺平前的旧值**(`relay-orchestrator.js` = 序㉕ 之前的陈旧版)⇒ 路 B 会同时把 relay 侧退回**旧版**,这正是"回到本棒之前"的**完整**语义。
|
||
|
||
✅ **回滚点内容已复核**(2026-09-18 05:3x 现读):两机均含 **6 个**文件 —— `orchestrator.js` 82401 B / `remote-spawner.js` 13284 B / `leased-spawner.js` 9369 B / `relay-orchestrator.js` 70994 B / `relay-remote-spawner.js` 13284 B / `relay-leased-spawner.js` 9369 B。
|
||
✅ **当前生产 md5 已复核**(同上时刻):两机 **4 处** 三文件全同 —— `orchestrator.js` = `1456d1609f5872c2d635ae7cf4f5c26b` / `remote-spawner.js` = `0438afd5738176999d3a4f4f404c9435` / `leased-spawner.js` = `12ba045990b2054aaeebeb2c95d70f83` ⇒ **部署后未被任何后续动作漂移**;两机 `dshs-worker` / `dshs-relay` 均 **active**。
|
||
|
||
---
|
||
|
||
## §9 回头条件(**一出现必须回头**)
|
||
|
||
1. 🔴 **⛔ 任何一棒都不许把 `RELAY_FAILOVER_COOLDOWN_MS=0` 写进回滚 / 演练 / 夹具路径。** 依据:该值**看起来合法、实际会自锁** —— `num()` 的 `/^\d+$/` 放行 `'0'`,但归零会让"失败候选必须被排除"一起失效 ⇒ 候选链卡在第一个失败候选上(**实测 121–123 s 无切换**)。本单的验收里 `grep -c` 该值必须 = **0**。
|
||
2. **出现"双实例共 profile"**(档案 30 复现:会话 / settings 写冲突)⇒ **立刻回滚**(§6)+ 报告。
|
||
3. **端口区间耗尽**(`实例端口区间已耗尽`)⇒ 停下报告,⛔ **不许私自扩 `--span`**(属参数表改动,须另立单)。
|
||
4. **`adopted + stopped !== scanned`** ⇒ 停下报告(自洽性破了 ⇒ 判据不可信)。
|
||
5. **需要超出 §3.1 在册文件集** ⇒ 停下报告(**R7**)。⚠️ 本单已凭用户「选 B」解除了处②/③ 的阻塞,**不含第三次扩范围**。
|
||
6. **出现"租约过期后另一 Manager 接管同一 uid"的实测案例** ⇒ 停下报告(双写风险;见 §7.4 lease 行)。
|
||
7. **需要改 bwrap 参数** ⇒ ⛔ 停(🔴 **47 是 bubblewrap 0.4.0,`--perms` 属 0.5+ ⇒ 沙箱起不来 = 所有实例全挂**)。
|
||
8. **零回归出现"新"的红**(非本单引入)⇒ 先归因(判别器 / 日志原文)再决定是否继续;⛔ 不许"重跑一次就好了"了事。
|
||
9. **`enter` 的真实用户可见面出现 503 **时,注意区分**:这是 B 下**已知且刻意保留**的边界(`launchToken` 不可恢复 —— 序 ㉕ §0 的客观边界),⛔ **不是本单引入的缺陷**,但**必须如实写进 §8.11**(收益边界 = "不被杀 + 访问时自然替换",⛔ 不是"重启后直接可用")。
|
||
|
||
---
|
||
|
||
## §10 未验证项(**一个都不许编数**)
|
||
|
||
| # | 未验证项 | 为什么重要 | 怎么验(口径) |
|
||
|---|---|---|---|
|
||
| 1 | **租约过期 ⇒ 另一 Manager 接管同 uid 的窗口风险** | B 下实例不随进程死,归属记录会与存活实例不一致 ⇒ 理论上有"两个 Manager 都以为自己该管这个 uid"的窗口 | 起第 2 个 Manager 副本(本机多实例,用户 2026-09-17 已授权「本机内存大 可以模拟多台」)⇒ 停其一 ⇒ 等 `LEASE_TTL_MS=30 s` 过期 ⇒ 观察是否出现双 owner / 是否触发 `cleanStaleScopes` 替换 |
|
||
| 2 | **`used_mb` 记账在 Manager 重启后是否自愈** | 记账漂移会让容量决策失准 | 重启前后各读一次 `dsh_hosts.used_mb`(47 的 PG:`127.0.0.1:15432`),观察心跳恢复后是否回到真值 |
|
||
| 3 | **106 侧 idle-reap 是否真的在跑** | §7.3 的"谁来收"前提在 106 上只靠它 | 106 上造一个空闲实例 ⇒ 观察 `idleReapIntervalSeconds=60 s` 的扫描日志;⚠️ **TTL 7 天 ⇒ 短窗口内验不到"收"**,只能验"扫描在跑"(须显式写清这个口径) |
|
||
| 4 | **B 的真实收益边界(用户可见面)** | 收益是"不被杀 + 访问时自然替换",**不是**"重启后可直接进"(`launchToken` 不可恢复 ⇒ `enter` 仍 503) | 用临时 session(**R4**:`mksess` 直插、用完即删)走一次 `POST /api/dsh/enter`,记录 503 与其后"自然替换"(换端口换 token)的全过程 |
|
||
| 5 | **认领探活对启动时长的影响** | `scanned × stagger(500 ms) + 单条 probe(2000 ms)`;实例多时启动会变慢 | 用 N = 1 / 4 / 8 个夹具 scope 各跑一次,记录"启动 → `[rehydrate] summary` 出现"的墙钟 |
|
||
| 6 | **性能/成本:端口区间长期占用** | 见 §7.4 端口行 | 连续 5 次 spawn/替换,记录区间占用峰值与是否出现耗尽告警 |
|
||
|
||
---
|
||
|
||
## §11 指纹与状态
|
||
|
||
### 11.1 归档号(**本棒已原子占号**)
|
||
- **现核最大号**(⛔ **不写死**,取号命令):`ls 04-调整方案/ | grep -oE '^[0-9]+' | sort -n | tail -1` ⇒ **本棒实测 = `128`**。
|
||
⚠️ **prompt 写的"归档编号至 112"已过期**(现核 128 ⇒ 序号 113–128 是立单时的历史值)⇒ 任何引用都必须**复跑取号**。
|
||
- **占号动作(已执行)**:`mkdir 04-调整方案/.lock-129` ⇒ **成功** ⇒ 本单归档号 = **129**。
|
||
- 🔴 **63 是空号,⛔ 不补占**(已核:`ls 04-调整方案/ | grep -E '^63'` = 空)。
|
||
- ⚠️ **本棒不落 04 孪生** —— 取证结论:本线的 `交接单_*.md` **均无** `04-调整方案/` 孪生(逐条比对 16 份单 = 0 命中);带孪生的是**方案类文档**(如 `123-方案规划方法-覆盖网络线提炼.md` ↔ 根同名,md5 同值 `cf063cc0…`)。⇒ **占号窗口在落单后即释放**,执行棒收官建档案时**必须重新现核取号**。
|
||
|
||
### 11.2 指纹
|
||
- **§8 前前缀**(口径:`sed '/^## §8 回报格式/,$d' <本单> | md5sum`):**见下方"落单后实测"** —— 每次改本单(含回填 §8)都要重算。
|
||
- ⚠️ **§8 一旦回填,前缀指纹不变**(§8 在截断点之后);**全文 md5 会变**。
|
||
- **落单后实测**(2026-09-18 00:5x,序 ㉘ 规划棒):
|
||
- **§8 前前缀** = **`c9aeac82381f569fc8b9effad0fefa99`**(⚠️ 本值**稳定** —— §11 在截断点之后,改 §11/回填 §8 都不动它)
|
||
- **全文 md5(§11 本段回填前快照)** = **`dd4f95d5dcae747d63437c8d0ede5fd8`**
|
||
- **全文 md5(§11 本段回填后最终)** = **`见下方 §11.4`**(写完本段后现算;⚠️ 二者**必然不同** —— 口径必须写明,⛔ 不许把快照当最终值引用)
|
||
- **行数** = **383**(快照时刻;回填 §8 后会增长)
|
||
|
||
### 11.3 状态
|
||
- **本单状态**:**✅ 已执行 · 待归档**(2026-09-18 00:5x–01:0x 由「**覆盖网络线-序 ㉚ 执行棒 · 单 A**」执行完毕;**P1 + P2 双绿**、零回归三件套全绿、夹具已清理、生产 4 处已上线)。
|
||
- **执行棒回报** = 本单 **§8.1–§8.12**(全节已回填;§8.12 为回滚路径补正)。
|
||
- **本棒(序 ㉘ 规划棒)边界自证**:⛔ **零代码改动** · ⛔ **零服务器改动**(只读取证:`systemctl list-units` / `systemctl show -p Environment` 两条只读命令 / 两机各一次)· ⛔ 未 commit / 未 push · ⛔ 不改任何生产值。
|
||
- **上游单状态**:`交接单_实例逐步拉起_20260917.md` **§8.3 已由本单结清**(B 被采纳 ⇒ 该单的"待拍板"项关闭)。
|
||
- 🔴 **本单遗留的独立后续项**(均已在 §8.11 / §8.12 点名,⛔ 未扩大范围去做):① `summary.probeOk` 恒 0(既有观测面缺陷)② 「自然替换窗口内 stale 端点条目」的自愈时延未测 ③ §10 的 6 条未验证项(一条未验)④ 架构目标 `C`(§7.2,登记不执行)。
|
||
|
||
### 11.4 全文 md5 的口径(⛔ **不写死**)
|
||
- 全文 md5 **每改一次本单都会变**(回填 §8 尤其)⇒ **权威取数 = 现算**:
|
||
`md5sum "E:/ProgramData/AI技能/aliyun-dsh-server/交接单_退出路径不杀实例_20260918.md"`
|
||
- **落单快照**(§11.4 本段写入前的最后一次现算,2026-09-18 00:5x)= **`b1b93471081e10d9be9c28aa9b5c1d3c`**(**387 行**)。
|
||
⚠️ 本行写入后该值**即失效** ⇒ **引用前必须复跑上面的命令**(本库已有"写死基线 40 分钟内被打穿"的实证)。
|
||
- **§8 前前缀** 不受影响(§8 是截断点)⇒ 仍是 §11.2 的 **`c9aeac82381f569fc8b9effad0fefa99`**。
|
||
|
||
### 11.5 执行后实测(2026-09-18 05:3x · 供第三方复现)
|
||
|
||
- **§8 前前缀**(现算,口径 `sed '/^## §8 回报格式/,$d' <本单> | md5sum`)= **`c9aeac82381f569fc8b9effad0fefa99`** ⇒ 🔴 **回填 §8 + 新增 §8.12 之后仍逐字不变**(= "§8 是截断点"这条口径的**自证**)。
|
||
- **全文 md5 / 行数**:⛔ **不写死**,权威取数 = `md5sum <本单>` 现算(本单每改一次都会变)。
|
||
- **生产 md5(两机 4 处、三文件)**:`orchestrator.js = 1456d1609f5872c2d635ae7cf4f5c26b` / `remote-spawner.js = 0438afd5738176999d3a4f4f404c9435` / `leased-spawner.js = 12ba045990b2054aaeebeb2c95d70f83`。
|
||
- **两机活跃实例 scope** = **0 / 0**(夹具已清);`dshs-worker` / `dshs-relay` = **active**。
|