已合并
feat(session-manager): 场景 F 快失败——拆除有界等待队列,scope 满立即 503 SCOPE_FULL #491
王明琦创建于 2 天前
feat(session-manager): 场景 F 快失败——拆除有界等待队列,scope 满立即 503 SCOPE_FULL #491
已合并
共 36 个文件变更+408-768
| @@ -24,11 +24,11 @@ | |||
| 24 | ```bash | 24 | ```bash |
| 25 | cd applications/agent_runtime | 25 | cd applications/agent_runtime |
| 26 | uv sync --extra local | 26 | uv sync --extra local |
| 27 | -uv run pytest # 442 个用例:状态层 Lua(含 EVICT/TOUCH/ROUTE_PLACE 残骸自卫) / 路由匹配纯函数(含表达式解析) / config 层(含三段式契约与容器表水合、config_refresh 强制刷新、写库单事务+锁看门狗) / 容器规范形(container_spec) / envFrom / 组件全链路(含 sidecars 多容器与卷挂载) / HTTP 冒烟 / corner case / 双实例多副本 / 停机韧性 / 审计实锤回归(test_audit_repro) / 强制刷新自然老化(test_force_refresh) / 健壮性故障注入(test_k8s_io_timeouts / test_infra_faults / test_main_lifecycle,见 docs/feature/2026-09-robustness-hardening.md) | 27 | +uv run pytest # 433 个用例:状态层 Lua(含 EVICT/TOUCH/ROUTE_PLACE 残骸自卫) / 路由匹配纯函数(含表达式解析) / config 层(含三段式契约与容器表水合、config_refresh 强制刷新、写库单事务+锁看门狗) / 容器规范形(container_spec) / envFrom / 组件全链路(含 sidecars 多容器与卷挂载) / HTTP 冒烟 / corner case / 双实例多副本 / 停机韧性 / 审计实锤回归(test_audit_repro) / 强制刷新自然老化(test_force_refresh) / 健壮性故障注入(test_k8s_io_timeouts / test_infra_faults / test_main_lifecycle,见 docs/feature/2026-09-robustness-hardening.md) |
| 28 | ``` | 28 | ``` |
| 29 | 29 | ||
| 30 | - 构造 `ServiceManager` 必须传 `deploy_mode="subprocess"`(默认 k8s 会挂死测试)。 | 30 | - 构造 `ServiceManager` 必须传 `deploy_mode="subprocess"`(默认 k8s 会挂死测试)。 |
| 31 | -- fakeredis 陷阱:消费组 id=`"0"` bug、pubsub 需共享 FakeServer、EVAL 内 PUBLISH 需实测。 | 31 | +- fakeredis 陷阱:消费组 id=`"0"` bug;所有门面共享同一 client;EVAL(Lua)依赖 lupa,缺失自动 skip。 |
| 32 | - 双实例测试(`tests/integration/test_multi_replica.py` + `_dual_harness.py`):同进程两 App 共享一组 fakeredis/SQLite/FakeK8s,httpx ASGITransport 单事件循环驱动;lifespan 必须先手动驱动(否则 RestAdapter 惰性二建 sysctx 绕过后台 Job)。 | 32 | - 双实例测试(`tests/integration/test_multi_replica.py` + `_dual_harness.py`):同进程两 App 共享一组 fakeredis/SQLite/FakeK8s,httpx ASGITransport 单事件循环驱动;lifespan 必须先手动驱动(否则 RestAdapter 惰性二建 sysctx 绕过后台 Job)。 |
| 33 | - 旧 SDK 已知失败用例(非回归)见外层 jiuwenclaw 仓库 CLAUDE.md 末尾清单。 | 33 | - 旧 SDK 已知失败用例(非回归)见外层 jiuwenclaw 仓库 CLAUDE.md 末尾清单。 |
| 34 | 34 | ||
| @@ -44,7 +44,7 @@ cd applications/agent_runtime | |||
| 44 | `./scripts/integration_smoke.sh --image swr.cn-north-4.myhuaweicloud.com/openjiuwen/jiuwenclaw-agentserver-amd64:<tag> --sidecar-image swr.cn-north-4.myhuaweicloud.com/openjiuwen/jiuwenclaw-sandbox-amd64:<tag> --health-path /api/v1/health --sse-path /api/v1/events/stream --agent-env '{"AGENT_HTTP_ENABLED":"true","AGENT_HTTP_HOST":"0.0.0.0","AGENT_HTTP_PORT":"8086"}' --with-sidecar --with-mounts` | 44 | `./scripts/integration_smoke.sh --image swr.cn-north-4.myhuaweicloud.com/openjiuwen/jiuwenclaw-agentserver-amd64:<tag> --sidecar-image swr.cn-north-4.myhuaweicloud.com/openjiuwen/jiuwenclaw-sandbox-amd64:<tag> --health-path /api/v1/health --sse-path /api/v1/events/stream --agent-env '{"AGENT_HTTP_ENABLED":"true","AGENT_HTTP_HOST":"0.0.0.0","AGENT_HTTP_PORT":"8086"}' --with-sidecar --with-mounts` |
| 45 | (冒烟脚本参数直通 e2e_hld_acceptance.py;替身 influxdb:1.8 仅用于快速回归,默认契约 /health+8086;真镜像门禁必须带三件套契约参数——脚本模板的 health_path/sse_path/agent_env 由这三个参数注入,漏带则 readiness 永不通过、阶段 2 起全红;`--with-sidecar --sidecar-image` 开双真镜像 sidecar 全规格,`--with-mounts` 开全量真实规格阶段——主容器 cm/hp/pvc 三挂载 + PVC 静态预置 + 逐字段断言,2026-08-28 起为门禁标配) | 45 | (冒烟脚本参数直通 e2e_hld_acceptance.py;替身 influxdb:1.8 仅用于快速回归,默认契约 /health+8086;真镜像门禁必须带三件套契约参数——脚本模板的 health_path/sse_path/agent_env 由这三个参数注入,漏带则 readiness 永不通过、阶段 2 起全红;`--with-sidecar --sidecar-image` 开双真镜像 sidecar 全规格,`--with-mounts` 开全量真实规格阶段——主容器 cm/hp/pvc 三挂载 + PVC 静态预置 + 逐字段断言,2026-08-28 起为门禁标配) |
| 46 | - 冒烟内置回归网:阶段 1b(无请求预热)、阶段 5b(自然老化零回拨)、阶段 11b(内部不变量巡检:idle⊆pods:all / idle_since 存在 / 静息 deploying=0 / 快照 deploy_ver==RM cfg)——2026-08-26 真环境实测缺陷①②④⑤的固化。 | 46 | - 冒烟内置回归网:阶段 1b(无请求预热)、阶段 5b(自然老化零回拨)、阶段 11b(内部不变量巡检:idle⊆pods:all / idle_since 存在 / 静息 deploying=0 / 快照 deploy_ver==RM cfg)——2026-08-26 真环境实测缺陷①②④⑤的固化。 |
| 47 | -- 经多副本 LB 亦可跑(实测 65/65),前提:部署带 `AGENT_RUNTIME_SCOPE_FULL_TIMEOUT`(deploy 模板默认 8,**须显著小于模板 session_ttl**,否则等待者 deadline 与会话到期碰撞产生混合结果);排查实录见 `docs/spec/e2e-test-cases.md` §8.1。 | 47 | +- 经多副本 LB 亦可跑(实测 65/65);历史排查实录见 `docs/spec/e2e-test-cases.md` §8.1。 |
| 48 | - cleanup 空目标必须用**无匹配 label_selector**,不得指向业务 ns(同 label 真实 AgentServer 会被误删)或不存在的 ns(in-cluster SA 对其 403 而非空列表)。 | 48 | - cleanup 空目标必须用**无匹配 label_selector**,不得指向业务 ns(同 label 真实 AgentServer 会被误删)或不存在的 ns(in-cluster SA 对其 403 而非空列表)。 |
| 49 | 49 | ||
| 50 | ### 多副本(真环境) | 50 | ### 多副本(真环境) |
| @@ -10,11 +10,11 @@ | |||
| 10 | |---|---|---| | 10 | |---|---|---| |
| 11 | | 本文件 | 架构一页纸 + 键前缀总览 + 测试/部署入口 | 先读 | | 11 | | 本文件 | 架构一页纸 + 键前缀总览 + 测试/部署入口 | 先读 | |
| 12 | | [service-core.md](service-core.md) | 组装(main)/CLI/配置(`AGENT_RUNTIME_*`)/错误码契约/字段分类/工具/部署 | 改装配、配置、错误契约、部署时 | | 12 | | [service-core.md](service-core.md) | 组装(main)/CLI/配置(`AGENT_RUNTIME_*`)/错误码契约/字段分类/工具/部署 | 改装配、配置、错误契约、部署时 | |
| 13 | -| [session-manager.md](session-manager.md) | SM:route/touch/config_sync/config_refresh/cleanup 编排、7 个 Lua、SM 键表 | 改会话编排/配置层时 | | 13 | +| [session-manager.md](session-manager.md) | SM:route/touch/config_sync/config_refresh/cleanup 编排、6 个 Lua、SM 键表 | 改会话编排/配置层时 | |
| 14 | | [resource-manager.md](resource-manager.md) | RM:acquire/后台任务/K8s 适配、6 个 Lua、RM 键表 | 改 Pod 池/扩缩容/清理时 | | 14 | | [resource-manager.md](resource-manager.md) | RM:acquire/后台任务/K8s 适配、6 个 Lua、RM 键表 | 改 Pod 池/扩缩容/清理时 | |
| 15 | | [e2e-test-cases.md](e2e-test-cases.md) | 全部 e2e 用例的场景/输入/预期输出 | 写或跑 e2e 时 | | 15 | | [e2e-test-cases.md](e2e-test-cases.md) | 全部 e2e 用例的场景/输入/预期输出 | 写或跑 e2e 时 | |
| 16 | | `../design/Agent-Runtime-HLD.md` | 架构总览/接口契约/场景 A–N/Redis 键表(语义权威) | 语义不确定时 | | 16 | | `../design/Agent-Runtime-HLD.md` | 架构总览/接口契约/场景 A–N/Redis 键表(语义权威) | 语义不确定时 | |
| 17 | -| `../design/session-manager-design.md` | SM 详细设计(7 个 Lua 全文) | 深挖 SM 设计动机 | | 17 | +| `../design/session-manager-design.md` | SM 详细设计(6 个 Lua 全文) | 深挖 SM 设计动机 | |
| 18 | | `../design/resource-manager-design.md` | RM 详细设计(6 个 Lua 全文) | 深挖 RM 设计动机 | | 18 | | `../design/resource-manager-design.md` | RM 详细设计(6 个 Lua 全文) | 深挖 RM 设计动机 | |
| 19 | 19 | ||
| 20 | ## 一页纸架构 | 20 | ## 一页纸架构 |
| @@ -38,7 +38,7 @@ claw mgr ──config_sync──► ├─ session_manager 持 App,5 个 HTT | |||
| 38 | ## Redis 键前缀总览(逐键明细见各模块 spec) | 38 | ## Redis 键前缀总览(逐键明细见各模块 spec) |
| 39 | 39 | ||
| 40 | ``` | 40 | ``` |
| 41 | -{session_manager}:… SM 编排态(会话四处/scope 闸门/等待队列/候选集/注册表/路由快照) | 41 | +{session_manager}:… SM 编排态(会话四处/scope 闸门/候选集/注册表/路由快照) |
| 42 | {resource_manager}:… RM 编排态(per-scope Pod 池/idle 暖池/deploy 占位/follower 等待室/选主锁) | 42 | {resource_manager}:… RM 编排态(per-scope Pod 池/idle 暖池/deploy 占位/follower 等待室/选主锁) |
| 43 | agent_runtime:job:… 后台任务选主执行锁(main.py:_build_jobs 注册) | 43 | agent_runtime:job:… 后台任务选主执行锁(main.py:_build_jobs 注册) |
| 44 | {agent_runtime:job:…}:winner/candidates:… 选主抽签键(与执行锁同底,hash tag 同槽) | 44 | {agent_runtime:job:…}:winner/candidates:… 选主抽签键(与执行锁同底,hash tag 同槽) |
| @@ -16,7 +16,7 @@ | |||
| 16 | | 层 | 入口 | 规模 | 依赖环境 | 退出码 | | 16 | | 层 | 入口 | 规模 | 依赖环境 | 退出码 | |
| 17 | |---|---|---|---|---| | 17 | |---|---|---|---|---| |
| 18 | | 进程内双实例 | `uv run pytest tests/integration/test_multi_replica.py` | 14 用例 | 无(离线,fakeredis) | pytest 标准 | | 18 | | 进程内双实例 | `uv run pytest tests/integration/test_multi_replica.py` | 14 用例 | 无(离线,fakeredis) | pytest 标准 | |
| 19 | -| **审计实锤回归网** | `uv run pytest tests/integration/test_audit_repro.py` | 16 用例 | 无(离线,fakeredis) | pytest 标准 | | 19 | +| **审计实锤回归网** | `uv run pytest tests/integration/test_audit_repro.py` | 14 用例 | 无(离线,fakeredis) | pytest 标准 | |
| 20 | | 集成冒烟(M6) | `./scripts/integration_smoke.sh`(sidecar 阶段加 `--with-sidecar`,全量规格加 `--with-mounts`) | 121 项断言(全规格形态;2026-09-01 真镜像 0.0.9s 门禁 121/121) | 单实例 server 模式 + 真 Redis/MySQL/K8s | 0/1/2 | | 20 | | 集成冒烟(M6) | `./scripts/integration_smoke.sh`(sidecar 阶段加 `--with-sidecar`,全量规格加 `--with-mounts`) | 121 项断言(全规格形态;2026-09-01 真镜像 0.0.9s 门禁 121/121) | 单实例 server 模式 + 真 Redis/MySQL/K8s | 0/1/2 | |
| 21 | | 多副本 e2e(M7) | `uv run --no-sync python scripts/e2e_multi_replica.py` | 35 项断言 | K8s 多副本 + Service LB + 真 Redis | 0/1/2 | | 21 | | 多副本 e2e(M7) | `uv run --no-sync python scripts/e2e_multi_replica.py` | 35 项断言 | K8s 多副本 + Service LB + 真 Redis | 0/1/2 | |
| 22 | | 压测/浸泡 | `uv run --no-sync python scripts/load_test.py` | 3 场景 | 任意入口(建议 LB) | 0/1 | | 22 | | 压测/浸泡 | `uv run --no-sync python scripts/load_test.py` | 3 场景 | 任意入口(建议 LB) | 0/1 | |
| @@ -53,8 +53,7 @@ | |||
| 53 | 53 | ||
| 54 | | error_code | HTTP | retry_after | 语义 | | 54 | | error_code | HTTP | retry_after | 语义 | |
| 55 | |---|---|---|---| | 55 | |---|---|---|---| |
| 56 | -| `SCOPE_QUEUE_FULL` | 503 | ✅ | 等待队列满,快失败 | | 56 | +| `SCOPE_FULL` | 503 | ✅ | scope 满/达总容量,立即快失败(2026-09 起;原 503 队列满/504 等待超时两码已随等待队列拆除;总预算超限同属过载,粗化为 NO_POD_AVAILABLE) | |
| 57 | -| `SCOPE_FULL_TIMEOUT` | 504 | ✅ | 队列内等待超时(2026-09 起另有推导式总预算 `scope_full_timeout+ready_timeout+10s`,need_acquire 无上界循环同款 504;冷部署不受队列预算误伤) | | ||
| 58 | | `NO_POD_AVAILABLE` | 503 | ✅ | acquire 失败(MaxPodsReached/DeployFailed 映射) | | 57 | | `NO_POD_AVAILABLE` | 503 | ✅ | acquire 失败(MaxPodsReached/DeployFailed 映射) | |
| 59 | | `STATE_UNAVAILABLE` | 503 | ✅ | 状态后端(Redis/DB)连接级故障,handler 层翻译(2026-09;真环境注入手段有限,单测 `test_infra_faults.py` 覆盖) | | 58 | | `STATE_UNAVAILABLE` | 503 | ✅ | 状态后端(Redis/DB)连接级故障,handler 层翻译(2026-09;真环境注入手段有限,单测 `test_infra_faults.py` 覆盖) | |
| 60 | | `CONFIG_NOT_FOUND` | 503 | ❌ | 无匹配规则/模板禁用 | | 59 | | `CONFIG_NOT_FOUND` | 503 | ❌ | 无匹配规则/模板禁用 | |
| @@ -63,8 +62,7 @@ | |||
| 63 | 62 | ||
| 64 | ### 2.3 Redis 真相源(e2e 直接断言的键) | 63 | ### 2.3 Redis 真相源(e2e 直接断言的键) |
| 65 | 64 | ||
| 66 | -- `{session_manager}:scope:{sid}:sessions`(SET,SCARD=scope 闸门)/`:pods`(ZSET,first-fit 候选) | 65 | +- `{session_manager}:scope:{sid}:sessions`(SET,SCARD=scope 闸门)/`:pods`(ZSET,first-fit 候选); |
| 67 | - /`:waiters`(**ZSET**,score=deadline;2026-08-28 起 SET→ZSET,断言用 ZCARD); | ||
| 68 | `{session_manager}:routing:snapshot`(STRING,路由快照) | 66 | `{session_manager}:routing:snapshot`(STRING,路由快照) |
| 69 | - `{session_manager}:session:{sid}`(HASH)/`session_expiry`(ZSET)/`pods:registered`(SET) | 67 | - `{session_manager}:session:{sid}`(HASH)/`session_expiry`(ZSET)/`pods:registered`(SET) |
| 70 | - `{resource_manager}:resource:scope:{sid}:pods|idle|config|deploying`(deploying 为 **ZSET**, | 68 | - `{resource_manager}:resource:scope:{sid}:pods|idle|config|deploying`(deploying 为 **ZSET**, |
| @@ -229,7 +227,7 @@ sidecar 镜像随 `--sidecar-image` 分流:真 jiuwenbox 用 8321 + `JIUWENBOX_L | |||
| 229 | 227 | ||
| 230 | | # | 输入 | 预期(内部状态变迁) | | 228 | | # | 输入 | 预期(内部状态变迁) | |
| 231 | |---|---|---| | 229 | |---|---|---| |
| 232 | -| 18 | —(自然到期,阶段 2 落位起 ≤45s) | `scope:sessions` 清空(sweeper 1s tick 扫 `session_expiry` 到期集→逐个 EVICT+PUBLISH free) | | 230 | +| 18 | —(自然到期,阶段 2 落位起 ≤45s) | `scope:sessions` 清空(sweeper 1s tick 扫 `session_expiry` 到期集→逐个 EVICT) | |
| 233 | | 19 | — | 会话四处全清(session HASH/expiry/pod 集/scope 集) | | 231 | | 19 | — | 会话四处全清(session HASH/expiry/pod 集/scope 集) | |
| 234 | | 20 | — | 空 Pod pass → idle_consider → RM `idle` 暖池 2 个(空闲 Pod 回暖池等待回收/复用) | | 232 | | 20 | — | 空 Pod pass → idle_consider → RM `idle` 暖池 2 个(空闲 Pod 回暖池等待回收/复用) | |
| 235 | | 21 | — | 不变量 5:`pods:registered` 仍 2 个(`--with-sidecar` 时 3,box Pod 长存;`--with-mounts` 时再 +1,mnt Pod 2c 已 route;待 RM 回收后清) | | 233 | | 21 | — | 不变量 5:`pods:registered` 仍 2 个(`--with-sidecar` 时 3,box Pod 长存;`--with-mounts` 时再 +1,mnt Pod 2c 已 route;待 RM 回收后清) | |
| @@ -267,16 +265,16 @@ sidecar 镜像随 `--sidecar-image` 分流:真 jiuwenbox 用 8321 + `JIUWENBOX_L | |||
| 267 | 265 | ||
| 268 | > 留白:create 成功但不 Ready 的失败形态(审计网 C4a)与取消中途形态(C4b)在替身镜像下不可构造(不可拉镜像 create 后立即 Pending,走同一条超时路径但物理清理断言需 FakeK8s);物理孤儿 Pod 的兜底删除未在此断言。 | 266 | > 留白:create 成功但不 Ready 的失败形态(审计网 C4a)与取消中途形态(C4b)在替身镜像下不可构造(不可拉镜像 create 后立即 Pending,走同一条超时路径但物理清理断言需 FakeK8s);物理孤儿 Pod 的兜底删除未在此断言。 |
| 269 | 267 | ||
| 270 | -**阶段 7:F 容量满/队列**(前置:e2e-f(cc=2/pc=1)空闲;max_waiters=2×cc=4;5 项) | 268 | +**阶段 7:F 容量满快失败**(前置:e2e-f(cc=2/pc=1)空闲;5 项;2026-09 起无等待队列) |
| 271 | 269 | ||
| 272 | | # | 输入 | 预期(内部状态变迁) | | 270 | | # | 输入 | 预期(内部状态变迁) | |
| 273 | |---|---|---| | 271 | |---|---|---| |
| 274 | | 29–30 | `route(f1/f2, e2e-f)` 串行 | 各 200,2 Pod 占满(scope 额度 2/2) | | 272 | | 29–30 | `route(f1/f2, e2e-f)` 串行 | 各 200,2 Pod 占满(scope 额度 2/2) | |
| 275 | -| 31 | 5 并发 `route(f-over-0..4)` | ≥1 个 503 `SCOPE_QUEUE_FULL`(队列满快失败:LUA_WAITER_GATE 先清过期再 ZADD 先行+超限自退) | | 273 | +| 31 | 5 并发 `route(f1,f2,f-over-0..2)`(前 2 个为亲和续期) | **恰好 2×200(亲和续期)+ 3×503 `SCOPE_FULL`**(Lua 闸门毫秒级拒新会话;用亲和续期凑 200 是为冷突发下确定性——并发冷 `need_acquire` 输家得 NO_POD_AVAILABLE 而非 SCOPE_FULL) | |
| 276 | -| 32 | 同上 | ≥2 个 504 `SCOPE_FULL_TIMEOUT`(队列内等待至 deadline;信号唤醒或 0.5s 轮询重仲裁均以 route_place 为唯一仲裁) | | 274 | +| 32 | 同上 | 5 并发总耗时 <1s(无等待);拒绝响应均带 `retry_after` | |
| 277 | -| 33 | — | `scope:{FSCOPE}:waiters` ZCARD=0(等待者 finally 出队) | | 275 | +| 33 | — | `scope:{FSCOPE}:waiters` 键不存在(2026-09 拆除净空:拒绝路径零写入) | |
| 278 | 276 | ||
| 279 | -> 留白:丢唤醒窗口(publish 早于 subscribe)与崩溃遗留 waiter 的 deadline 自清在审计网 C7/C9 覆盖(需确定性时序注入);evict 唤醒等待者后「被唤醒者抢到/抢不到」两种落点未分别断言。 | 277 | +> 历史:2026-08 版阶段 7 断言 503 队列满 + 504 等待超时,随等待队列拆除作废(见 §8.1 历史化记录与 docs/feature/2026-09-scope-full-fastfail.md)。 |
| 280 | 278 | ||
| 281 | **阶段 8:H min_idle 热备**(前置:e2e-warm(min_idle=1)、阶段 1b 已预热 1 个、w1 未路由;3 项) | 279 | **阶段 8:H min_idle 热备**(前置:e2e-warm(min_idle=1)、阶段 1b 已预热 1 个、w1 未路由;3 项) |
| 282 | 280 | ||
| @@ -347,9 +345,9 @@ sidecar 镜像随 `--sidecar-image` 分流:真 jiuwenbox 用 8321 + `JIUWENBOX_L | |||
| 347 | | 47 | — | `{session_manager}:pod:*` 注册态全清 | | 345 | | 47 | — | `{session_manager}:pod:*` 注册态全清 | |
| 348 | 346 | ||
| 349 | **阶段 12b:表达式 or 支(清场后确定性验证,1 项)**:原位置在阶段 2 尾,但彼时 e2e-main 已被 | 347 | **阶段 12b:表达式 or 支(清场后确定性验证,1 项)**:原位置在阶段 2 尾,但彼时 e2e-main 已被 |
| 350 | -s1–s3 占满(cc=3),or 支 route 只能排队 504——原断言仅在「部署慢、会话先过期」时序下碰巧 | 348 | +s1–s3 占满(cc=3),or 支 route 只能被 503 快失败拒绝——原断言仅在「部署慢、会话先过期」时序下 |
| 351 | -200(2026-08-27 快跑实测暴露,镜像预分发后快跑必现);移到阶段 12 清场后(Pod/会话全空、配置 | 349 | +碰巧 200(2026-08-27 快跑实测暴露,时为排队 504;2026-09 起为快失败);移到阶段 12 清场后 |
| 352 | -仍在)e2e-main 空闲,确定性 200: | 350 | +(Pod/会话全空、配置仍在)e2e-main 空闲,确定性 200: |
| 353 | 351 | ||
| 354 | | # | 场景 | 输入 | 预期输出/断言 | | 352 | | # | 场景 | 输入 | 预期输出/断言 | |
| 355 | |---|---|---|---| | 353 | |---|---|---|---| |
| @@ -418,10 +416,10 @@ s1–s3 占满(cc=3),or 支 route 只能排队 504——原断言仅在「部署 | |||
| 418 | | # | 输入 | 预期 | | 416 | | # | 输入 | 预期 | |
| 419 | |---|---|---| | 417 | |---|---|---| |
| 420 | | 1–2 | 串行 `route(mr-f1/mr-f2)` | 各 200,占满 | | 418 | | 1–2 | 串行 `route(mr-f1/mr-f2)` | 各 200,占满 | |
| 421 | -| 3 | 8 并发 `route(mr-burst-*)` 经 LB | **0 个 200**(闸门跨副本全局生效) | | 419 | +| 3 | 8 并发 `route(mr-f1,mr-f2,mr-burst-0..5)` 经 LB(前 2 个亲和续期) | **恰好 2×200 + 6×503 `SCOPE_FULL`**(闸门跨副本全局生效;2026-09 起无等待队列) | |
| 422 | -| 4 | 同上 | 4×503 `SCOPE_QUEUE_FULL` + 4×504 `SCOPE_FULL_TIMEOUT`(max_waiters=4;30s 超时属预期) | | 420 | +| 4 | 同上 | 拒绝均带 retry_after;总耗时 <1s | |
| 423 | | 5 | — | `scope:sessions` SCARD=2(不超收) | | 421 | | 5 | — | `scope:sessions` SCARD=2(不超收) | |
| 424 | -| 6 | —(≤45s 轮询) | waiters 清空(ZCARD=0;残留时打印成员便于定位) | | 422 | +| 6 | — | `scope:{scope}:waiters` 键不存在(拆除净空) | |
| 425 | | 7 | — | `deploying` ZCARD=0(占位清空) | | 423 | | 7 | — | `deploying` ZCARD=0(占位清空) | |
| 426 | 424 | ||
| 427 | **S5 幂等跨副本重放**(2 项): | 425 | **S5 幂等跨副本重放**(2 项): |
| @@ -466,20 +464,19 @@ httpx ASGITransport 单事件循环并发驱动。**输入全部走完整 HTTP** | |||
| 466 | |---|---|---|---| | 464 | |---|---|---|---| |
| 467 | | 1 | 身份与共享态 | route 经 A,touch 经 B | instance_id 互异且 RM 镜像;B touch 到 A 建的会话 `touched=true` | | 465 | | 1 | 身份与共享态 | route 经 A,touch 经 B | instance_id 互异且 RM 镜像;B touch 到 A 建的会话 `touched=true` | |
| 468 | | 2 | 交替亲和 | 同 session A→B→A→B route | 恒同 Pod;SCARD=1 | | 466 | | 2 | 交替亲和 | 同 session A→B→A→B route | 恒同 Pod;SCARD=1 | |
| 469 | -| 3 | 跨副本突发不超收 | cc=2/pc=1 占满后 8 并发交替 A/B | 0×200;4×503 队列满 + 4×504 超时;终态 SCARD=2、waiters=0、deploying=0 | | 467 | +| 3 | 跨副本突发不超收 | cc=2/pc=1 占满后 8 并发(2 亲和续期+6 新)交替 A/B | 恰好 2×200 + 6×503 SCOPE_FULL 快失败;终态 SCARD=2、无 waiters 键、deploying=0 | |
| 470 | | 4 | deploy 锁串行化 + follower 复用 | SlowFakeK8s(deploy 0.4s),A/B 并发冷启动 + 追加 s3 | 并发对**恰好 1 次部署**(输家进等待室复用同 Pod);pod1 满后 s3 才第 2 次部署;窗口零重叠;占位/等待室清空 | | 468 | | 4 | deploy 锁串行化 + follower 复用 | SlowFakeK8s(deploy 0.4s),A/B 并发冷启动 + 追加 s3 | 并发对**恰好 1 次部署**(输家进等待室复用同 Pod);pod1 满后 s3 才第 2 次部署;窗口零重叠;占位/等待室清空 | |
| 471 | | 5 | 输家复用暖 Pod | 手持 deploy 锁 + 后台注册 idle Pod 后释放;A route | 返回他副本 Pod;本侧零部署;占位清空 | | 469 | | 5 | 输家复用暖 Pod | 手持 deploy 锁 + 后台注册 idle Pod 后释放;A route | 返回他副本 Pod;本侧零部署;占位清空 | |
| 472 | -| 6 | 跨副本唤醒 | A 占满→A 排队→回拨过期→**B** touch | B 的 touch 返回 `touched=false`(惰性驱逐);A 的等待者 <2s 被唤醒并占释放额度 | | 470 | +| 6 | 幂等跨副本 | 同 request_id A 首发、B 重放 | 响应一致;仅一会话 | |
| 473 | -| 7 | 幂等跨副本 | 同 request_id A 首发、B 重放 | 响应一致;仅一会话 | | 471 | +| 7 | 配置失效传播 | B 改 session_ttl,A 再 route 新会话 | 缓存即 DEL;新会话 expiry=now+90 | |
| 474 | -| 8 | 配置失效传播 | B 改 session_ttl,A 再 route 新会话 | 缓存即 DEL;新会话 expiry=now+90 | | 472 | +| 8 | 单选主验证 | 采样 sm_sweep/rm_autoscale 5.5s | 每 epoch winner∈candidates;candidates 并集=双实例(winner 轮换仅记录) | |
| 475 | -| 9 | 单选主验证 | 采样 sm_sweep/rm_autoscale 5.5s | 每 epoch winner∈candidates;candidates 并集=双实例(winner 轮换仅记录) | | 473 | +| 9 | sweeper 互斥 | 手持 lock:sweep 后 A sweep_once | 直退不误扫;锁释放后补扫完成 | |
| 476 | -| 10 | sweeper 互斥 | 手持 lock:sweep 后 A sweep_once | 直退不误扫;锁释放后补扫完成 | | 474 | +| 10 | 并发收敛 | A/B sweep_once 并发 gather | 无异常;全部老化;锁正常释放;`pods:registered` 不变 | |
| 477 | -| 11 | 并发收敛 | A/B sweep_once 并发 gather | 无异常;全部老化;锁正常释放;`pods:registered` 不变 | | 475 | +| 11 | /healthz | 分别 GET 两 App | 200 + 各自 instance_id | |
| 478 | -| 12 | /healthz | 分别 GET 两 App | 200 + 各自 instance_id | | 476 | +| 12 | follower 上限严格快失败 | cc=8/pc=2,4 并发冷启动(deploy 0.4s) | 2×200(同 Pod)+ 2×503 NO_POD_AVAILABLE(闸门拒);恰好 1 次部署;占位/等待室清空 | |
| 479 | -| 13 | follower 上限严格快失败 | cc=8/pc=2,4 并发冷启动(deploy 0.4s) | 2×200(同 Pod)+ 2×503 NO_POD_AVAILABLE(闸门拒);恰好 1 次部署;占位/等待室清空 | | 477 | +| 13 | leader 失败 follower 不接管 | deploy 慢速失败(0.5s 后抛),2 并发 | 双 503 NO_POD_AVAILABLE;占位/等待室全清 | |
| 480 | -| 14 | leader 失败 follower 不接管 | deploy 慢速失败(0.5s 后抛),2 并发 | 双 503 NO_POD_AVAILABLE;占位/等待室全清 | | ||
| 481 | 478 | ||
| 482 | -### 5.2 审计实锤回归网:`tests/integration/test_audit_repro.py`(16 用例) | 479 | +### 5.2 审计实锤回归网:`tests/integration/test_audit_repro.py`(14 用例;C7/C9 已随 2026-09 场景 F 快失败拆除——等待机制不存在,缺陷面消失) |
| 483 | 480 | ||
| 484 | 2026-08-27 全量审计(五维:状态层原子性/停机并发/外部契约/测试缺口/业务语义)产出 | 481 | 2026-08-27 全量审计(五维:状态层原子性/停机并发/外部契约/测试缺口/业务语义)产出 |
| 485 | ~40 条假设,先以本套用例**实锤**再修(16/16 全部成立;修复后全绿转回归网)。 | 482 | ~40 条假设,先以本套用例**实锤**再修(16/16 全部成立;修复后全绿转回归网)。 |
| @@ -502,19 +499,17 @@ FakeK8s 忽略探测参数的保真度缺口(按 (ip,port,path) 判定——2026 | |||
| 502 | | C4b | deploy 取消不留物理孤儿(C4) | 替身在 create 后卡在等 Ready 窗口 | 0.3s 后 cancel(优雅停机语义) | Redis 占位清(⑤)+ 物理 Pod 清(deploy 层清理契约) | | 499 | | C4b | deploy 取消不留物理孤儿(C4) | 替身在 create 后卡在等 Ready 窗口 | 0.3s 后 cancel(优雅停机语义) | Redis 占位清(⑤)+ 物理 Pod 清(deploy 层清理契约) | |
| 503 | | C5 | REGISTER 步失败不占容量(C5) | max_pods=2/pod_concurrency=1;注入 REGISTER 步抛错一次 | 三连 route(第 1 个触发注错) | 第 3 个会话仍能扩到 2 个真实 Pod——注册步失败不得永久虚占 max_pods | | 500 | | C5 | REGISTER 步失败不占容量(C5) | max_pods=2/pod_concurrency=1;注入 REGISTER 步抛错一次 | 三连 route(第 1 个触发注错) | 第 3 个会话仍能扩到 2 个真实 Pod——注册步失败不得永久虚占 max_pods | |
| 504 | | C6 | 死 Pod 不回放(C6) | 会话 Pod 经 watch 判死 PURGE(注册全清) | 同 `request_id` 重试 route(网关幂等重试语义) | 拿到**新** Pod——idem 命中须校验存活,不得复活死 Pod 喂死地址 | | 501 | | C6 | 死 Pod 不回放(C6) | 会话 Pod 经 watch 判死 PURGE(注册全清) | 同 `request_id` 重试 route(网关幂等重试语义) | 拿到**新** Pod——idem 命中须校验存活,不得复活死 Pod 喂死地址 | |
| 505 | -| C7 | 丢唤醒后轮询重仲裁(C7) | scope 满(cc=1),sess_2 入队等待 | 注入:释放信号恰在 subscribe 完成前发布(天然并发窗口) | ≤0.5s 轮询重仲裁拿到空闲额度,而非空等满 2s 后 504 | | ||
| 506 | | C8 | 亲和 Pod 被清后换绑(C8) | 按 notify_pod_dead 真实顺序驱动窗口:evict 已枚举会话→窗口内新会话落上该 Pod→cleanup 收口(info 已清) | 再 route 该会话(线程+硬 join 有界 5s) | 2s 内换绑新 Pod——refresh 须有存活守卫,不得无限自旋(每圈续期 expiry 的绝症) | | 502 | | C8 | 亲和 Pod 被清后换绑(C8) | 按 notify_pod_dead 真实顺序驱动窗口:evict 已枚举会话→窗口内新会话落上该 Pod→cleanup 收口(info 已清) | 再 route 该会话(线程+硬 join 有界 5s) | 2s 内换绑新 Pod——refresh 须有存活守卫,不得无限自旋(每圈续期 expiry 的绝症) | |
| 507 | -| C9 | 幽灵 waiter 自清(C9) | scope 满;经真实闸门塞 2 个 **deadline 已过期**的幽灵 waiter(模拟崩溃副本遗留) | 新请求 route | 能入队、等满 0.5s 得 504——而非被永久 503 SCOPE_QUEUE_FULL | | ||
| 508 | | C10a | 畸形 int 400(C10) | 配置在 | config_sync `session_ttl="abc"` | 400 VALIDATION——int 畸形不得裸抛 ValueError 成 500 | | 503 | | C10a | 畸形 int 400(C10) | 配置在 | config_sync `session_ttl="abc"` | 400 VALIDATION——int 畸形不得裸抛 ValueError 成 500 | |
| 509 | -| C10b | 0 值策略字段拒绝(C10) | 配置在 | config_sync `pod_concurrency=0` | 400 VALIDATION——0 值是拒绝服务配置(满 max_pods 个必用不上的 Pod 后永久 scope_full) | | 504 | +| C10b | 0 值策略字段拒绝(C10) | 配置在 | config_sync `pod_concurrency=0` | 400 VALIDATION——0 值是拒绝服务配置(满 max_pods 个必用不上的 Pod 后每个 route 恒 503 SCOPE_FULL) | |
| 510 | | C11 | 幻影 scope 收敛(C11) | 双 scope 下发;注入 drain 推送(min_idle=0 无 spec)失败一次(模拟滚动重启中断扩散③) | 删 scope 后 2×(sync+autoscale) | 幻影 scope 零 Pod——drain 目标集以 RM 已知 scope 为真源,重试必补推 | | 505 | | C11 | 幻影 scope 收敛(C11) | 双 scope 下发;注入 drain 推送(min_idle=0 无 spec)失败一次(模拟滚动重启中断扩散③) | 删 scope 后 2×(sync+autoscale) | 幻影 scope 零 Pod——drain 目标集以 RM 已知 scope 为真源,重试必补推 | |
| 511 | | C12 | 重试补跑软摘除(C12) | 会话在老版本 Pod 上;注入软摘除步失败一次(写 DB 后) | 同载荷重试 config_sync + route 新会话 | 新会话不落旧版本 Pod——候选集版本收敛是声明式每拍重算,不由 diff 驱动 | | 506 | | C12 | 重试补跑软摘除(C12) | 会话在老版本 Pod 上;注入软摘除步失败一次(写 DB 后) | 同载荷重试 config_sync + route 新会话 | 新会话不落旧版本 Pod——候选集版本收敛是声明式每拍重算,不由 diff 驱动 | |
| 512 | | C13 | 路径归一(C13) | — | config_sync `sse_path="api/v1/stream"`(缺前导 /) | URL `port=8080` 可解析且 path=/api/v1/stream——缺 / 会拼出 `http://ip:8080api/...` 非法端口 → 健康 Pod 被探死循环 | | 507 | | C13 | 路径归一(C13) | — | config_sync `sse_path="api/v1/stream"`(缺前导 /) | URL `port=8080` 可解析且 path=/api/v1/stream——缺 / 会拼出 `http://ip:8080api/...` 非法端口 → 健康 Pod 被探死循环 | |
| 513 | 508 | ||
| 514 | > **本层留白(需真环境/真镜像)**:C3/C4/C13 的替身保真度依赖可编程 K8s 双打—— | 509 | > **本层留白(需真环境/真镜像)**:C3/C4/C13 的替身保真度依赖可编程 K8s 双打—— |
| 515 | > 真 K8s 的删除异步性、CrashLoopBackOff 判死、真 AgentServer 的 health 契约仍只在 | 510 | > 真 K8s 的删除异步性、CrashLoopBackOff 判死、真 AgentServer 的 health 契约仍只在 |
| 516 | -> 门禁层可见;C9 的硬崩形态(kill -9 遗留 waiter)在本层用过期 deadline 模拟, | 511 | +> 门禁层可见;硬崩遗留(RM deploy 占位)在本层用 deadline 过期模拟,真进程硬崩的 |
| 517 | -> 真进程硬崩的端到端未覆盖;reclaim 与 acquire 的 TOCTOU(在用 Pod 被回收)两轮 | 512 | +> 端到端未覆盖(SM waiter 面 2026-09 已拆除);reclaim 与 acquire 的 TOCTOU(在用 Pod 被回收)两轮 |
| 518 | > 审计均确认存在但确定性复现需真时序,列为已知 P1 遗留(feature 记录遗留清单)。 | 513 | > 审计均确认存在但确定性复现需真时序,列为已知 P1 遗留(feature 记录遗留清单)。 |
| 519 | 514 | ||
| 520 | ### 5.3 强制刷新自然老化网:`tests/integration/test_force_refresh.py`(4 用例) | 515 | ### 5.3 强制刷新自然老化网:`tests/integration/test_force_refresh.py`(4 用例) |
| @@ -536,13 +531,12 @@ FakeK8s 忽略探测参数的保真度缺口(按 (ip,port,path) 判定——2026 | |||
| 536 | |---|---|---| | 531 | |---|---|---| |
| 537 | | `route` | 每 scope 50 并发容量,8 会话/scope 轮转 route | 全 200;p50/p90/p99 报告;冷启动 max≈deploy 等待 | | 532 | | `route` | 每 scope 50 并发容量,8 会话/scope 轮转 route | 全 200;p50/p90/p99 报告;冷启动 max≈deploy 等待 | |
| 538 | | `route_touch` | 同上 + 半数请求 touch 保活 | 同上(实测 2 副本 LB:16186 请求**零错误**,p50 7.3ms,p99 24.3ms) | | 533 | | `route_touch` | 同上 + 半数请求 touch 保活 | 同上(实测 2 副本 LB:16186 请求**零错误**,p50 7.3ms,p99 24.3ms) | |
| 539 | -| `queued` | cc=2/pc=2 小容量模板 | 直方图出现 `SCOPE_QUEUE_FULL`/`SCOPE_FULL_TIMEOUT` **属预期**(排队路径被刻意打到),只报告不判败 | | 534 | +| `queued` | cc=2/pc=2 小容量模板 | 直方图出现 503 `SCOPE_FULL` **属预期**(容量满快失败路径被刻意打到),只报告不判败 | |
| 540 | 535 | ||
| 541 | - 速率:闭环(并发全速)或开环(`--rps` 令牌桶);`--duration` 长 → 浸泡 | 536 | - 速率:闭环(并发全速)或开环(`--rps` 令牌桶);`--duration` 长 → 浸泡 |
| 542 | (`--report-interval` 周期增量报告);Ctrl-C 优雅部分报告。 | 537 | (`--report-interval` 周期增量报告);Ctrl-C 优雅部分报告。 |
| 543 | - 安全边界:全程只走 HTTP,无 FLUSHDB、默认不调 cleanup 端点(会删 ns 下全部 | 538 | - 安全边界:全程只走 HTTP,无 FLUSHDB、默认不调 cleanup 端点(会删 ns 下全部 |
| 544 | AgentServer Pod);模板/规则/组按 run-id 命名空间化,靠 TTL 老化。 | 539 | AgentServer Pod);模板/规则/组按 run-id 命名空间化,靠 TTL 老化。 |
| 545 | -- 服务侧注意:每排队请求持一条 Redis pubsub 连接(`maxclients` 默认 10k)。 | ||
| 546 | 540 | ||
| 547 | --- | 541 | --- |
| 548 | 542 | ||
| @@ -595,7 +589,7 @@ uv run --no-sync python scripts/load_test.py \ | |||
| 595 | 589 | ||
| 596 | ## 8. 已知语义差异与暂缓项 | 590 | ## 8. 已知语义差异与暂缓项 |
| 597 | 591 | ||
| 598 | -### 8.1 「经多副本 LB 跑 M6 冒烟 63/65」排查实录(2026-08-18,已全部修复) | 592 | +### 8.1 「经多副本 LB 跑 M6 冒烟 63/65」排查实录(2026-08-18,已全部修复;**2026-09 历史化**——第 1 条的等待队列机制已随场景 F 快失败整体拆除,该变量/碰撞问题不复存在;第 2 条 cleanup 403/404 语义仍有效) |
| 599 | 593 | ||
| 600 | 两处失败当初被初步归因为「多副本冷突发语义」,**深入排查后证明均另有根因**—— | 594 | 两处失败当初被初步归因为「多副本冷突发语义」,**深入排查后证明均另有根因**—— |
| 601 | 都是「该集群部署与单实例环境的配置差异」,修复后经 LB 稳定 **65/65**: | 595 | 都是「该集群部署与单实例环境的配置差异」,修复后经 LB 稳定 **65/65**: |
| @@ -644,8 +638,8 @@ uv run --no-sync python scripts/load_test.py \ | |||
| 644 | (§5.2 C3 在替身层已覆盖逻辑,真 AgentServer 契约须真镜像门禁)。 | 638 | (§5.2 C3 在替身层已覆盖逻辑,真 AgentServer 契约须真镜像门禁)。 |
| 645 | - deploy 失败/取消的物理孤儿在真 K8s 的清理(§5.2 C4 为替身双打;真 K8s 的 | 639 | - deploy 失败/取消的物理孤儿在真 K8s 的清理(§5.2 C4 为替身双打;真 K8s 的 |
| 646 | 删除异步性、节点驱逐形态未覆盖)。 | 640 | 删除异步性、节点驱逐形态未覆盖)。 |
| 647 | -- 进程硬崩(kill -9)遗留的端到端自愈:waiter/占位 deadline 在本层以过期 | 641 | +- 进程硬崩(kill -9)遗留的端到端自愈:RM deploy 占位 deadline 在本层以过期 |
| 648 | - deadline 模拟,真进程硬崩 + 重启收敛未覆盖。 | 642 | + deadline 模拟,真进程硬崩 + 重启收敛未覆盖(SM waiter 面 2026-09 已拆除)。 |
| 649 | - 依赖故障注入:Redis 运行中闪断、DB 不可达时的 route/config_sync/启动降级、 | 643 | - 依赖故障注入:Redis 运行中闪断、DB 不可达时的 route/config_sync/启动降级、 |
| 650 | K8s API 5xx/超时、create 409 名字冲突——全部零覆盖。 | 644 | K8s API 5xx/超时、create 409 名字冲突——全部零覆盖。 |
| 651 | - 真实配置形状的**镜像侧行为**(2026-08-28 阶段 0m/2c 已把配置渲染与挂载链路 | 645 | - 真实配置形状的**镜像侧行为**(2026-08-28 阶段 0m/2c 已把配置渲染与挂载链路 |
| @@ -662,11 +656,11 @@ uv run --no-sync python scripts/load_test.py \ | |||
| 662 | - reclaim 与 acquire 复用的 TOCTOU(在用 Pod 被物理删):两轮审计确认, | 656 | - reclaim 与 acquire 复用的 TOCTOU(在用 Pod 被物理删):两轮审计确认, |
| 663 | LUA_RELEASE/PURGE 无「仍在 idle」守卫;确定性复现需真时序。 | 657 | LUA_RELEASE/PURGE 无「仍在 idle」守卫;确定性复现需真时序。 |
| 664 | - route 全程不重 resolve:config_sync 与长 route 并发时按旧 scope/模板仲裁、 | 658 | - route 全程不重 resolve:config_sync 与长 route 并发时按旧 scope/模板仲裁、 |
| 665 | - 对已删 scope 白等满超时。 | 659 | + 对已删 scope 按旧快照仲裁一次(route 已无等待面,窗口=单次请求,2026-09 起)。 |
| 666 | - B 类调小(session/cc)低于现活跃数的回收语义:存量超限不驱逐只老化回落, | 660 | - B 类调小(session/cc)低于现活跃数的回收语义:存量超限不驱逐只老化回落, |
| 667 | 行为未固化无断言。 | 661 | 行为未固化无断言。 |
| 668 | - 停机预算:uvicorn 无 `timeout_graceful_shutdown` + 30s 宽限 vs 300s 在飞请求 | 662 | - 停机预算:uvicorn 无 `timeout_graceful_shutdown` + 30s 宽限 vs 300s 在飞请求 |
| 669 | - ——优雅停机可能整体跳过(占位/等待已 deadline 自愈,锁内中断仍依赖 finally)。 | 663 | + ——优雅停机可能整体跳过(占位 deadline 自愈,锁内中断仍依赖 finally;SM 等待已拆除,在飞面仅剩 acquire)。 |
| 670 | 664 | ||
| 671 | **C. 加速手法残留(与「零回拨」硬标准的差距)** | 665 | **C. 加速手法残留(与「零回拨」硬标准的差距)** |
| 672 | - 冒烟阶段 4(D 老化)与阶段 5(K reclaim 主体)仍回拨 expiry/idle_since—— | 666 | - 冒烟阶段 4(D 老化)与阶段 5(K reclaim 主体)仍回拨 expiry/idle_since—— |
| @@ -26,7 +26,7 @@ AgentServer 原生支持 GET /health 后补验(单测已覆盖)。 | |||
| 26 | D 老化回收 / E 保活 → 阶段 4(session_ttl 到期 → idle 暖池) | 26 | D 老化回收 / E 保活 → 阶段 4(session_ttl 到期 → idle 暖池) |
| 27 | K reclaim 自治 → 阶段 5(回拨 idle_since,真删 K8s Pod) | 27 | K reclaim 自治 → 阶段 5(回拨 idle_since,真删 K8s Pod) |
| 28 | I acquire deploy 失败分支 → 阶段 6(不可拉镜像 → NO_POD_AVAILABLE + 占位清) | 28 | I acquire deploy 失败分支 → 阶段 6(不可拉镜像 → NO_POD_AVAILABLE + 占位清) |
| 29 | - F 容量满(队列 + 快失败/超时) → 阶段 7(并发 5 请求 → 503 + 504) | 29 | + F 容量满(快失败) → 阶段 7(并发 5 请求 → 2×200 + 3×503 SCOPE_FULL) |
| 30 | H min_idle 热备 → 阶段 8(autoscale 预建热备 Pod) | 30 | H min_idle 热备 → 阶段 8(autoscale 预建热备 Pod) |
| 31 | G 死 Pod 会话清洗 / J 死 Pod 探测 → 阶段 9(kubectl 删 Pod → watch 兜底 → 会话失效) | 31 | G 死 Pod 会话清洗 / J 死 Pod 探测 → 阶段 9(kubectl 删 Pod → watch 兜底 → 会话失效) |
| 32 | N 半死探测 → 【暂缓】待 AgentServer 原生支持 GET /health | 32 | N 半死探测 → 【暂缓】待 AgentServer 原生支持 GET /health |
| @@ -573,8 +573,8 @@ async def stage2_route_abc(c: Client, r) -> dict: | |||
| 573 | first.get("pod_id") == second.get("pod_id") | 573 | first.get("pod_id") == second.get("pod_id") |
| 574 | and await r.scard(f"{SM_PREFIX}scope:{MAIN}:sessions") == 3) | 574 | and await r.scard(f"{SM_PREFIX}scope:{MAIN}:sessions") == 3) |
| 575 | # 表达式 or 支(user 白名单跨 group 命中 e2e-main)在阶段 12b 验证: | 575 | # 表达式 or 支(user 白名单跨 group 命中 e2e-main)在阶段 12b 验证: |
| 576 | - # 此处 e2e-main 已被 s1–s3 占满(cc=3),or 支 route 只会排队 504—— | 576 | + # 此处 e2e-main 已被 s1–s3 占满(cc=3),or 支 route 只会 503 SCOPE_FULL |
| 577 | - # 原位置仅在「部署慢、s1 先过期」的时序下碰巧 200(2026-08-27 快跑实测 504)。 | 577 | + # 快失败——原位置仅在「部署慢、s1 先过期」的时序下碰巧 200。 |
| 578 | return state | 578 | return state |
| 579 | 579 | ||
| 580 | 580 | ||
| @@ -1114,26 +1114,26 @@ async def stage6_deploy_failure(c: Client, r) -> None: | |||
| 1114 | 1114 | ||
| 1115 | 1115 | ||
| 1116 | async def stage7_queue(c: Client, r) -> None: | 1116 | async def stage7_queue(c: Client, r) -> None: |
| 1117 | - print("\n== 阶段 7:场景 F —— 容量满:等待队列 + 快失败/超时 ==") | 1117 | + print("\n== 阶段 7:场景 F —— 容量满:立即快失败(2026-09 起 SCOPE_FULL)==") |
| 1118 | for sid in ("f1", "f2"): # 2 Pod 全满(cc=2, pc=1, max=2) | 1118 | for sid in ("f1", "f2"): # 2 Pod 全满(cc=2, pc=1, max=2) |
| 1119 | code, raw, _ = await c.post("route", session_id=sid, group="e2e-f") | 1119 | code, raw, _ = await c.post("route", session_id=sid, group="e2e-f") |
| 1120 | check(f"F-部署并占满 {sid}", code == 200 and raw.get("pod_id"), str(raw)[:120]) | 1120 | check(f"F-部署并占满 {sid}", code == 200 and raw.get("pod_id"), str(raw)[:120]) |
| 1121 | t0 = time.monotonic() | 1121 | t0 = time.monotonic() |
| 1122 | + # 5 并发:f1/f2 亲和续期(200)+ 3 个新会话撞 scope 闸门(503 SCOPE_FULL) | ||
| 1122 | results = await asyncio.gather(*[ | 1123 | results = await asyncio.gather(*[ |
| 1123 | - c.post("route", session_id=f"f-over-{i}", group="e2e-f") for i in range(5)]) | 1124 | + c.post("route", session_id=sid, group="e2e-f") |
| 1124 | - codes = [code for code, _, _ in results] | 1125 | + for sid in ("f1", "f2", "f-over-0", "f-over-1", "f-over-2")]) |
| 1125 | - queue_full = [b for code, _, b in results if code == 503 | ||
| 1126 | - and b.get("error_code") == "SCOPE_QUEUE_FULL"] | ||
| 1127 | - full_timeout = [b for code, _, b in results if code == 504 | ||
| 1128 | - and b.get("error_code") == "SCOPE_FULL_TIMEOUT"] | ||
| 1129 | took = time.monotonic() - t0 | 1126 | took = time.monotonic() - t0 |
| 1130 | - check("F-队列满(max_waiters=2×cc=4)→ 快失败 503 SCOPE_QUEUE_FULL", | 1127 | + ok = [raw for code, raw, _ in results if code == 200] |
| 1131 | - len(queue_full) >= 1, f"codes={codes} ({took:.0f}s)") | 1128 | + rejected = [(code, b.get("error_code")) for code, _, b in results if code != 200] |
| 1132 | - check("F-队列内等待 → 超时 504 SCOPE_FULL_TIMEOUT", | 1129 | + check("F-并发 5 = 恰好 2×200(亲和续期)+ 3×503 SCOPE_FULL", |
| 1133 | - len(full_timeout) >= 2, str([b.get("error_code") for _, _, b in results])) | 1130 | + len(ok) == 2 and rejected == [(503, "SCOPE_FULL")] * 3, |
| 1134 | - await asyncio.sleep(1) | 1131 | + str([(code, b.get("error_code")) for code, _, b in results])) |
| 1135 | - check("F-等待者全部出队(finally 清理)", | 1132 | + check("F-快失败不等待(<1s)", took < 1.0, f"{took:.2f}s") |
| 1136 | - await r.zcard(f"{SM_PREFIX}scope:{FSCOPE}:waiters") == 0) | 1133 | + check("F-过载响应带 retry_after", |
| 1134 | + all(b.get("retry_after") for _, _, b in results if b.get("error_code"))) | ||
| 1135 | + check("F-拆除净空:无 waiters 键", | ||
| 1136 | + not await r.keys(f"{SM_PREFIX}scope:{FSCOPE}:waiters")) | ||
| 1137 | 1137 | ||
| 1138 | 1138 | ||
| 1139 | async def stage8_warm(c: Client, r) -> dict: | 1139 | async def stage8_warm(c: Client, r) -> dict: |
| @@ -1380,8 +1380,9 @@ async def stage12_reconcile_cleanup(c: Client, r) -> None: | |||
| 1380 | async def stage12b_or_branch(c: Client, r) -> None: | 1380 | async def stage12b_or_branch(c: Client, r) -> None: |
| 1381 | """表达式 or 支(阶段 12 清场后):e2e-main 此时空闲,or 支命中可确定性 200。 | 1381 | """表达式 or 支(阶段 12 清场后):e2e-main 此时空闲,or 支命中可确定性 200。 |
| 1382 | 1382 | ||
| 1383 | - 原位置(阶段 2 尾)被 s1–s3 占满 cc=3,or 支 route 只能排队 504——只有 | 1383 | + 原位置(阶段 2 尾)被 s1–s3 占满 cc=3,or 支 route 只能 503 SCOPE_FULL |
| 1384 | - 「部署慢、会话先过期」的时序下碰巧 200(2026-08-27 快跑实测暴露)。 | 1384 | + 快失败——只有「部署慢、会话先过期」的时序下碰巧 200(2026-08-27 快跑 |
| 1385 | + 实测暴露,时为排队 504;2026-09 起为快失败)。 | ||
| 1385 | """ | 1386 | """ |
| 1386 | print("\n== 阶段 12b:表达式 or 支(清场后确定性验证)==") | 1387 | print("\n== 阶段 12b:表达式 or 支(清场后确定性验证)==") |
| 1387 | code, raw_vip, _ = await c.post("route", session_id="s-vip", | 1388 | code, raw_vip, _ = await c.post("route", session_id="s-vip", |