docs: external-resources manual — xceesd kill/restart recover semantics

Live-verified on t1 (2026-10-29): kill -9 xceesd with 3 RUN license jobs
holding markers, restart, and confirm recovery end to end.

- §10.1: recover semantics — xceesd is a stateless collector; fresh
  restart joins, skips full re-push (0/1 sources, no cached value), and
  the first poll round pushes the collector script's live value, which
  re-aligns the pool in one shot. Correctness rests on the script being
  liveness-based (§5.3).
- §1.2: esd outage is the unbounded form of the exposure window — the
  controller freezes on the last pushed value and STALE is observation
  only, never an admission input.
- §11.5 (new, scenario 4): full E2E recipe with pass criteria, incl.
  the ops pitfall found while testing — a PEND during the outage may be
  a slots limit (limit1 per-user cap) rather than the license pool;
  check pending_reason via the apiserver JSON (CLI shows "-").

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-10-29 10:26:42 +00:00
parent 0b47076d05
commit 7520cca784

View file

@ -18,8 +18,8 @@
7. [策略层:limits 配额](#7-策略层limits-配额) 7. [策略层:limits 配额](#7-策略层limits-配额)
8. [作业提交](#8-作业提交) 8. [作业提交](#8-作业提交)
9. [运维观测](#9-运维观测) 9. [运维观测](#9-运维观测)
10. [故障处理](#10-故障处理) 10. [故障处理](#10-故障处理)(§10.1 含掉线重启 recover 语义)
11. [测试指南(E2E 配方)](#11-测试指南e2e-配方) 11. [测试指南(E2E 配方)](#11-测试指南e2e-配方)(§11.5 场景 4:xceesd 被杀 recover)
12. [已知限制](#12-已知限制) 12. [已知限制](#12-已知限制)
--- ---
@ -88,6 +88,11 @@
资源的作业,可能在自由值还没降下来时都获准入(真实 license 服务器会在 资源的作业,可能在自由值还没降下来时都获准入(真实 license 服务器会在
checkout 时兜底拒绝,见 §11.2 licjob 的 exit 99)。生产上要么接受这个 checkout 时兜底拒绝,见 §11.2 licjob 的 exit 99)。生产上要么接受这个
窗口,要么在提交脚本里隔开(≥ poll_interval)。 窗口,要么在提交脚本里隔开(≥ poll_interval)。
- **xceesd 掉线是暴露窗口的无界版**:掉线后没有新值进来,controller 保留
最后一次推送的旧值(sharedInst 不因 daemon 掉线清零),调度器按冻结值
继续放行,直到真实 license 服务器 checkout 失败兜底或值恢复推送。STALE
只在观测面(§1.3/§9.1),**不进准入判式**——掉线要靠告警发现,不能指望
调度器自动收紧。恢复语义见 §10.1。
- §7 的 limit vres 是**非 EXTERNAL** 的 GLOBAL 资源,走完整 - §7 的 limit vres 是**非 EXTERNAL** 的 GLOBAL 资源,走完整
`inuse + uncomit + wanted` 公式,配额在调度器本地即时扣减——不依赖 `inuse + uncomit + wanted` 公式,配额在调度器本地即时扣减——不依赖
推送,同一轮内也严格生效。 推送,同一轮内也严格生效。
@ -594,10 +599,30 @@ sharedInst 不会因 daemon 掉线自动清零)。调度器会继续按旧值
ps aux | grep [x]ceesd ps aux | grep [x]ceesd
tail -50 /tmp/xceesd.log tail -50 /tmp/xceesd.log
# 2. 重启(会自动 rejoin + 全量重推,池子自动恢复) # 2. 重启(recover 语义见下;配方见 §11.5 场景 4)
xceesd -c /etc/xce/esd.yaml -l /tmp/xceesd.log & 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 重启 ### 10.2 controller 重启
xceesd 检测到连接断开会自动 re-join + 全量重推(bug-1978 修复的语义), 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 用量无关) # 期望: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://<apiserver>: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://<apiserver>:8080/xce/v1/jobs/<jobid>" | \
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 <jobid-1>
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 回归清单
提交相关改动前跑: 提交相关改动前跑: