feat(cache): read cache TTLs from storage settings and renew tracked TMDB details (CACHE-2)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
58e358e4a9
commit
85b352b8eb
10 files changed
+140
-35
No files matched your search
@@ -98,8 +98,11 @@ export interface TmdbConfig {
|
||||
maxRetryAfterMs?: number;
|
||||
maxConcurrent?: number;
|
||||
minIntervalMs?: number;
|
||||
/** 默认 max 1024、ttl 24h;空结果(搜索 0 条)用 emptyTtlMs,默认 30min(对照 MP EMPTY_RESULT_CACHE_TTL) */
|
||||
cache?: { max?: number; ttlMs?: number; emptyTtlMs?: number };
|
||||
/**
|
||||
* 默认 max 1024、ttl 24h;空结果(搜索 0 条)用 emptyTtlMs,默认 30min(对照 MP EMPTY_RESULT_CACHE_TTL);
|
||||
* searchTtlMs:/search/、/find/ 请求的 TTL(缺省同 ttlMs),其余(详情 / 季)用 ttlMs。
|
||||
*/
|
||||
cache?: { max?: number; ttlMs?: number; searchTtlMs?: number; emptyTtlMs?: number };
|
||||
/**
|
||||
* 跨进程共享的请求缓存(服务端传 PG 缓存);给了就不再用进程内 LRU。
|
||||
* 语义对照 MP @cached(skip_none、业务失败不缓存、results 为空按 emptyTtlMs)。
|
||||
@@ -164,6 +167,8 @@ export class TmdbClient {
|
||||
for (const k of Object.keys(params).sort()) if (params[k] !== undefined) qs.set(k, params[k]!);
|
||||
const key = `${path}?${qs}`;
|
||||
const emptyTtl = this.cfg.cache?.emptyTtlMs ?? 30 * 60_000;
|
||||
const baseTtl = this.cfg.cache?.ttlMs ?? 24 * 3600_000;
|
||||
const ttl = /^\/(search|find)\//.test(path) ? (this.cfg.cache?.searchTtlMs ?? baseTtl) : baseTtl;
|
||||
if (this.cfg.sharedCache) {
|
||||
const shared = this.cfg.sharedCache;
|
||||
const hit = await shared.get(key);
|
||||
@@ -171,14 +176,14 @@ export class TmdbClient {
|
||||
// 网络错误 / 业务失败在 fetchWithRetry 抛出,不会走到写缓存(MP skip_none / skip_if)
|
||||
const v = await this.limiter.run(() => this.fetchWithRetry(path, qs));
|
||||
if (v !== null && v !== undefined) {
|
||||
await shared.set(key, v, isEmptyResult(v) ? emptyTtl : (this.cfg.cache?.ttlMs ?? 24 * 3600_000));
|
||||
await shared.set(key, v, isEmptyResult(v) ? emptyTtl : ttl);
|
||||
}
|
||||
return v as T;
|
||||
}
|
||||
return this.cache.getOrLoad(
|
||||
key,
|
||||
() => this.limiter.run(() => this.fetchWithRetry(path, qs)),
|
||||
(v) => (isEmptyResult(v) ? emptyTtl : undefined),
|
||||
(v) => (isEmptyResult(v) ? emptyTtl : ttl === baseTtl ? undefined : ttl),
|
||||
) as Promise<T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,14 @@ export class PgCache {
|
||||
return r.rows.length ? (r.rows[0]!.value as T) : null;
|
||||
}
|
||||
|
||||
/** 续期:未过期的条目把 expires_at 推到 now + ttl(只延长不缩短);返回是否有行被续期 */
|
||||
async renew(region: string, key: string, ttl: number): Promise<boolean> {
|
||||
if (ttl <= 0) return false;
|
||||
const r = await sql`update kv_cache set expires_at = greatest(expires_at, now() + make_interval(secs => ${ttl}))
|
||||
where region = ${region} and key = ${key} and expires_at > now()`.execute(this.db);
|
||||
return Number(r.numAffectedRows ?? 0) > 0;
|
||||
}
|
||||
|
||||
async exists(region: string, key: string): Promise<boolean> {
|
||||
const r =
|
||||
await sql`select 1 from kv_cache where region = ${region} and key = ${key} and expires_at > now()`.execute(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// 统一从本包取 Kysely 与 sql,避免各包各装一份 kysely
|
||||
export { type Kysely, type Selectable, sql, type Transaction } from "kysely";
|
||||
export { type Kysely, type RawBuilder, type Selectable, sql, type Transaction } from "kysely";
|
||||
export {
|
||||
type CachedOptions,
|
||||
cachedCall,
|
||||
|
||||
Reference in new issue
Block a user