mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-10 18:06:48 +08:00
perf(subscribe): bound candidate routing work
This commit is contained in:
@@ -77,6 +77,7 @@ class CandidateIndex:
|
||||
self._unknown: set[int] = set()
|
||||
self._reconcilable: set[int] = set()
|
||||
self._explicit_identity: dict[int, tuple[str, str]] = {}
|
||||
self.last_examined_count = 0
|
||||
for domain, contexts in candidates.items():
|
||||
for context in contexts:
|
||||
position = len(self._ordered)
|
||||
@@ -136,9 +137,10 @@ class CandidateIndex:
|
||||
)
|
||||
|
||||
routed: CandidateGroups = {}
|
||||
for position, (domain, context) in enumerate(self._ordered):
|
||||
if position not in positions:
|
||||
continue
|
||||
self.last_examined_count = 0
|
||||
for position in sorted(positions):
|
||||
domain, context = self._ordered[position]
|
||||
self.last_examined_count += 1
|
||||
if domains and domain not in domains:
|
||||
continue
|
||||
torrent_info = getattr(context, "torrent_info", None)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# MoviePilot 订阅执行治理
|
||||
|
||||
> 状态:`active(2026-09-01 已由 MoviePilot v3 接管)`
|
||||
> 当前叶:`SUB-GOV-001E(条件评估)`
|
||||
> 当前叶:`SUB-GOV-002A`
|
||||
> 迁移来源:`V3-RDY-009A`、`V3-RDY-009A1`、`V3-RDY-009A1B`、
|
||||
> `V3-RDY-009A1C`、`V3-RDY-009A1D`
|
||||
> 适用范围:V3 订阅刷新、匹配、兜底搜索、下载提交与用户状态闭环
|
||||
@@ -231,15 +231,15 @@ RSS/Spider 保持现有逐站点串行刷新,不通过并发化换取几秒收
|
||||
| `SUB-GOV-001B` | 区分完整缓存与本轮 delta,建立无损候选索引;先证明命中集合与基线完全一致 | 001A | `completed(2026-09-01)` |
|
||||
| `SUB-GOV-001C` | 将资源匹配与完成对账拆开;只有受候选影响的订阅进入匹配,完成对账独立保持新鲜语义 | 001B | `completed(2026-09-01)` |
|
||||
| `SUB-GOV-001D` | 引入单轮新鲜事实租约,评估稳定元数据复用、轻量季集查询和单媒体服务器事实合并 | 001C | `completed(2026-09-01)` |
|
||||
| `SUB-GOV-001E` | 若 001B–D 后仍有可重复等待热点,再通过隔离压测评估 2/4 worker 的有界准备;提交保持串行 | 001D | `in_progress(条件评估)` |
|
||||
| `SUB-GOV-002A` | 将 24 小时兜底搜索移出 Match/提交长锁,建立批次、订阅 single-flight、可取消等待和恢复游标 | 001C | `pending` |
|
||||
| `SUB-GOV-001E` | 若 001B–D 后仍有可重复等待热点,再通过隔离压测评估 2/4 worker 的有界准备;提交保持串行 | 001D | `not_activated(2026-09-01)` |
|
||||
| `SUB-GOV-002A` | 将 24 小时兜底搜索移出 Match/提交长锁,建立批次、订阅 single-flight、可取消等待和恢复游标 | 001C | `in_progress` |
|
||||
| `SUB-GOV-002B` | 为兜底搜索建立每站点并发、间隔和错误冷却预算;RSS/Spider 保持基线压力 | 002A | `pending` |
|
||||
| `SUB-GOV-002C` | 联合运行日常 Match、手工搜索和兜底搜索,验证公平性、锁等待、站点压力和失败恢复 | 001D, 002B | `pending` |
|
||||
| `SUB-GOV-003A` | 建立跨入口的订阅级下载幂等、下载器不确定终态和取消补偿 | 001C, 002A | `pending` |
|
||||
| `SUB-GOV-003B` | 后端提供订阅及批次业务状态,前端展示排队、匹配、搜索、提交、完成、失败和取消 | 002A, 003A | `pending` |
|
||||
| `SUB-GOV-004` | 多条订阅记录指向同一媒体时的跨记录季集去重和产品规则 | 003A | `pending(最低优先级)` |
|
||||
|
||||
当前只激活 `SUB-GOV-001E` 的条件评估。001B–D 是日常 Match 主线;002A–C 是 24 小时兜底搜索主线;003A/B
|
||||
当前只激活 `SUB-GOV-002A`。001B–D 是日常 Match 主线;002A–C 是 24 小时兜底搜索主线;003A/B
|
||||
只有在前两条链路的身份和终态稳定后实施。每个叶子完成验收并更新本表后,才激活下一个满足依赖的叶子。
|
||||
|
||||
### 6.1 SUB-GOV-001A 验收证据
|
||||
@@ -290,6 +290,15 @@ RSS/Spider 保持现有逐站点串行刷新,不通过并发化换取几秒收
|
||||
- 验证:订阅与候选回放 `118 passed`,事实租约/治理回放/架构组合 `108 passed`,错误级源代码 Pylint 为 0,
|
||||
Host 架构基线与 `git diff --check` 通过。
|
||||
|
||||
### 6.5 SUB-GOV-001E 条件评估
|
||||
|
||||
- 修正 `CandidateIndex` 的末端遍历:路由直接按选中位置读取,不再先枚举完整候选后判断位置集合;
|
||||
- 受控样本固定为 `200` 条订阅、`20` 个站点、`1000` 条明确身份候选、`100` 个唯一媒体;基线组合量为
|
||||
`200000`,新索引实际检查并路由 `2000` 条,单轮事实租约执行 `100` 次加载并命中 `100` 次;
|
||||
- 该样本证明本地 `S * C` 热点和同媒体重复识别已消除,但没有提供 001B–D 后相互独立的 provider/媒体服务器
|
||||
等待仍主导批次的同 revision 证据;因此不激活 2/4 worker,不引入共享客户端线程安全与提交排序风险;
|
||||
- 后续若受控生产形态证据再次显示准备等待占主导,可从本条件叶重新开启,但不得绕过 SiteBudget 和串行提交。
|
||||
|
||||
## 7. 上线前验证与验收
|
||||
|
||||
### 7.1 场景
|
||||
|
||||
@@ -5,6 +5,7 @@ from unittest.mock import Mock, patch
|
||||
|
||||
from app.application.subscription.candidates import CandidateBatch, CandidateIndex
|
||||
from app.application.subscription.contract import SubscriptionSnapshot
|
||||
from app.application.subscription.facts import FreshFactLease
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.domain.context import Context, MediaInfo, TorrentInfo
|
||||
from app.domain.metainfo import MetaInfo
|
||||
@@ -179,3 +180,60 @@ def test_candidate_index_custom_words_preserve_complete_candidate_set():
|
||||
)
|
||||
|
||||
assert routed == candidates
|
||||
|
||||
|
||||
def test_candidate_index_target_scale_avoids_subscription_candidate_product():
|
||||
"""200 条订阅与 1000 候选只检查命中身份位置,并按唯一媒体合并事实读取。"""
|
||||
media_count = 100
|
||||
subscription_count = 200
|
||||
site_count = 20
|
||||
candidates = {f"site-{site_index}.example": [] for site_index in range(site_count)}
|
||||
for candidate_index in range(1000):
|
||||
media_id = str(1000 + candidate_index % media_count)
|
||||
domain = f"site-{candidate_index % site_count}.example"
|
||||
candidates[domain].append(
|
||||
_context(
|
||||
f"目标剧集 {media_id} S01E{candidate_index + 1:04d}",
|
||||
media_id=media_id,
|
||||
meta_media_id=media_id,
|
||||
enclosure=f"https://{domain}/{candidate_index}",
|
||||
)
|
||||
)
|
||||
subscribes = [
|
||||
_subscribe(
|
||||
id=index + 1,
|
||||
name=f"目标剧集 {index % media_count}",
|
||||
media_id=str(1000 + index % media_count),
|
||||
)
|
||||
for index in range(subscription_count)
|
||||
]
|
||||
|
||||
candidate_index = CandidateIndex(candidates)
|
||||
examined = 0
|
||||
routed = 0
|
||||
for subscribe in subscribes:
|
||||
groups = candidate_index.route_for_match(subscribe)
|
||||
examined += candidate_index.last_examined_count
|
||||
routed += CandidateBatch.count(groups)
|
||||
|
||||
lease = FreshFactLease()
|
||||
fact_loads = []
|
||||
for subscribe in subscribes:
|
||||
lease.get_or_load(
|
||||
subscribe,
|
||||
lambda subscribe=subscribe: fact_loads.append(subscribe.media_id)
|
||||
or MediaInfo(
|
||||
media_source=MediaSource.TMDB,
|
||||
media_id=subscribe.media_id,
|
||||
type=MediaType.TV,
|
||||
season=1,
|
||||
),
|
||||
)
|
||||
|
||||
assert len(candidates) == site_count
|
||||
assert CandidateBatch.count(candidates) == 1000
|
||||
assert examined == routed == 2000
|
||||
assert examined < subscription_count * CandidateBatch.count(candidates) // 50
|
||||
assert len(fact_loads) == media_count
|
||||
assert lease.loads == media_count
|
||||
assert lease.hits == subscription_count - media_count
|
||||
|
||||
Reference in New Issue
Block a user