agent-exec transport d90f144a0e3d59039289174e486a8816/e9bc28611767
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
# 独立代码审查:fix/remaining-hardcoded-ports(剩余同类端口硬编码改为内核分配,与 PR #668 同形)
|
||||
|
||||
- 审查对象:`444fb1dee2..HEAD`(HEAD=70c810a)。本单元 diff = `.task/fix-remaining-hardcoded-ports.md`(host 豁免路径)+ `tests/_port_alloc.py`(新增 13 行)+ `tests/test_port_alloc.py`(新增 24 行)+ `tests/test_svc.py`(4 处用例、6 个 `svc --port` 调用点 + 批量更新 2 个 seed 端口)+ `tests/test_svc_realcombo.py`(3 个 realcombo 用例 + import)。产品侧 `scripts/svc.sh` 零改动(`git diff 444fb1dee2..HEAD --stat -- scripts/` 为空)。
|
||||
- 审查者身份:全新会话独立审查,未参与实现;本会话除本报告外不写任何仓库文件;无 git add/commit/push,无 PR 操作。工作区(detached HEAD 70c810a)在报告写入前后 `git status --porcelain` 仅有本报告。
|
||||
- 依据:PR #668 审查(.task/reviews/2026-10-20-svc-flake-review.md)N4 记录的同类脆弱面 =「rdm 组真实绑口的 18842/18843/18844 与 realcombo 9471/9473/9474」;本单元即对该类做同形治理(bind(("127.0.0.1", 0)) 内核分配)。
|
||||
|
||||
## 结论:PASS(无阻塞项;非阻塞建议 3 条)
|
||||
|
||||
判定标准逐项:契约用例不再依赖固定端口(实测全数转换,动态临时口 32768-60999 与全仓剩余固定口不相交);helper 与 PR #668 及仓内既有 5 处夹具(test_svc_recover.py:187 `free_fixture_port` 等)逐字节同构,非新模式;受保护内容(`scripts/svc.sh`、`register_pg`/DSN 18951、`test_svc_recover.py`、`test_task_pr_check.py`、`test_task_triage.py`)零改动;补丁未外溢(`git diff --name-only` 仅 5 个文件)。realcombo 三例因需真实二进制环境(SVC_TEST_REAL / SVC_REAL_RDM_BIN / SVC_RDM_ORIGIN,本单元禁网)为 NOT_RUN,以代码走读 + 本地只读模块跑通(16 skipped 门槛确认)替代。
|
||||
|
||||
---
|
||||
|
||||
## 重点 1:helper `free_test_port` 的设计
|
||||
|
||||
**写法**(tests/_port_alloc.py):每次调用新开 `AF_INET/SOCK_STREAM` 套接字 → `bind(("127.0.0.1", 0))` 由内核从临时口范围分配 → `getsockname()[1]` 取实际端口 → `with` 退出即 close(return 表达式在 with 内求值,关闭发生在返回前)。不池化、不 SO_REUSEPORT,与 `free_fixture_port`(test_svc_recover.py:187-190)及 PR #668 写法同构。docstring 如实声明「closes it before returning」「Ports are not pooled」。
|
||||
|
||||
**「真空闲」的三类场景实测**(合成测试 /tmp/port_alloc_synthetic.py,独立于守护测试,3 次运行全 7/7 PASS):
|
||||
|
||||
1. **外部占用**:先取 helper 返回端口 P1 并真 LISTEN 占住(SO_ACCEPTCONN=1 确认),再次调用 helper 与后续 50 次调用共 51 次,**0 次命中 P1**,且每个返回值都能立即 bind+listen 成功——内核 `bind(0)` 的冲突检测以套接字表为准,与占用者是本进程还是外部进程无关,故 in-process LISTEN 持有者可作外部占用的等价证据。被占端口本身确实可用(我们正是用它完成 LISTEN)。
|
||||
2. **TIME_WAIT**:制造真实 TIME_WAIT(服务端先关,端口 45387/53933/42733 三轮),先证明无 SO_REUSEADDR 的裸 rebind 报 EADDRINUSE(TIME_WAIT 确实阻断重绑),再测 helper 50 次调用 **0 次命中** TIME_WAIT 端口——内核分配阶段即跳过,helper 不会把调用方推进一个必然 bind 失败的端口。
|
||||
3. **多调用区分度**:连续 500 次无持有调用 → 497 个不同值(远处可重复 3 次),但**相邻两次重复 0/499**——内核临时口游标使相邻调用实际上总不同。守护测试的 `assertNotEqual(first, second)` 依赖的正是相邻性,实证可靠(另 3×50 连续调用亦全不同)。远处重复(如两次 alloc 间隔数百次调用后撞回同值)无害:前一次的 reservation 早已 close 且从未 LISTEN 过、无 TIME_WAIT、无持有者,调用方 bind 必成功。
|
||||
4. **子进程仍持套接字**:与场景 1 同表(内核冲突表不分进程),daemon 未退时 helper 不会返回其端口;helper 自身的 reservation 从未 accept/连接,close 后不留 TIME_WAIT。
|
||||
|
||||
**残余 TOCTOU 窗口(非阻塞,与 PR #668 同等接受)**:reservation close 到 daemon 真正 bind 之间端口不被保留。这是该模式(含仓内既有 5 处)的固有窗口,docstring 已如实声明。失效模式是响亮失败而非假绿:端口真被抢时 `check_port_free`(svc.sh:1575)报「端口被占用(非本服务)」非零退出 → `self.svc(check=True)` 断言带全量 stdout 失败,可归因(`port_owner`)。动态口 32768-60999(实测 `/proc/sys/net/ipv4/ip_local_port_range`)与仓内全部固定口(9431-9472、188xx-189xx)不相交,套件内不会互相踩。套件级彻底解法(传 fd/重试)属独立增量,不建议本任务扩围。
|
||||
|
||||
## 重点 2:三个 realcombo 用例端口一致性
|
||||
|
||||
逐例核验(tests/test_svc_realcombo.py):
|
||||
|
||||
- `test_U3_real_agent_identity_lifecycle_sqlite`:`:172 port = free_test_port()` 一次 → `seed_real("rc1", port)`(写 svc.env 的 addr 与 exec `--addr 127.0.0.1:port`)→ `svc start` → `http_ok(port)`(:186)→ 两次裸 restart(svc restart 沿用登记端口)→ `http_ok(port)`(:211)→ stop。三处同源同值。✓
|
||||
- `test_U3_real_rdm_binary_lifecycle_sqlite`:`:219` 一次 alloc → seed_real/start/http_ok(:226)全用 `port`。✓
|
||||
- `test_U3_real_rdm_full_deploy_track`:`:245 agent_port = free_test_port()` 一次 → `deploy --port str(agent_port)` → 裸 restart 后 `http_ok(agent_port)`(:269)。伴生 `rcfweb` 的端口不归本 alloc:svc.sh `deploy_rdm_companion_ui`(svc.sh:3141)以无 --port 子进程走 `derive_port`(8800+hash%300 冲突递增),测试从 `kv("rcfweb","addr")` 读回再探活(:263)——读写分离正确,alloc 只管 agent 口。✓
|
||||
|
||||
文件内 9471/9473/9474 零残留(grep 实测)。**alloc→bind 窗口在 realcombo 最宽**(seed_real 里的 `initialize_store` 会起停一个独立临时口的 fixture 服务,秒级),但该窗口语义与重点 1 相同:碰撞即响亮失败(start 门禁报占用),不会假绿;环境门禁(无真实二进制即 skip)意味着 CI 常规路径根本不执行,风险面进一步收窄。NOT_RUN 如实标注。
|
||||
|
||||
## 重点 3:test_svc.py 六个 `svc --port` 调用点
|
||||
|
||||
六个调用点(行号为 HEAD)全部经 `free_test_port()` 取值:`:904`(rdm e2e deploy)、`:931`(rdm 缺 web 失败 deploy)、`:944/:948/:954`(source-switch 三连)、`:970`(rdm update 轨道 deploy)。零 18841-18844 残留(grep 实测)。
|
||||
|
||||
**「同一测试多次 svc 调用端口应不同」的精确判定**:唯一的同测试多次调用是 `test_source_switch_requires_flag`,它 **有意** 共享一次 alloc(`:942 port = str(free_test_port())`,:944/:948/:954 三处同值)。这不违反意图:三个调用是对**同一实例 sw1** 的顺序 deploy → 被拒 deploy → 换源 redeploy;svc.sh deploy 流程「停旧 → 原子切换 → 启动」(agent_do_deploy,svc.sh:3211 起)在重绑前先停掉旧服务,`check_port_free`/`reclaim_own_port`(svc.sh:1575/1115)兜底本服务残留——端口不会「被自身 reuse 占满」,因为旧持有者在重绑前已被 svc.sh 自己停掉;被拒调用在 `require_allow_source_switch` 门禁就失败,从未 bind。该流程在改前用固定 18844 即已如此,本次仅换端口来源,实测通过(见证据 5)。每实例多端口场景(批量更新 a/b)则是每实例各 alloc 一次(`:673`),两次相邻调用必不同(重点 1.3)。判定:**无「每调用必须独立 alloc」的违例;共享 alloc 是对该用例语义的最小忠实转换。**
|
||||
|
||||
另有 `test_update_preserves_binding` 等用例经 update/restart 沿登记端口重启——属 svc.sh 持久化语义,端口来源已由 seed 处 alloc/保留,不在六调用点之列。
|
||||
|
||||
## 重点 4:守护测试 test_port_alloc 的覆盖度
|
||||
|
||||
**能否真的失败——能,两类回归可拦**:
|
||||
- 退化成固定/缓存端口(如误改为 `return 18844`):`assertNotEqual(first, second)` 即刻失败——合成证据 4 用假 helper 复现了该失败路径。
|
||||
- 返回不可绑端口(如未来改成返回他者持有口):循环内 `bind+listen` 直接抛 OSError → 用例 ERROR。
|
||||
- 相邻两次拿到同值(游标回绕):assertNotEqual 失败(实测概率 ≈0,0/499 相邻重复,且该回绕对调用方无害——见重点 1.3,但作为回归信号仍成立)。
|
||||
|
||||
**覆盖缺口(非阻塞)**:守护测试未覆盖「端口已被外部占用时 helper 避开它」与 TIME_WAIT 避让——本审查以 /tmp 合成测试补证(外部 LISTEN 持有者 51 次调用 0 命中;TIME_WAIT 端口 50 次 0 命中),结论为 helper 行为正确,仅测试面未固化。alloc→bind TOCTOU 窗口则任何确定性测试都无法覆盖(内核不为已 close 的 reservation 保留端口),与 PR #668 的接受一致。若后续想固化,可在 `test_two_calls_...` 前置一步「先占住第一次的返回值再取第二次」的断言,属独立增量。
|
||||
|
||||
## 重点 5:范围核对
|
||||
|
||||
- `git diff --name-only 444fb1dee2..HEAD` 实测仅 5 文件:`.task/fix-remaining-hardcoded-ports.md` + 4 个测试文件;`scripts/` 零差异(`git diff --stat -- scripts/` 空)。✓
|
||||
- `tests/test_svc_recover.py` 不动:正确——其已有同构 `free_fixture_port`(:187),全部用例早已内核分配。✓
|
||||
- `tests/test_task_pr_check.py` 不动:正确——`ThreadingHTTPServer(("127.0.0.1", 0))`(:90)与 `closed_port_url()`(:100-102)本就内核分配,无固定绑口。✓
|
||||
- `tests/test_task_triage.py` 不动:正确——夹具为内联 `bind(("127.0.0.1", 0))`(:230-232);文中的数字端口(如 30319)仅出现在 URL 脱敏字符串断言里,不 bind。✓
|
||||
- `register_pg`/DSN 18951 保留:正确——`register_pg`(test_svc.py:242 起)仅 `write_text` 写 svc.env 元数据,从不建/绑 socket,18951 不可能引发占口型偶发失败。实测 base `444fb1dee2` 的 753-754 两行与 HEAD 逐字一致(行号平移到 757-758,系文件头新增 4 行 import 所致;本单元 diff 中 18951 出现次数为 0)。✓
|
||||
|
||||
**残余同类面(非阻塞,均不在本单元契约内,仅记录供后续增量)**:test_svc.py 仍有会在 `svc start/update` 时真实绑口的固定 seed 端口(18809、18832、18834-18837、18952,共 7 用例);`test_svc_plugins.py` 的 `deploy --port 18981/18982`、`test_svc_identity.py` 9431-9435、`test_svc_u2.py`/`test_svc_user_binding.py`/`test_svc_rdm_admin.py`/`test_svc_lifecycle_isolation.py` 的 943x-947x seed 端口同属 N4 类。动态临时口与它们不相交,本单元的转换不与之交互、也不掩盖其问题。
|
||||
|
||||
---
|
||||
|
||||
## 已运行的验证命令与结果(均只读;工作区除本报告外零修改)
|
||||
|
||||
| # | 命令 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | `git diff --stat 444fb1dee2..HEAD` 与 `--name-only` | 5 文件(任务卡 + 4 测试文件);`-- scripts/` 为空 |
|
||||
| 2 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_port_alloc -v` | **OK**(Ran 1, 0.000s) |
|
||||
| 3 | `python3 /tmp/port_alloc_synthetic.py`(合成测试:占住 helper 返回口再调用、51 次避让、TIME_WAIT 制造与避让、假 helper 触发断言;3 次运行) | 3 次 **exit=0,7/7 PASS**(典型值:held=36633 recall=49051;50-calls distinct=49-50/50 且 0 次命中被占口) |
|
||||
| 4 | `python3 -c` 500 次连续调用统计 | 497/500 不同;**相邻重复 0/499** |
|
||||
| 5 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest`(5 个改造用例:rdm e2e、rdm 缺 web、source-switch 三连、rdm update 轨道、批量更新双实例) | **OK**(Ran 5, 8.557s) |
|
||||
| 6 | `PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_realcombo -v` | OK(skipped=16:SVC_TEST_REAL/SVC_REAL_RDM_BIN/SVC_RDM_ORIGIN 未设 → realcombo 三例 NOT_RUN,禁网无法取得真实二进制) |
|
||||
| 7 | `grep` 残留扫描(9471/9473/9474/18841-18844)与 `free_test_port` 调用点清单 | 两文件零残留;调用点 = test_svc.py:673/904/931/942/970 + realcombo:172/219/245 |
|
||||
| 8 | `git show 444fb1dee2:tests/test_svc.py` 753-754 行 vs HEAD 757-758;本单元 diff 中 `18951` 计数 | 内容逐字一致;diff 计数 0 |
|
||||
| 9 | `cat /proc/sys/net/ipv4/ip_local_port_range` | 32768 60999(与全部固定测试口不相交) |
|
||||
| 10 | `git status --porcelain` / `git stash list`(测试运行前后) | 空(测试全在 /tmp 临时目录内自包含) |
|
||||
|
||||
## 判定标准对照
|
||||
|
||||
- 契约用例仍会因端口占用偶发失败:未发生(6 个 --port 调用点 + 批量更新 2 seed + realcombo 3 例全数转内核分配;动态口与仓内固定口不相交;占口失效模式为响亮失败)→ 非阻塞。
|
||||
- helper 在外部占用/TIME_WAIT 下取错端口:未发生(合成实测 0 命中)→ 非阻塞。
|
||||
- 受保护内容被改(svc.sh、register_pg 18951、recover/pr_check/triage 三文件):未发生(零 diff 实测)→ 非阻塞。
|
||||
- 补丁外溢到契约外文件:未发生(5 文件实测,其一为 host 豁免的任务卡)→ 非阻塞。
|
||||
- 结论:**PASS**。realcombo 三例运行级验证 NOT_RUN(环境门禁 + 禁网),以代码走读与模块级 skip 确认替代,已如实标注。
|
||||
Reference in New Issue
Block a user