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:
rootandClaude Opus 5.5 committed 2026-09-28 22:26:29 +08:00
1 parent 61f585a707
commit 946fca5e55
22 files changed
+743 -14

No files matched your search

@@ -11,9 +11,11 @@ import {
type SiteDefinition,
type SiteRateLimiter,
type TmdbClient,
type TmdbSharedCache,
} from "@umt/adapters";
import { DEFAULT_SETTINGS, type SitesSettings } from "@umt/contracts";
import { pgTokenBucketFor, type SiteRow } from "@umt/db";
import { CACHE_REGIONS, sharedCache, type TmdbCachePolicy, tmdbCachePolicy } from "../../shared-cache.ts";
import type { ServerContext } from "../types.ts";
// 模块运行时依赖:站点适配器与限流器(每站一个实例,令牌在 PG 跨进程共享)、RSS 抓取器、TMDB 客户端、时钟。
@@ -71,12 +73,14 @@ export class SourcesRuntime {
return this.tmdbClient;
}
const meta = await this.ctx.settings.get("metadata").catch(() => null);
const policy = await tmdbCachePolicy(this.ctx);
const env = (k: string) => Deno.env.get(k);
const ref = meta?.tmdbApiKeySecretRef ?? null;
const baseUrl = meta?.tmdbBaseUrl ?? env("TMDB_BASE_URL");
const sig = JSON.stringify([
ref,
baseUrl ?? null,
policy,
ref ? null : [!!env("TMDB_API_KEY"), !!env("TMDB_ACCESS_TOKEN")],
]);
if (this.tmdbClient !== undefined && this.tmdbSig === sig) return this.tmdbClient;
@@ -103,6 +107,9 @@ export class SourcesRuntime {
...(accessToken ? { accessToken } : {}),
...(baseUrl ? { baseUrl } : {}),
...(this.overrides.fetch ? { fetch: this.overrides.fetch } : {}),
// 请求响应进 PG 缓存(region tmdb.request),三进程共享;TTL / 条数上限 / 空结果短 TTL 取设置(对照 MP)
cache: { ttlMs: policy.ttl * 1000, emptyTtlMs: policy.emptyTtl * 1000 },
sharedCache: tmdbSharedCache(this.ctx, policy),
})
: null;
return this.tmdbClient;
@@ -233,6 +240,22 @@ export class SourcesRuntime {
}
}
/** TMDB 请求响应缓存:PG kv_cache region=tmdb.request;PG 出错按未命中 / 不写处理,不让 TMDB 请求失败 */
function tmdbSharedCache(ctx: ServerContext, policy: TmdbCachePolicy): TmdbSharedCache {
const cache = sharedCache(ctx);
const warn = (err: unknown) =>
ctx.logger.warn({ err: { message: (err as Error).message } }, "TMDB 缓存读写失败,按未命中处理");
return {
get: (key) =>
cache.get(CACHE_REGIONS.tmdbRequest, key).catch((e) => {
warn(e);
return null;
}),
set: (key, value, ttlMs) =>
cache.set(CACHE_REGIONS.tmdbRequest, key, value, { ttl: ttlMs / 1000, maxsize: policy.maxsize }).catch(warn),
};
}
function sameUrl(a: string, b: string): boolean {
return a.replace(/\/+$/, "") === b.replace(/\/+$/, "");
}