fix(acquisition): 同一种子被别的计划占用而取消的计划不让位,避免 B/C 两线合并后取消 → 重评 → 再取消的空转

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-03 21:23:52 +08:00
1 parent 15d7f2ec2f
commit 5b023ad682
2 files changed
+22 -2

No files matched your search

@@ -1103,12 +1103,18 @@ export function subscriptionSavePath(t: Pick<InstanceTopology, "inst" | "mapping
/** /**
* 旧计划是否让位给新计划(T1-3c):失败 / 取消 / 过期可以重建(站点 Cookie 修好、下载器恢复、集数映射改号后重评), * 旧计划是否让位给新计划(T1-3c):失败 / 取消 / 过期可以重建(站点 Cookie 修好、下载器恢复、集数映射改号后重评),
* 但用户明确不要的(在下载器里移除、手动取消)和整理阶段失败的(文件已下好,重下同一种子无意义,交给整理重试)仍挡住。 * 但用户明确不要的(在下载器里移除、手动取消)和整理阶段失败的(文件已下好,重下同一种子无意义,交给整理重试)仍挡住;
* 同一种子已被别的计划占用而取消的(torrent_in_use_by_plan)也挡住,否则取消即重评、重评又撞上同一任务,来回空转。
*/ */
function planRetryable(p: { status: string; status_reason: string | null }): boolean { function planRetryable(p: { status: string; status_reason: string | null }): boolean {
if (!["failed", "cancelled", "expired"].includes(p.status)) return false; if (!["failed", "cancelled", "expired"].includes(p.status)) return false;
const r = p.status_reason ?? ""; const r = p.status_reason ?? "";
return r !== "download_removed" && r !== "cancelled_by_user" && !r.startsWith("organize:"); return (
r !== "download_removed" &&
r !== "cancelled_by_user" &&
!r.startsWith("organize:") &&
!r.startsWith("torrent_in_use_by_plan")
);
} }
async function reserve( async function reserve(
@@ -302,6 +302,20 @@ describe.skipIf(!hasPg)("被挡住与失败后的重评", () => {
expect((await evaluateAcquisition(h.d, { sourceReleaseId: rel })).plans).toHaveLength(0); expect((await evaluateAcquisition(h.d, { sourceReleaseId: rel })).plans).toHaveLength(0);
}); });
it("同一种子已被别的计划占用而取消的不让位(避免取消 → 重评 → 再撞上同一任务的空转)", async () => {
const { entityId, slotId } = await movieSlot("InUse Movie");
const rel = await release(entityId, "InUse.Movie.2024.1080p.WEB-DL.x264-GRP");
const ev1 = await evaluateAcquisition(h.d, { sourceReleaseId: rel });
expect(ev1.plans).toHaveLength(1);
await db()
.updateTable("acquisition_plan")
.set({ status: "cancelled", status_reason: "torrent_in_use_by_plan:00000000-0000-0000-0000-000000000000" })
.where("id", "=", ev1.plans[0]!)
.execute();
await releaseSlot(slotId);
expect((await evaluateAcquisition(h.d, { sourceReleaseId: rel })).plans).toHaveLength(0);
});
it("在途计划(已提交)仍挡住同一资源", async () => { it("在途计划(已提交)仍挡住同一资源", async () => {
const { entityId } = await seriesSlot("Inflight Series"); const { entityId } = await seriesSlot("Inflight Series");
const rel = await release(entityId, "Inflight.Series.S01E01.1080p.WEB-DL.x264-GRP"); const rel = await release(entityId, "Inflight.Series.S01E01.1080p.WEB-DL.x264-GRP");