Merged in feature/redis-queue-deletes (pull request #2397)

Feature/redis queue deletes

* feat: add queuing for deletes

* merge

* .

* .

* .


Approved-by: Linus Flood
This commit is contained in:
Joakim Jäderberg
2025-06-19 09:20:38 +00:00
parent 3af994b0a9
commit 105c4d9cf3
5 changed files with 258 additions and 43 deletions
@@ -0,0 +1,32 @@
import ioredis from "ioredis";
import { env, redisConfig } from "@/env";
export const redis = new ioredis({
host: redisConfig.host,
port: redisConfig.port,
username: redisConfig.username,
password: redisConfig.password,
maxRetriesPerRequest: 1, // Avoid excessive retries,
tls: !env.IS_DEV
? {
rejectUnauthorized: true,
}
: undefined,
lazyConnect: true,
commandTimeout: 1_000,
socketTimeout: 5_000,
});
export const bullmqredis = new ioredis({
host: redisConfig.host,
port: redisConfig.port,
username: redisConfig.username,
password: redisConfig.password,
maxRetriesPerRequest: null, // Avoid excessive retries,
tls: !env.IS_DEV
? {
rejectUnauthorized: true,
}
: undefined,
});
@@ -0,0 +1,84 @@
import { redis, bullmqredis } from ".";
import z from "zod";
import { loggerModule } from "@/utils/logger";
import { Worker, Queue } from "bullmq";
import { timeout } from "@/utils/timeout";
import { env } from "@/env";
import { sentry } from "@/server/sentry.server.config";
const DELETE_JOB = "deleteQueueJob";
const deleteQueueLogger = loggerModule("deleteQueue");
const deleteQueueSchema = z.object({
pattern: z.string().min(1, "Pattern must be at least 1 character long"),
});
const deleteQueue = new Queue(DELETE_JOB, { connection: bullmqredis });
const worker = new Worker(
DELETE_JOB,
async (job) => {
if (job.name === "delete") {
const { pattern } = deleteQueueSchema.parse(job.data);
deleteQueueLogger.info(
`Job: ${job.id} processing. With pattern: ${pattern}`,
{ pattern, jobId: job.id },
);
const now = performance.now();
const deletedCount = await deleteWithPattern(pattern);
const elapsed = performance.now() - now;
deleteQueueLogger.info(
`Job: ${job.id} completed. Deleted ${deletedCount} keys for pattern '${pattern}' in ${elapsed.toFixed(2)}ms.`,
{ deletedCount, pattern, elapsed, jobId: job.id },
);
}
},
{ connection: bullmqredis },
);
worker.on("failed", (job, error) => {
deleteQueueLogger.error(`Job failed: ${job?.id} with ${error.message}`, {
error,
jobId: job?.id,
pattern: job?.data?.pattern,
});
sentry.captureException(error);
});
export async function queueDelete({ pattern }: { pattern: string }) {
deleteQueue.add("delete", { pattern });
}
async function deleteWithPattern(pattern: string) {
let cursor = "0";
const SCAN_SIZE = env.DELETE_BATCH_SIZE;
let totalDeleteCount = 0;
do {
const [newCursor, foundKeys] = await redis.scan(
cursor,
"MATCH",
pattern,
"COUNT",
SCAN_SIZE,
);
cursor = newCursor;
if (foundKeys.length === 0) {
continue;
}
const deleteCount = await redis.unlink(foundKeys);
totalDeleteCount += deleteCount;
// Rate limiting to avoid overwhelming the Redis server
await timeout(100);
} while (cursor !== "0");
return totalDeleteCount;
}