feat(cache): PG kv_cache ported from MP TTLCache, TMDB request and recognize caches
Migration 0042_cache adds UNLOGGED kv_cache. PgCache mirrors MP
runtime/cache.py (region, per-key TTL, TLRU maxsize eviction, @cached as
cachedCall); only the storage moves from memory to PG. TMDB request
responses (MP TMDb.request @cached) and recognize results (MP TmdbCache:
same key, {id:0} negative cache with EMPTY_RESULT_CACHE_TTL, type-conflict
checks, backfill after manual assignment) live in kv_cache, shared by all
processes. Settings cache.tmdbTtlHours / tmdbMaxSize / emptyResultTtlSec
map to META_CACHE_EXPIRE / CONF.tmdb / EMPTY_RESULT_CACHE_TTL. TMDB
details now also store posterPath / backdropPath.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
61f585a707
commit
946fca5e55
22 files changed
+743
-14
No files matched your search
@@ -43,7 +43,7 @@ const pingModule: ServerModule = {
|
||||
};
|
||||
|
||||
describe("ServerModule 骨架", () => {
|
||||
it("清单里是四个 W2 模块 + 文件管理 + Pi 上游 + 定时任务 + 全局搜索 + 浏览器 + MCP + 进程与健康(末尾),名字唯一", () => {
|
||||
it("清单里是四个 W2 模块 + 文件管理 + Pi 上游 + 定时任务 + 全局搜索 + 浏览器 + MCP + 缓存 + 进程与健康(末尾),名字唯一", () => {
|
||||
expect(SERVER_MODULES.map((m) => m.name)).toEqual([
|
||||
"auth-settings",
|
||||
"sources-identity",
|
||||
@@ -55,6 +55,7 @@ describe("ServerModule 骨架", () => {
|
||||
"search",
|
||||
"browser",
|
||||
"mcp",
|
||||
"cache",
|
||||
"system",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CapabilityRegistry } from "@umt/agent";
|
||||
import { sql } from "@umt/db";
|
||||
import { createMockTmdb, type MockTmdb, TMDB_FIXTURES, type TmdbFixture } from "@umt/testkit";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { refreshMetadata } from "../../src/modules/sources-identity/service/metadata.ts";
|
||||
@@ -453,4 +454,39 @@ describe.skipIf(!up)("resolveObservation / 身份与元数据", () => {
|
||||
expect(search.items.length).toBeGreaterThan(0);
|
||||
await expect(reg.invoke("identity.resolve", { sourceReleaseId: twinsRelease }, c)).rejects.toThrow();
|
||||
});
|
||||
it("识别缓存(移植 MP TmdbCache):键 [类型][语言]名称-年份-季;命中不再搜索;查无写 {id:0} 短 TTL;force 不读缓存", async () => {
|
||||
const rows = async () =>
|
||||
(
|
||||
await sql<{ key: string; value: { id: number; type?: string; title?: string }; minutes: number }>`
|
||||
select key, value, extract(epoch from expires_at - now()) / 60 as minutes
|
||||
from kv_cache where region = 'tmdb.recognize'`.execute(h.db)
|
||||
).rows;
|
||||
const opp = (await rows()).find((r) => r.key === "[未知][zh-CN]Oppenheimer-2023-None");
|
||||
expect(opp?.value).toMatchObject({ id: 872585, type: "movie", title: "奥本海默" });
|
||||
expect(Number(opp!.minutes)).toBeGreaterThan(23 * 60);
|
||||
// 同名同年的另一条资源:命中识别缓存,不再调 TMDB 搜索
|
||||
const searches = tmdb.callsTo(/\/3\/search\//).length;
|
||||
const again = await release("Oppenheimer.2023.1080p.BluRay.x264-OTHER");
|
||||
await resolveRelease(h.rt, { sourceReleaseId: again.id });
|
||||
expect(tmdb.callsTo(/\/3\/search\//).length).toBe(searches);
|
||||
// 查无此项 → 负缓存 {id: 0},TTL = 空结果短 TTL(30 分钟)
|
||||
const none = await release("Zzqxv.Nonexistent.Title.2019.1080p.WEB-DL-NOGRP");
|
||||
await resolveRelease(h.rt, { sourceReleaseId: none.id });
|
||||
const neg = (await rows()).find((r) => r.key === "[未知][zh-CN]Zzqxv Nonexistent Title-2019-None");
|
||||
expect(neg?.value).toEqual({ id: 0 });
|
||||
expect(Number(neg!.minutes)).toBeLessThanOrEqual(30);
|
||||
const s2 = tmdb.callsTo(/\/3\/search\//).length;
|
||||
await resolveRelease(h.rt, { sourceReleaseId: none.id });
|
||||
expect(tmdb.callsTo(/\/3\/search\//).length).toBe(s2);
|
||||
// force:不读识别缓存(回源结果由请求层缓存兜住),照常回写识别缓存
|
||||
const at = async () =>
|
||||
(
|
||||
await sql<{ t: Date }>`select updated_at as t from kv_cache
|
||||
where region = 'tmdb.recognize' and key = ${neg!.key}`.execute(h.db)
|
||||
).rows[0]!.t.getTime();
|
||||
const before = await at();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await resolveRelease(h.rt, { sourceReleaseId: none.id, force: true });
|
||||
expect(await at()).toBeGreaterThan(before);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user