Merged in feat/redis-fix (pull request #3207)
Feat/redis fix * feat(redis): delete multiple keys in one partition scan * fix(BOOK-603): make it possible to do multiple deletes in redis at once using one partition scan Approved-by: Linus Flood
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import { Elysia, t } from "elysia";
|
||||
|
||||
import { ModelValidationError } from "@/errors/ModelValidationError";
|
||||
import { redis } from "@/services/redis";
|
||||
import { queueDelete } from "@/services/redis/queueDelete";
|
||||
import { queueDelete, queueDeleteMultiple } from "@/services/redis/queueDelete";
|
||||
import { loggerModule } from "@/utils/logger";
|
||||
import { truncate } from "@/utils/truncate";
|
||||
import { validateKey } from "@/utils/validateKey";
|
||||
|
||||
const MIN_LENGTH = 1;
|
||||
|
||||
const QUERY_TYPE = t.Object({ key: t.String({ minLength: MIN_LENGTH }) });
|
||||
const DELETEMULTIPLE_BODY_TYPE = t.Object({
|
||||
keys: t.Array(t.String({ minLength: MIN_LENGTH })),
|
||||
fuzzy: t.Optional(t.Boolean({ default: false })),
|
||||
});
|
||||
|
||||
const cacheRouteLogger = loggerModule("cacheRoute");
|
||||
export const cacheRoutes = new Elysia({ prefix: "/cache" })
|
||||
.get(
|
||||
@@ -68,6 +73,53 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
|
||||
response: { 204: t.Undefined(), 400: t.String() },
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/multiple",
|
||||
async ({ body: { keys, fuzzy = false } }) => {
|
||||
const validatedKeys = keys.map(validateKey);
|
||||
|
||||
cacheRouteLogger.debug(
|
||||
`DELETE /multiple keys=${validatedKeys.join(",")} ${fuzzy ? "(fuzzy)" : ""}`,
|
||||
);
|
||||
|
||||
// 1. Fuzzy deletes → Single SCAN pass
|
||||
if (fuzzy) {
|
||||
const patterns = validatedKeys.map((k) => `*${k}*`);
|
||||
|
||||
await queueDeleteMultiple({ patterns });
|
||||
return { status: "queued" };
|
||||
}
|
||||
|
||||
// 2. Exact deletes → Batch unlink
|
||||
const now = performance.now();
|
||||
|
||||
// Use UNLINK for async deletes
|
||||
const deletedKeys = await redis.unlink(...validatedKeys);
|
||||
|
||||
const elapsed = performance.now() - now;
|
||||
|
||||
cacheRouteLogger.info(
|
||||
`Deleted ${deletedKeys} keys in ${elapsed}ms`,
|
||||
{
|
||||
deletedKeys,
|
||||
keys: validatedKeys,
|
||||
elapsed,
|
||||
},
|
||||
);
|
||||
|
||||
return { deletedKeys };
|
||||
},
|
||||
{
|
||||
body: DELETEMULTIPLE_BODY_TYPE,
|
||||
response: {
|
||||
200: t.Union([
|
||||
t.Object({ deletedKeys: t.Number() }),
|
||||
t.Object({ status: t.Literal("queued") }),
|
||||
]),
|
||||
400: t.String(),
|
||||
},
|
||||
},
|
||||
)
|
||||
.delete(
|
||||
"/",
|
||||
async ({ query: { key, fuzzy } }) => {
|
||||
@@ -105,19 +157,3 @@ export const cacheRoutes = new Elysia({ prefix: "/cache" })
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function validateKey(key: string) {
|
||||
const parsedKey = decodeURIComponent(key);
|
||||
|
||||
if (parsedKey.length < MIN_LENGTH) {
|
||||
throw new ModelValidationError(
|
||||
"Key has to be at least 1 character long",
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedKey.includes("*")) {
|
||||
throw new ModelValidationError("Key cannot contain wildcards");
|
||||
}
|
||||
|
||||
return parsedKey;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user