mirror of
https://github.com/Chevron7Locked/kima-hub.git
synced 2026-06-19 07:37:17 +00:00
fix(settings): make Clear Caches actually work, and scope it safely
The Clear Caches button never did anything: the handler used the node-redis
v4 scan signature (options object + { cursor, keys } result) against our
ioredis client, whose scan takes positional args and returns [cursor, keys].
Every call threw and cleared nothing -- which is why clearing the cache did
not dislodge the wedged podcast jobs.
Even had it run, "delete every key except sess:" would have wiped live
BullMQ queue state (bull:*, 200+ keys) and the enrichment/audio/clap control
plane. Replace that with an allowlist of genuine rebuildable caches
(MusicBrainz, cover art, Last.fm, Wikidata, Deezer, iTunes, hero images) and
delete in chunks. Verified read-only against production: clears ~5130 cache
keys, preserves all bull:/audio:/enrichment:/clap:/sess: keys.
This commit is contained in:
@@ -847,35 +847,59 @@ router.post("/queue-cleaner/stop", (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Clear all Redis caches
|
||||
// Cache key prefixes that are safe to drop -- external-API and image-derivation
|
||||
// caches that rebuild on demand. Deliberately an allowlist: it must never touch
|
||||
// BullMQ queue state ("bull:"), the enrichment/analysis control plane
|
||||
// ("enrichment", "audio:", "clap:"), or sessions ("sess:"). The previous
|
||||
// "delete everything except sess:" approach would have wiped live job state --
|
||||
// and it never ran anyway because it used the node-redis scan signature against
|
||||
// our ioredis client (positional args + array return), so it threw every time.
|
||||
const CLEARABLE_CACHE_PREFIXES = [
|
||||
"mb:",
|
||||
"album-cover:",
|
||||
"hero:",
|
||||
"cover-art:",
|
||||
"caa:",
|
||||
"lastfm:",
|
||||
"wikidata:",
|
||||
"deezer:",
|
||||
"itunes:",
|
||||
];
|
||||
|
||||
// Clear external-API / image caches (not queue or control-plane state)
|
||||
router.post("/clear-caches", async (req, res) => {
|
||||
try {
|
||||
const { redisClient } = require("../utils/redis");
|
||||
const { notificationService } =
|
||||
await import("../services/notificationService");
|
||||
|
||||
// Collect all keys using SCAN (non-blocking) and exclude session keys
|
||||
const allKeys: string[] = [];
|
||||
let cursor = 0;
|
||||
// SCAN with the ioredis signature: positional MATCH/COUNT args, and an
|
||||
// [nextCursor, keys] array result; the cursor is a string, "0" terminates.
|
||||
const keysToDelete: string[] = [];
|
||||
let cursor = "0";
|
||||
do {
|
||||
const result = await redisClient.scan(cursor, { MATCH: "*", COUNT: 100 });
|
||||
cursor = result.cursor;
|
||||
allKeys.push(...result.keys);
|
||||
} while (cursor !== 0);
|
||||
|
||||
const keysToDelete = allKeys.filter(
|
||||
(key: string) => !key.startsWith("sess:"),
|
||||
);
|
||||
const [next, keys] = await redisClient.scan(
|
||||
cursor,
|
||||
"MATCH",
|
||||
"*",
|
||||
"COUNT",
|
||||
500,
|
||||
);
|
||||
cursor = next;
|
||||
for (const key of keys) {
|
||||
if (CLEARABLE_CACHE_PREFIXES.some((p) => key.startsWith(p))) {
|
||||
keysToDelete.push(key);
|
||||
}
|
||||
}
|
||||
} while (cursor !== "0");
|
||||
|
||||
if (keysToDelete.length > 0) {
|
||||
// Delete in chunks so a large cache doesn't build one oversized command.
|
||||
for (let i = 0; i < keysToDelete.length; i += 500) {
|
||||
await redisClient.del(...keysToDelete.slice(i, i + 500));
|
||||
}
|
||||
logger.debug(
|
||||
`[CACHE] Clearing ${keysToDelete.length} cache entries (excluding ${
|
||||
allKeys.length - keysToDelete.length
|
||||
} session keys)...`,
|
||||
);
|
||||
await redisClient.del(keysToDelete);
|
||||
logger.debug(
|
||||
`[CACHE] Successfully cleared ${keysToDelete.length} cache entries`,
|
||||
`[CACHE] Cleared ${keysToDelete.length} cache entries`,
|
||||
);
|
||||
|
||||
// Send notification to user
|
||||
|
||||
Reference in New Issue
Block a user