Files
model-platform/HANDOVER.md
T
2026-08-14 19:30:25 +08:00

11 KiB
Raw Blame History

Handover — 近期变更与待办

面向接手运维 / 二次开发的人。记录最近几次 commit 的动机、改动范围、未尽事项, 以及「线上有旧数据时怎么过渡」。详细架构见 ARCHITECTURE.md;本仓库的开发规约 见 DEVELOP.md


1. 最近 5 个 commit(按时间倒序)

139f2c0 — fix: delete errortrash 路径统一 + 删除后 DB 字段更新)

背景:删除文件统一改为移动到 trash 桶后,存在三个连锁 bug:

  1. local 模式 trash 路径双重 datatrash_key = f"{item.bucket_name}/{item.object_key}"。 local 模式下 bucket_name 是绝对路径(如 /data/workspace),最终落到 data/trash/data/workspace/{ws_id}/{user_id}/...
  2. s3 模式桶名 / 路径名错位.env 之前是 S3_WORKSPACE_BUCKET=workspaces(复数), 代码 default / PURPOSE_BUCKETS / USAGE_TYPE_TO_PURPOSE 全部是单数 workspace。 跨环境部署产生 KeyError: 'workspaces'(软删时 object_stores[item.bucket_name] 抛错)。
  3. 软删后 storage_objects 路径字段未更新:只翻 trash_key / object_status / deleted_at / is_deletedbucket_name / object_key / storage_uri 仍指向源, 与物理位置不一致,purge_trash_object 必须读旧 trash_key 才能定位 trash 文件。

改动5 个文件,44+/24-):

文件 改动
backend/src/backend/services/storage.py trash_key = f"{source_purpose}/{item.object_key}";软删成功后更新 bucket_name / object_key / storage_uri 指向 trash 位置
backend/src/backend/storage_api.py restore_objectitem.object_key 直接定位 trash,按 /source_purpose,写回源桶,更新 DB 字段指回 source;purge_trash_object(actual_bucket_name("trash"), item.object_key) 删除,加 bucket_name != trash 的 409 防御
common/src/common/storage/factory.py 引入 USAGE_TYPE_TO_PURPOSE 字典(替代 storage_api.py 的本地定义)
common/src/common/storage/__init__.py 导出 USAGE_TYPE_TO_PURPOSE
.env.example S3_WORKSPACE_BUCKET=workspacesworkspaceS3_VERSION_BUCKET=versionsversionS3_RUN_LOG_BUCKET=run-logsrun-log(统一单数)

注意点

  • workspaces_root() 函数名 / WORKSPACES_ROOT 环境变量名不动(这是 path 概念,不是 bucket 名)。
  • Workspaces 数据库表名 / auth.py"workspaces" JSON key(API 响应)不动(这些不是桶名)。
  • visibility="workspace" 是 ACL Literal,不动。

2e070fa — fix: pre check(前端 script 创建/上传前置校验)

CreateScriptModal.tsx+25 行)— 文件名 / workspace_id / 类型前置校验。 ScriptsPage.tsx+1/ scriptWorkspaceStore.ts+19/-10)— store action 增加 precheck 步骤,提前拦截无效请求。

5c5dec9 — fix: allows_same_name_different_parent

允许同名 script 出现在不同父目录下。scripts.py+32/-?)、services/storage.pystorage_api.py 微调;test_scripts.py 新增 57 行回归测试。

5d49ff5 — fix: file upload error(大重写)

