diff --git a/src/docs/external-resources-user-manual.md b/src/docs/external-resources-user-manual.md index cbd07f98..cfd672db 100644 --- a/src/docs/external-resources-user-manual.md +++ b/src/docs/external-resources-user-manual.md @@ -18,8 +18,8 @@ 7. [策略层:limits 配额](#7-策略层limits-配额) 8. [作业提交](#8-作业提交) 9. [运维观测](#9-运维观测) -10. [故障处理](#10-故障处理) -11. [测试指南(E2E 配方)](#11-测试指南e2e-配方) +10. [故障处理](#10-故障处理)(§10.1 含掉线重启 recover 语义) +11. [测试指南(E2E 配方)](#11-测试指南e2e-配方)(§11.5 场景 4:xceesd 被杀 recover) 12. [已知限制](#12-已知限制) --- @@ -88,6 +88,11 @@ 资源的作业,可能在自由值还没降下来时都获准入(真实 license 服务器会在 checkout 时兜底拒绝,见 §11.2 licjob 的 exit 99)。生产上要么接受这个 窗口,要么在提交脚本里隔开(≥ poll_interval)。 +- **xceesd 掉线是暴露窗口的无界版**:掉线后没有新值进来,controller 保留 + 最后一次推送的旧值(sharedInst 不因 daemon 掉线清零),调度器按冻结值 + 继续放行,直到真实 license 服务器 checkout 失败兜底或值恢复推送。STALE + 只在观测面(§1.3/§9.1),**不进准入判式**——掉线要靠告警发现,不能指望 + 调度器自动收紧。恢复语义见 §10.1。 - §7 的 limit vres 是**非 EXTERNAL** 的 GLOBAL 资源,走完整 `inuse + uncomit + wanted` 公式,配额在调度器本地即时扣减——不依赖 推送,同一轮内也严格生效。 @@ -594,10 +599,30 @@ sharedInst 不会因 daemon 掉线自动清零)。调度器会继续按旧值 ps aux | grep [x]ceesd tail -50 /tmp/xceesd.log -# 2. 重启(会自动 rejoin + 全量重推,池子自动恢复) +# 2. 重启(recover 语义见下;配方见 §11.5 场景 4) xceesd -c /etc/xce/esd.yaml -l /tmp/xceesd.log & ``` +**重启 recover 语义(2026-10-29 活体验证)**:xceesd 是无状态采集器, +进程被杀(含 SIGKILL)后直接重启即恢复,不需要任何手工恢复动作: + +1. 启动 → join cluster(`joined cluster as xceesd`)。 +2. startup 全量重推:日志 `esd full re-push: 0/1 source(s)`——**0 是正常 + 的**:重启后的 xceesd 没有任何缓存值(`!src->have_value` 的源被跳过, + esd.c esd_push_all),它不知道池子该是多少,也不该猜。 +3. 首轮 poll 立即采集脚本**当前实况值**并推送。恢复正确性的关键在这: + 采集脚本必须报 live 状态(剩余数),而不是自己记账的累计值。liveness + 式脚本(如 §11.1 的 fake_lic.sh,marker 文件 = 活 pid 持有,死亡即 + 自清理)天然满足——重启瞬间第一轮采集就收敛到真实占用,掉线期间 + 已 checkout 的 license 不会被多报也不会丢报。 +4. 之后与正常运行无异:debounce(值不变不推)、每轮写 status JSON、 + `pushes ok/fail` 从 0 重新累计。 + +换言之,掉线窗口内发生过的一切(作业 checkout、release、崩溃释放)都 +由"重启后第一次采集 = 实况"一次性对齐,xceesd 自己没有任何需要修复的 +状态。若脚本不是 liveness 式而是自记账式,掉线期间的漂移要由脚本自身 +的对账逻辑负责——写脚本时优先选 liveness 式(§5.3)。 + ### 10.2 controller 重启 xceesd 检测到连接断开会自动 re-join + 全量重推(bug-1978 修复的语义), @@ -814,7 +839,73 @@ xce jobs submit -q normal -P projB -R "rusage[extlic=1]" -- /tmp/e2e/licjob.sh s # 期望:RUN(projB 有自己的 3 个配额,与 projA 用量无关) ``` -### 11.5 回归清单 +### 11.5 场景 4:xceesd 意外被杀 → 重启 recover(E2E) + +实测通过的完整配方(2026-10-29,t1)。验证目标:有作业持着 license 时 +`kill -9 xceesd`,重启后 daemon 自行对齐实况、无幻影容量、无丢失占用, +且回填/再准入链路不受掉线影响。 + +**前置**:场景 1 的环境(fake_lic.sh + licjob.sh + esd.yaml,TOTAL=4), +xceesd 在跑且已推过值。作业用 `sleep 900`(15 分钟)——步骤多、每步要 +等 push/debounce,短 sleep 作业会在操作中途自然跑完污染现场。 + +```bash +# ── 1. 造现场:3 个持 license 的 RUN 作业 ────────────────────────── +for i in 1 2 3; do + xce jobs submit -q normal -R "rusage[extlic=1]" -- /tmp/e2e/licjob.sh sleep 900 +done +# 等全部 RUN(sleep 900 不会过期),确认: +ls /tmp/e2e/licpool/ | wc -l # 期望 3(3 个活 marker) +xce resources distribution list -t dynamic | grep extlic +# 期望 1(free = 4-3) + +# ── 2. SIGKILL xceesd,记录现场 ──────────────────────────────────── +kill -9 $(pgrep -x xceesd) +pgrep -x xceesd | wc -l # 期望 0 +# 掉线窗口语义(§1.2):controller 冻结在 1,3 个作业继续 RUN。 +# 此刻提交 rusage[extlic=1] 仍会按冻结值判(可能被放行——暴露窗口)。 +# 注意:此时若作业 PEND,先查 pending reason 再怪池子——本机 limits.yaml +# 若带 slots 类 limit(如 limit1 per-user 3 slots),第 4 个作业可能是被 +# 配额卡的(reason=limit_global),不是 license(reason=NO_RESOURCE)。 +# pending reason 用 JSON 看(CLI 显示 "-",已知限制 §12.8): +TOKEN=$(curl -s -X POST "http://:8080/xce/v1/auth/logon" \ + -H "Content-Type: application/json" \ + -d '{"username":"admin","password":"..."}' | python3 -c \ + "import json,sys; print(json.load(sys.stdin)['data']['token'])") +curl -s -H "Authorization: Bearer $TOKEN" \ + "http://:8080/xce/v1/jobs/" | \ + python3 -m json.tool | grep -A3 pending_reason + +# ── 3. 重启 xceesd ───────────────────────────────────────────────── +xceesd -c /tmp/e2e/esd.yaml -l /tmp/xceesd-d.log -v & +# 日志期望(顺序): +# joined cluster as xceesd (1 source(s)) +# esd full re-push: 0/1 source(s) ← 0 正常!无缓存值,跳过(§10.1) +# 随后首轮 poll 立即采集实况并推送。 +sleep 15 +python3 -m json.tool /tmp/e2e/esd-status.json +# 期望:pid=新进程,pushes_ok>=1,last_value=1(3 个 RUN 的 marker 还活着, +# fake_lic.sh liveness 语义自动排除死亡进程的陈旧 marker) +xce resources distribution list -t dynamic | grep extlic +# 期望 1——与掉线前一致,无幻影 4/0 + +# ── 4. recover 后回填:kill 一个 RUN 作业 ───────────────────────── +xce jobs kill +sleep 15 # 等 poll(10s)+ push +ls /tmp/e2e/licpool/ | wc -l # 期望 2 +xce resources distribution list -t dynamic | grep extlic +# 期望 2(free 回升,push 链正常) +# 再提交第 4 个作业 → RUN(recover 后再准入不受影响) + +# ── 5. 收尾:等/杀掉全部作业,确认 free 回到 4 ──────────────────── +``` + +**通过判据**:重启后 status JSON 的 pid 换新、pushes_ok 重新累计且无 +fail;`full re-push: 0/1` 后首轮推送值 = 掉线前的真实 free(本例 1); +kill 作业后 marker 回收、值回升、PEND/RUN 链路照常。任何一步看到值与 +池子实况不符(多报/少报),先怀疑采集脚本不是 liveness 式(§10.1)。 + +### 11.6 回归清单 提交相关改动前跑: