mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-03 14:37:36 +08:00
refactor: complete durable transfer execution settlement
This commit is contained in:
@@ -69,16 +69,16 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
|
||||
| 指标 | 当前值 | 解释 |
|
||||
|---|---:|---|
|
||||
| 宿主 Python 模块 / 内部依赖边 | 837 / 6,834 | `dependency-baseline.json` 当前快照 |
|
||||
| 宿主 Python 模块 / 内部依赖边 | 843 / 6,883 | `dependency-baseline.json` 当前快照 |
|
||||
| 非平凡 SCC | 2 | 新增 Chain 包根环;另一个是隔离的 29 模块 TMDB 移植包环 |
|
||||
| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 |
|
||||
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
|
||||
| Module Contract | 217 specs / 215 methods / 265 calls | 动态方法名为 0;内部 planning 合同不进入插件调度,旧 transfer 只保留 provider ABI |
|
||||
| Module Contract | 217 specs / 215 methods / 266 calls | 动态方法名为 0;内部 planning 合同不进入插件调度,旧 transfer 只保留 provider ABI |
|
||||
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
|
||||
| Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 |
|
||||
| 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 |
|
||||
| 全量 mypy 历史债务 | 11,983 / 601 文件 | strict frontier 当前只覆盖 41 个文件,且 ratchet 已新增 2 个错误 |
|
||||
| Ruff 历史诊断 | 934 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| Ruff 历史诊断 | 929 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
|
||||
| 覆盖率低水位 | Application 78.24%,Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |
|
||||
|
||||
### 3.3 热点文件
|
||||
@@ -194,8 +194,8 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
返回空后再以第二次 CAS 提交 `planned`;planned 重放只消费冻结上下文和目标。
|
||||
- `S1-L1.3 Lease 与恢复调度`:`VERIFIED`。已交付 token fencing 的
|
||||
claim/lease/heartbeat/attempt、过期接管、固定退避的唯一恢复入口和有界关闭 owner。
|
||||
- `S1-L1.4 幂等执行与终态结算`:`PLANNED`。交付文件/历史幂等、唯一 retry owner 和
|
||||
`manual_review` 语义。
|
||||
- `S1-L1.4 幂等执行与终态结算`:`VERIFIED`。已交付稳定 operation ledger、严格结果探测、
|
||||
唯一 retry owner、`manual_review` 人工判定和 history/pending/outbox 同 UoW 终态结算。
|
||||
- `S1-L1.5 E3 全链收口`:`PLANNED`。完成崩溃矩阵、兼容验收与旧路径删除。此叶交付前,
|
||||
ARCH-102 父项保持“执行中”,不得以局部绿色宣称 E3 完成。
|
||||
|
||||
@@ -216,10 +216,18 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
fencing。启动和同进程恢复共享唯一 scheduler,确定性失败按固定轮询退避,关闭时 worker、replay、
|
||||
lease release 和 heartbeat 都由有界生命周期 owner 持有。损坏投影以无有效租约 CAS 留痕,同错不
|
||||
重复刷写,且不会阻塞后续健康任务。
|
||||
- `TransferPending` 仍缺少逐步骤执行结果和 `manual_review`,因此还不能判定“文件已移动、历史未提交”
|
||||
等外部结果未知的后续中间态。
|
||||
- 这与 `docs/adr/0007-background-action-reliability.md:123-139` 对 E3 的稳定身份、步骤状态、
|
||||
lease/heartbeat 和人工恢复要求不一致。
|
||||
- `S1-L1.4` 已增加 `TransferExecutionStep` 独立账本:每一步在副作用前冻结 intent 和稳定
|
||||
operation ID,以 lease + attempt 双 CAS 提交结果;重启遇到遗留 `STARTED` 时必须先严格探测,
|
||||
只有 `NOT_APPLIED` 能轮换 attempt 自动重试,`UNKNOWN/CONFLICT` 进入 `manual_review`。
|
||||
- 文件 cleanup、目录创建、版本发现/删除、覆盖目标删除、目标物化和跨存储 move 的源删除均已拆为
|
||||
可重放步骤;本地复制使用完整内容比较,远端结果证据不足时不会伪造 exactly-once。
|
||||
- 成功、失败及覆盖拒绝均通过 task-aware writer 在一个 UoW 内提交 history、pending、step cleanup
|
||||
与可选 outbox;revision 和确定性 occurrence key 使“文件已移动、历史未提交”在恢复后只补历史,
|
||||
不重复文件副作用。历史/API/Agent 重试只登记 durable retry intent,由唯一 scheduler 重新 claim。
|
||||
- 管理员人工判定 API 只公开 `not_applied` 与带结果证据的 `applied`,并持久记录操作者、理由、结论
|
||||
和 revision;无租约人工路径不能直接伪造失败终态。
|
||||
- 以上实现已满足 `docs/adr/0007-background-action-reliability.md:123-139` 对 E3 稳定身份、步骤状态、
|
||||
lease/heartbeat 和人工恢复的阶段性要求;完整崩溃矩阵与兼容收口仍由 `S1-L1.5` 验收。
|
||||
|
||||
**目标与步骤**
|
||||
|
||||
@@ -228,9 +236,9 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
- [x] 初始登记保存稳定源身份、版本化请求和状态;目标与有序操作在纯规划完成后以 planning
|
||||
checkpoint 原子更新,任何文件副作用不得早于该提交。
|
||||
- [x] 增加 claim/lease/heartbeat/attempt 与过期接管,同一任务同时只能有一个 worker owner。
|
||||
- [ ] 设计幂等文件操作和历史提交;只有所有必要步骤达到持久终态后才能删除记录。
|
||||
- [ ] 在持久状态机与现有失败历史/AI retry 之间指定唯一 retry owner,定义旧记录迁移和兼容规则。
|
||||
- [ ] E3 失败使用持久 `failed/manual_review`、最后稳定 checkpoint 和补偿边界,不直接套用 E2
|
||||
- [x] 设计幂等文件操作和历史提交;只有所有必要步骤达到持久终态后才能删除记录。
|
||||
- [x] 在持久状态机与现有失败历史/AI retry 之间指定唯一 retry owner,定义旧记录迁移和兼容规则。
|
||||
- [x] E3 失败使用持久 `failed/manual_review`、最后稳定 checkpoint 和补偿边界,不直接套用 E2
|
||||
Outbox 的 dead-letter 语义;禁止按年龄通用清理 pending。
|
||||
- [x] 当前 admission/planning 数据模型变更均配套 Alembic migration,并验证升级、降级和中断重跑。
|
||||
|
||||
@@ -238,10 +246,10 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`
|
||||
|
||||
- [x] 登记后、内存入队前崩溃,重启可继续。
|
||||
- [x] 持久登记成功但内存入队失败,重启可继续。
|
||||
- [ ] 文件移动后、历史提交前崩溃,在支持稳定身份/幂等操作的存储上不重复移动且可补齐历史。
|
||||
- [ ] worker 未知异常和 lease 超时后保留可诊断状态。
|
||||
- [ ] 重复回放、重复消息和人工重试都保持幂等。
|
||||
- [ ] 外部存储返回结果未知时进入 `manual_review`,不得伪装成 exactly-once 成功。
|
||||
- [x] 文件移动后、历史提交前崩溃,在支持稳定身份/幂等操作的存储上不重复移动且可补齐历史。
|
||||
- [x] worker 未知异常和 lease 超时后保留可诊断状态。
|
||||
- [x] 重复回放、重复消息和人工重试都保持幂等。
|
||||
- [x] 外部存储返回结果未知时进入 `manual_review`,不得伪装成 exactly-once 成功。
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m pytest \
|
||||
|
||||
@@ -704,8 +704,8 @@ flowchart LR
|
||||
|
||||
| 指标 | 当前值 |
|
||||
|---|---:|
|
||||
| Python 模块 | 837 |
|
||||
| 内部导入边 | 6,834 |
|
||||
| Python 模块 | 843 |
|
||||
| 内部导入边 | 6,883 |
|
||||
| 非平凡 SCC | 2(`ARCH-107` 临时 Chain 包根环;精确 containment 的 TMDB 移植包环) |
|
||||
| Direct egress | 66(12 条待迁移债务,54 条精确 containment) |
|
||||
| Module Contract V2 spec | 217(其中 215 个进入 `run_module` 观察面) |
|
||||
|
||||
@@ -95,7 +95,7 @@ G-ARCH 只有在以下条件全部满足后才可完成:
|
||||
| S1-L1.1 Durable admission | `VERIFIED` | S0 | Application-owned typed Port + DB adapter + migration 落地;先持久 commit 再入队,入队失败保留可恢复记录;宿主不再通过 raw/`Any` `TransferPendingOper` 处理 admission |
|
||||
| S1-L1.2 Planning checkpoint | `VERIFIED` | S1-L1.1 | 版本化输入与指纹先持久化;无 legacy provider 时以 `accepted -> planned` CAS 提交完整计划,有 provider 时先提交 `provider_pending`,全部返回空后再以第二次 CAS 提交 `planned`;重放只执行冻结目标,所有文件副作用晚于对应 checkpoint commit |
|
||||
| S1-L1.3 Lease 与恢复调度 | `VERIFIED` | S1-L1.2 | claim/lease/heartbeat/attempt 与过期接管规则落地;启动回放和同进程恢复共用唯一调度入口,同一任务同时只有一个 worker owner |
|
||||
| S1-L1.4 幂等执行与终态结算 | `PLANNED` | S1-L1.3 | 文件操作、历史提交和 checkpoint 可重放;唯一 retry owner 生效,未知外部结果进入 `manual_review`,仅完整终态删除 pending |
|
||||
| S1-L1.4 幂等执行与终态结算 | `VERIFIED` | S1-L1.3 | 文件操作、历史提交和 checkpoint 可重放;唯一 retry owner 生效,未知外部结果进入 `manual_review`,仅完整终态删除 pending |
|
||||
| S1-L1.5 E3 全链收口 | `PLANNED` | S1-L1.4 | 崩溃矩阵、升级/降级、重复回放和插件 ABI 验收完整;旧 fail-open、重复状态与兼容层外旧入口删除,ARCH-102 债务归零 |
|
||||
| S1-L2 Workflow typed query | `PLANNED` | S0 | Workflow Application Port 不返回 `Any`/ORM,Session 内投影 DTO,正式调用方全部切换 |
|
||||
| S1-L3 Chain/Agent typed data ports | `PLANNED` | S1-L2 | `ChainDataPorts`/`AgentDataPorts` 的 raw Oper/`Any` factory 全部清零,兼容调用进入 Legacy 层 |
|
||||
@@ -144,7 +144,7 @@ G-ARCH 只有在以下条件全部满足后才可完成:
|
||||
| S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 |
|
||||
| S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 |
|
||||
| S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 |
|
||||
| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 934 条诊断归零,规则集扩展经过独立审查且新增诊断为零 |
|
||||
| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 929 条诊断归零,规则集扩展经过独立审查且新增诊断为零 |
|
||||
| S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverage;raw concurrency 分类清零;Module Quality 有真实 evidence test |
|
||||
|
||||
### S5:Plugin、Agent、Domain、Startup 与最终收口
|
||||
|
||||
@@ -158,6 +158,9 @@ FastAPI 的 HTTP 异常和参数校验异常统一使用 `message`,不再返
|
||||
| POST | `/api/v1/transfer/manual/target-path` | 按源文件与目录配置匹配手动整理目标路径;请求体为 `ManualTransferItem`,该接口不执行媒体识别 |
|
||||
| POST | `/api/v1/transfer/manual/history` | 查询文件、批量文件或目录命中的成功整理历史摘要,用于进入手动整理界面时显示重新整理状态 |
|
||||
| POST | `/api/v1/transfer/manual` | 手动整理;请求体可用 `media_source` + `media_id` 指定本次识别与刮削数据源;音乐请求未传 `music_type` 时,目录按 `album`、文件按 `recording` 解释;命中失败历史时自动清理旧目标和记录后重试,`reorganize=true` 时清理命中的成功历史和非移动模式旧目标后重新整理 |
|
||||
| GET | `/api/v1/transfer/tasks/manual-reviews` | 管理员分页查询 durable 人工复核任务;`state` 仅允许 `manual_review`(默认)或已经人工判定、等待调度恢复的 `retry_wait`,支持 `page` 与 `page_size`。响应只公开任务、源文件、状态、步骤意图/证据/错误和复核修订号,不返回 lease 或 attempt 身份 |
|
||||
| GET | `/api/v1/transfer/tasks/{task_id}/manual-review` | 管理员查询单个 durable 人工复核任务详情;仅可读取 `manual_review` 或已经人工判定的 `retry_wait` 任务,其余状态按不存在处理 |
|
||||
| POST | `/api/v1/transfer/tasks/{task_id}/manual-review` | 管理员判定处于 `manual_review` 的 durable 整理步骤;请求包含 `operation_id`、`decision=not_applied|applied`、`reason`,`applied` 还必须提供 `result_payload`。`failed` 不属于公开决策,失败终态只能由持租约的 durable 结算写入;响应仅返回任务、操作、决策、后续状态和复核修订号 |
|
||||
|
||||
#### 站点
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ to make the directory tree look symmetrical.
|
||||
| `app/application/chain/` | Injectable Chain runtime context and compatibility provider |
|
||||
| `app/application/agentdata.py` | Named Agent data ports; canonical Agent consumers use `get_agent_*_port()` and do not alias legacy proxies to Oper classes |
|
||||
| `app/application/outbox.py` | Durable intent and Outbox repository/dispatcher contracts for post-commit side effects |
|
||||
| `app/application/transfer_execution.py` | Durable transfer execution contracts: stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs; contains no SQLAlchemy or external I/O |
|
||||
| `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) |
|
||||
| `app/application/server/` | MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup |
|
||||
| `app/application/site/` | Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here |
|
||||
@@ -148,6 +149,23 @@ must not be reintroduced as Oper aliases in canonical Agent modules.
|
||||
Monitor history checks use `get_transfer_history_port()` from
|
||||
`app/application/history.py`; the constructible `TransferHistoryPort` facade is
|
||||
retained only for compatibility and is not a canonical Oper substitute.
|
||||
|
||||
Durable transfer execution follows one explicit boundary. The Chain freezes each
|
||||
external file operation into the Application-owned contract in
|
||||
`app/application/transfer_execution.py`; `app/db/adapters/transfer_execution.py`
|
||||
uses short transactions to persist the task ledger and fences every state change
|
||||
with the current lease and attempt token. `app/db/oper/transferexecutionstep.py`
|
||||
remains table-oriented and never owns retry or recovery policy. External file I/O
|
||||
runs outside those transactions. A legacy or remote operation whose result cannot
|
||||
be proven as applied or not applied enters `manual_review` and must not be replayed
|
||||
automatically. Terminal history, pending state, execution-step cleanup and the
|
||||
optional outbox intent are committed only by the task-aware implementation in
|
||||
`app/db/adapters/chain.py`; canonical callers must not add a second settlement or
|
||||
direct pending-deletion path. Task-aware settlement never performs synchronous
|
||||
event publication inside the worker callback; the committed outbox owns delivery.
|
||||
History mutation and maintenance paths may delete or replace only legacy rows with
|
||||
no `transfer_task_id`, because durable receipts are recovery evidence rather than
|
||||
ordinary user-maintained history.
|
||||
Canonical Chain, API, Scheduler and Agent consumers read notification and media
|
||||
server configuration through the named helpers in `app/application/notification.py`
|
||||
and `app/application/mediaserver.py`. `ServiceConfigHelper` remains the parser at
|
||||
|
||||
Reference in New Issue
Block a user