修复 file upload 路径上一连串 schema / API / 上传流程的不一致。改动面最大 (14 个文件,872+/604-):

  • 新增索引 / 调整唯一约束(去掉了 scripts 和 data_resources 的旧 unique index
  • 把多个小 migration 合并进 e1f2a3b4c5d6_rebuild_baseline.py 一份大 baseline
  • resources.py / scripts.py 上传流程对齐
  • test_resources.py / test_scripts.py 大量回归
  • API.md 增加 3 行

c65d6dc — fix: build error

backend/Dockerfile 单行修复(镜像构建报错)。


2. Trash 修复详解(commit 139f2c0

2.1 修复后的语义

  • trash_keyf"{source_purpose}/{item.object_key}"source_purpose 来自 USAGE_TYPE_TO_PURPOSE[item.usage_type],与 item.bucket_name 解耦 — 避免 local 模式绝对路径泄漏 + 命名错位风险。

  • 软删后 storage_objects 行的 7 个字段同步

    字段 软删前 软删后
    bucket_name 源桶 trash 桶
    object_key 源 key trash key
    trash_key NULL trash key
    storage_uri 源 URI trash URIbuild_storage_uri 重建)
    object_status available deleted
    is_deleted 0 1
    deleted_at NULL <utcnow>
  • 物理文件落点

    模式 源文件(被删) trash 文件(新落)
    S3 s3://<source_bucket>/<key> s3://trash/<source_purpose>/<key>
    local <LOCAL_STORAGE_BASE_DIR>/<source_purpose>/<key> <LOCAL_STORAGE_BASE_DIR>/trash/<source_purpose>/<key>

    source_purpose{workspace, version, run_log}

2.2 restore_object 反向流程

data = await object_stores[trash_bucket].get(item.object_key)        # 从 trash 读
source_purpose, _, source_key = item.object_key.partition("/")       # 拆 source_purpose
target_bucket = actual_bucket_name(source_purpose)                    # 还原目标桶
await object_stores[target_bucket].put(source_key, data)              # 写回源

item.bucket_name = target_bucket
item.object_key  = source_key
item.storage_uri = build_storage_uri(target_bucket, source_key)
# trash_key 保留不动 → 未来 reaper 可据此清理 trash 里的 orphan 副本
item.object_status = "available"
item.deleted_at = None

trash 文件不会被 restore 主动删除(保留现有安全语义:restore 失败时 数据仍在 trash 里有兜底)。未实现的 reaper 未来可扫 object_status='available' AND trash_key IS NOT NULL 清理 orphan。

2.3 purge_trash_object 防御

加 409 防御:if item.bucket_name != actual_bucket_name("trash"): raise 409。 避免误删源数据(用 item.object_key 直接删 trash 文件,不再读旧 trash_key)。

2.4 USAGE_TYPE_TO_PURPOSE 提升到 common 层

backend/storage_api.pybackend/services/storage.py 都依赖这个映射。 提到 common/storage/factory.py 后两边都 from common.storage import USAGE_TYPE_TO_PURPOSE 避免互相 import 循环。


3. 线上数据兼容(重要)

修复后新行为对 DB 里已有的旧行不感知。如果部署环境之前是 S3_WORKSPACE_BUCKET=workspaces(复数)且已上传过文件, storage_objects.bucket_name 列里会有复数残留。新代码 default 是单数, object_stores dict key 也是单数,访问旧行仍会 KeyError: 'workspaces'

部署新代码前必须先跑一次数据修复 SQL(先 SELECT 看数量,确认是同一个 deployment):

-- 1. 先看现状(按 backend 区分)
SELECT storage_backend, bucket_name, COUNT(*)
FROM storage_objects
WHERE object_status = 'available'
GROUP BY storage_backend, bucket_name
ORDER BY storage_backend, bucket_name;

-- 2a. S3 部署:把复数残留修正成单数
UPDATE storage_objects
SET bucket_name = 'workspace'
WHERE bucket_name = 'workspaces'
  AND object_status = 'available';

-- 2b. local 部署:把绝对路径里的复数残留修正成单数
UPDATE storage_objects
SET bucket_name = '/data/workspace'
WHERE bucket_name = '/data/workspaces'
  AND object_status = 'available';

只改 availabledeleted 行已经在 trash 里、bucket_name 会被新代码改写,无需处理。 如果同时改 version / run_log 桶残留,按相同模式跑。


4. 本地验收清单

# 1. 语法
uv run python -m compileall common/src backend/src runtime/src schedule/src

# 2. 测试
uv run --package backend pytest backend/tests -q
# 期望:30 passed

# 3. 命名一致性
grep -rn '"workspaces"\|"versions"\|"run-logs"' \
  --include='*.py' --include='*.yml' --include='*.env*' \
  backend/src common/src docker-compose.yml .env .env.example 2>/dev/null
# 期望:除 `__tablename__ = "workspaces"`DB 表名)和 `auth.py` 的 JSON key 外,零结果

# 4. 单复数对齐
grep -n 'S3_WORKSPACE_BUCKET\|S3_VERSION_BUCKET\|S3_RUN_LOG_BUCKET\|S3_TRASH_BUCKET' \
  .env.example
# 期望:全部 = 单数(workspace / version / run-log / trash

5. 回归测试覆盖

场景 触发方式 期望
同名 script 不同父目录 test_soft_delete_object_sets_is_deleted 旁边的新测试 两条 row 同名共存
软删 → 物理文件落到 trash 桶的 source_purpose/{key} 手工:上传 → 删 → mc ls s3/trash/ 文件在;源桶里已删
软删后 storage_objects 7 字段同步 软删后 SELECT * FROM storage_objects WHERE storage_object_id=... 见 §2.1 表格
restore_object 把 trash 文件写回源桶 软删 → restore → 查源桶 源桶恢复;trash 文件保留(orphan
purge_trash_object 拒绝误删源数据 手工:把 bucket_name 改成源桶后调 purge 409

6. 监控项(建议加)

  1. storage_objectsbucket_nameactual_bucket_name(purpose) 不一致的比例 (按 usage_type 分组 COUNT)。生产环境应长期趋近于 0。
  2. object_status='deleted'bucket_name != actual_bucket_name('trash') 的行数 — 应为 0(软删失败回滚残留)。
  3. trash 桶目录大小 vs object_status='deleted' 行数(孤儿文件比例)。 没实现 reaper 前这个会单调上升。

7. 已知未实现项

  • Trash reapers3_trash_retention_days 已声明,但代码没有扫表清理。 restore_object 故意保留 trash 文件作 orphan,等 reaper 兜底。 第一次实现建议在 schedule/src/schedule/ 加一个 tick job
    SELECT storage_object_id FROM storage_objects
    WHERE deleted_at < UTC_TIMESTAMP() - INTERVAL :retention_days DAY
      AND object_status = 'deleted'
    LIMIT 100;
    
    然后调 /internal/v1/admin/trash/purge
  • 跨 backend trash 兼容:当前只在 item.storage_backend == settings.storage_backend 的行上做物理移动。异构 backend 的旧行(少)软删时只翻 DB 状态,trash 文件实际没移动。 远程 8.153.151.51:8890 应该都是同一 backend,目前无影响。
  • 历史 trash_key 兼容读:新代码用 item.object_key 读 trash回退到 item.trash_key。如果迁移前有按旧 trash_key = f"{bucket_name}/{object_key}" 写入的已删行,object_key 还是源 keyrestore 会找不到 trash 文件(404)。 处理:在 restore_object 加 fallback — 先按 item.object_key 试,失败再按 item.trash_key 试一次(兼容窗口期)。建议在 reaper 上线后删除 fallback。

8. 仍未合并到 develop 的本地改动

M frontend/vite.config.ts       (4+/2-)
?? data/                        (untracked — 本地 docker volume 残留,可忽略)

frontend/vite.config.ts 的改动不在本次 trash 修复范围内,等下次 commit 一起提。