# MVP v2.5 开发计划(TDD 驱动) 本文是 v2.5 的**工程化开发计划**,强调“先写测试,再写实现”(TDD),并将每个里程碑拆成**可独立验收**的小闭环。 输入依据: - 路线图:`specs/mvp/mvp_roadmap_v2.md` - v2.5 设计:`specs/mvp/v2.5/v2.5_design.md` - v2.5 API 草案:`specs/mvp/v2.5/v2.5_api.md` - v2.5 验收:`specs/mvp/v2.5/v2.5_acceptance.md` v2.5 约束(已确认): - **不扩展 TaskSpec**:沿用 v2.0/v2.1 的 YAML 结构化字段与语义。 - **不支持自定义 reward function / 不支持用户自定义 verl 代码**。 - 训练输入(verl 代码、HF cache、datasets)统一使用 `/private/common/...`。 - 多用户隔离 v2.5 **先只隔离 jobs 输出目录**:`/private/users//jobs//...`。 --- ## 0. TDD 规范(所有功能都遵循) ### 0.1 测试分层 1) **单元测试(fast)** - 纯 Python 逻辑:DB、鉴权、ID、路径派生、head.json 解析/TTL、watchdog 决策逻辑。 - 目标:不依赖真实 Ray、不依赖 docker、不依赖网络。 2) **组件测试(中等)** - FastAPI 路由:使用 `fastapi.testclient.TestClient`(现有 v2.0 已采用)。 - 目标:验证 auth/权限隔离、API 行为、状态机。 3) **端到端(慢/手工或脚本)** - 在 `argus@h1` 上通过 scripts/compose 跑一次“head publish → worker auto-connect → API submit”闭环。 - 目标:验证无状态 worker + watchdog 的真实行为。 ### 0.2 测试约定 - 测试目录:`src/mvp/py/tests/` - 新增功能必须先补齐测试用例,并让其在未实现时失败(红)。 - 实现最小改动让测试变绿(绿)。 - 重构/去重复(重构)。 > 注:现有测试通过 `src/mvp/py/tests/conftest.py` 注入 ray stub,确保单测不依赖真实 ray 包;v2.5 新增模块也应复用此模式。 --- ## 1. 里程碑拆分(v2.5 = 4 个可验证闭环) ### M1:User 表/Token 表 + 基础鉴权(不影响现有内部 token 兼容) **目标** - 引入 user/token 的持久化与鉴权映射(token → user_id)。 - 兼容现有 `Authorization: Bearer ` 的“单租户模式”,避免一次性破坏 v2.0 用法: - v2.5 可以先支持两种 token 模式: - legacy:环境变量 `MVP_INTERNAL_TOKEN`(全局单租户); - user token:DB 内签发 token(多用户)。 - admin 能创建用户、签发 token、禁用用户。 **TDD 用例(先写测试)** 单测: - `test_user_db_create_disable()` - 创建用户 ACTIVE;禁用后状态变为 DISABLED;重复创建返回冲突或幂等(按最终约定)。 - `test_token_hashing()` - 签发 token 时 DB 中只保存 hash,不保存明文。 API 测试(TestClient): - `test_admin_create_user_and_issue_token()` - admin token 可创建用户并签发 token(明文 token 只返回一次)。 - `test_disabled_user_token_rejected()` - 用户被禁用后,使用旧 token 调用 API 返回 401/403。 **实现落点(建议模块)** - `argus.service.auth`:token 校验与 user_id 解析(兼容 legacy 模式) - `argus.service.db`:新增 `users`、`api_tokens` 表与 CRUD - `argus.service.app`:新增 user 管理 endpoints(admin scope) - `configs/dev.yaml`:补充 admin token/env 相关配置(保持 YAML 风格) **验收点** - `v2.5_acceptance.md`:U1 可通过自动化 API 测试覆盖。 --- ### M2:Task 绑定 user_id + API 可见性隔离(仍不改 TaskSpec) **目标** - 提交 task 时由 token 推导 `user_id`,写入 `tasks.user_id`。 - task 查询/取消/日志默认只允许 owner;他人访问返回 404(避免泄露存在性)。 - queue 默认只返回当前用户队列;admin 可查询全局队列(可选)。 **TDD 用例(先写测试)** 单测: - `test_tasks_table_has_user_id()`:创建任务必须落 `user_id`,且 `list_queue(user_id=...)` 只返回该用户任务。 API 测试: - `test_task_visibility_isolated()` - user A 创建 task;user B 查询 `/api/v2/tasks/{id}` 返回 404; - user B cancel/logs 也返回 404。 - `test_queue_isolated()` - A/B 各自创建 task;`GET /api/v2/queue` 只看到自己的。 **实现落点** - `argus.service.app`:为 task endpoints 增加 user scope - `argus.service.db`:tasks 表增加 user_id 字段、索引、按 user 过滤的查询方法 - `argus.service.scheduler`:pick_next_runnable_task 等仍按“全局 FIFO”或“按 user FIFO” - v2.5 先保持“全局 FIFO”最简单(但 API queue 视角是按 user 过滤)。 **验收点** - `v2.5_acceptance.md`:U2 可通过 API 测试覆盖。 --- ### M3:Jobs 输出目录按 user 隔离(只改输出,不改输入) **目标** - Ray Job 的 job_root 目录由服务端统一计算到: - `/private/users//jobs//...` - TaskSpec 内与输入相关的路径字段必须是 `/private/common/...`(v2.5 输入统一 common)。 - 任何用户无法通过 TaskSpec 指定输出写到非 user jobs 目录(避免越权写)。 **TDD 用例(先写测试)** 单测: - `test_job_root_derivation_per_user()` - 给定 user_id 与 ray_submission_id,派生 job_root 固定且正确。 - `test_reject_non_common_inputs()` - TaskSpec 中 train_file / val_file / code_path / hf 路径等若不以 `/private/common/` 开头则拒绝(HTTP 400)。 API 测试: - `test_job_dir_written_under_user_jobs()` - 提交 task 后,在 DB 或 submit payload 中能看到 job_root 在 user jobs 下(可通过 mock RayJobTool.submit 捕获 spec)。 **实现落点(建议最小侵入)** - 在 service 层派生 `job_root` 并注入到 RayJobTool/builders(而不是让用户从 TaskSpec 指定)。 - RayJobTool `_job_dir()` 改为接收“job_root 生成器”或直接接收 `job_root` 参数(由服务层提供)。 - 目标:保持 RayJobTool 的职责清晰:提交 Ray job;路径策略由 service 决定。 **验收点** - `v2.5_acceptance.md`:U3/U4 可通过 API/单测覆盖。 --- ### M4:Stateless Ray Node Pool(head.json + worker watchdog)+ 端到端脚本验证 **目标** - head 启动后持续写入 `/private/ray/discovery//head.json`(包含 TTL)。 - worker 容器内运行 watchdog(或启动脚本 + watchdog),无需平台显式传 head 地址: - 读取 head.json(存在且未过期)→ `ray start --address=:` - head.json 变化 → `ray stop` + `ray start` 重连 - 在 dev 环境(docker compose)提供一键脚本复现(e2e)。 **TDD 用例(先写测试)** 单测(不跑真实 ray): - `test_head_json_read_validate_ttl()` - 文件不存在/过期 → 返回“不可用” - 未过期 → 返回 head 地址 - `test_watchdog_decision_on_change()` - head_ip 变化 → 触发重连动作 - only updated_at 变化(地址不变)→ 不重连(或按策略重连,需确定) 组件/脚本级测试(可选): - 如果 watchdog 用 Python 实现,可对“执行命令”层做 stub(不真正跑 `ray start`),只验证会调用什么命令。 端到端脚本(手工/慢): - 提供脚本 `scripts/run_all_v25_stateless.sh`(命名示例): 1) 起 head(Ray head + API) 2) 启动 head publisher(写 head.json) 3) 起 2 个 worker(每个 4 GPU),worker 只跑 watchdog,不传 head 地址 4) `ray status` 显示 1 head + 2 worker 且 GPU=8 5) 通过 API 创建用户/签发 token,提交 PPO/GRPO/SFT 6) 重启 head(或更新 head.json 指向新地址)验证 worker 自动重连 **实现落点(建议实现策略)** 为了可测试性(TDD),推荐把“读 head.json/判定 TTL/生成 ray start 命令”做成 Python 模块: - `argus.ray.discovery`:read/write head.json(原子写、TTL) - `argus.ray.worker_watchdog`:watch loop(polling + change detection),执行命令可注入(便于单测 stub) 脚本层保持薄: - `scripts/` 负责 docker exec / compose 编排与进程守护; - watchdog 进程由容器内 python 模块运行(更可测、更易移植到生产平台的 entrypoint/command)。 **验收点** - `v2.5_acceptance.md`:A1/A2/A3 主要通过 e2e 脚本 + dashboard/日志验证。 --- ## 2. 回归策略(确保 v2.0 不被破坏) 在 v2.5 过程中保留并持续回归以下用例(至少单测覆盖): - 旧的内部 token 模式仍可访问 `GET /api/v2/queue` 与提交 task(若决定保留兼容)。 - scheduler 的“资源不足 → PENDING_RESOURCES → 延迟重试”行为不变(现有 `test_scheduler.py` 覆盖)。 - `ray entrypoint_resources` 强制 driver 落 worker(继续使用 `worker_node` 自定义资源)。 --- ## 3. 交付清单(代码/脚本/文档) ### 3.1 代码 - user/tokens:DB schema + auth + API endpoints - tasks:绑定 user_id + 权限隔离 - job_root:按 user jobs 输出目录派生(输入仍 common) - discovery/watchdog:head.json + worker 自愈 ### 3.2 scripts(dev e2e) - head:启动 Ray head + head publisher - workers:以无状态方式启动(不传 head addr)+ watchdog - `run_all`:一键跑通(含 API submit + 查询 + cancel + 观察队列) ### 3.3 文档 - 更新 `specs/mvp/v2.5/*`(设计/API/验收/开发计划) - 补充 `src/mvp/README.md` 的 v2.5 使用方式(如需要) --- ## 4. 关键待确认点(开始实现前必须定稿) 1) **legacy token 是否继续兼容** - 方案 A:保留 `MVP_INTERNAL_TOKEN`(单租户)+ 新增 user token(多租户) - 方案 B:v2.5 直接切换到 user token(破坏兼容,但更清晰) 2) **调度公平性** - v2.5 先全局 FIFO(简单);后续 v3 再引入 per-user 公平调度/配额。 3) **head.json 的生产写入者** - 方案 A:与 API 同进程线程(最少组件) - 方案 B:独立进程(更独立、易运维)