feat: enhance sync cache with size limits and entry management

This commit is contained in:
shuaiplus
2026-03-18 00:12:18 +08:00
parent 98a653efb6
commit 011fe15aae
2 changed files with 64 additions and 7 deletions
+6
View File
@@ -107,6 +107,12 @@
// In-memory /api/sync response cache TTL (milliseconds). // In-memory /api/sync response cache TTL (milliseconds).
// /api/sync 内存缓存有效期(毫秒)。 // /api/sync 内存缓存有效期(毫秒)。
syncResponseTtlMs: 30 * 1000, syncResponseTtlMs: 30 * 1000,
// Max size of a single cached /api/sync body in bytes.
// 单个 /api/sync 缓存响应允许的最大字节数。
syncResponseMaxBodyBytes: 512 * 1024,
// Max total in-memory bytes used by /api/sync cache per isolate.
// 每个 isolate 中 /api/sync 缓存允许占用的最大总字节数。
syncResponseMaxTotalBytes: 2 * 1024 * 1024,
// Max in-memory /api/sync cache entries per isolate. // Max in-memory /api/sync cache entries per isolate.
// 每个 isolate 的 /api/sync 最大缓存条目数。 // 每个 isolate 的 /api/sync 最大缓存条目数。
syncResponseMaxEntries: 64, syncResponseMaxEntries: 64,
+58 -7
View File
@@ -11,11 +11,16 @@ import {
} from '../utils/user-decryption'; } from '../utils/user-decryption';
interface SyncCacheEntry { interface SyncCacheEntry {
userId: string;
revisionDate: string;
body: string; body: string;
expiresAt: number; expiresAt: number;
bytes: number;
} }
const syncResponseCache = new Map<string, SyncCacheEntry>(); const syncResponseCache = new Map<string, SyncCacheEntry>();
let syncResponseCacheTotalBytes = 0;
const textEncoder = new TextEncoder();
function buildSyncCacheKey(userId: string, revisionDate: string, excludeDomains: boolean): string { function buildSyncCacheKey(userId: string, revisionDate: string, excludeDomains: boolean): string {
return `${userId}:${revisionDate}:${excludeDomains ? '1' : '0'}`; return `${userId}:${revisionDate}:${excludeDomains ? '1' : '0'}`;
@@ -25,21 +30,67 @@ function readSyncCache(key: string): string | null {
const hit = syncResponseCache.get(key); const hit = syncResponseCache.get(key);
if (!hit) return null; if (!hit) return null;
if (hit.expiresAt <= Date.now()) { if (hit.expiresAt <= Date.now()) {
syncResponseCache.delete(key); deleteSyncCacheEntry(key, hit);
return null; return null;
} }
return hit.body; return hit.body;
} }
function writeSyncCache(key: string, body: string): void { function deleteSyncCacheEntry(key: string, entry?: SyncCacheEntry): void {
if (syncResponseCache.size >= LIMITS.cache.syncResponseMaxEntries) { const existing = entry ?? syncResponseCache.get(key);
const oldestKey = syncResponseCache.keys().next().value as string | undefined; if (!existing) return;
if (oldestKey) syncResponseCache.delete(oldestKey); syncResponseCache.delete(key);
syncResponseCacheTotalBytes = Math.max(0, syncResponseCacheTotalBytes - existing.bytes);
}
function pruneExpiredSyncCache(nowMs: number = Date.now()): void {
for (const [key, entry] of syncResponseCache.entries()) {
if (entry.expiresAt <= nowMs) {
deleteSyncCacheEntry(key, entry);
}
} }
}
function pruneStaleUserSyncCache(userId: string, revisionDate: string): void {
for (const [key, entry] of syncResponseCache.entries()) {
if (entry.userId === userId && entry.revisionDate !== revisionDate) {
deleteSyncCacheEntry(key, entry);
}
}
}
function writeSyncCache(userId: string, revisionDate: string, key: string, body: string): void {
const nowMs = Date.now();
pruneExpiredSyncCache(nowMs);
pruneStaleUserSyncCache(userId, revisionDate);
const bodyBytes = textEncoder.encode(body).byteLength;
if (bodyBytes > LIMITS.cache.syncResponseMaxBodyBytes) {
return;
}
const existing = syncResponseCache.get(key);
if (existing) {
deleteSyncCacheEntry(key, existing);
}
while (
syncResponseCache.size >= LIMITS.cache.syncResponseMaxEntries ||
syncResponseCacheTotalBytes + bodyBytes > LIMITS.cache.syncResponseMaxTotalBytes
) {
const oldestKey = syncResponseCache.keys().next().value as string | undefined;
if (!oldestKey) break;
deleteSyncCacheEntry(oldestKey);
}
syncResponseCache.set(key, { syncResponseCache.set(key, {
userId,
revisionDate,
body, body,
expiresAt: Date.now() + LIMITS.cache.syncResponseTtlMs, expiresAt: nowMs + LIMITS.cache.syncResponseTtlMs,
bytes: bodyBytes,
}); });
syncResponseCacheTotalBytes += bodyBytes;
} }
// GET /api/sync // GET /api/sync
@@ -137,7 +188,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
}; };
const body = JSON.stringify(syncResponse); const body = JSON.stringify(syncResponse);
writeSyncCache(cacheKey, body); writeSyncCache(userId, revisionDate, cacheKey, body);
return new Response(body, { return new Response(body, {
status: 200, status: 200,