Cleanup finished graphile_worker.jobs last run more than 7 days ago

This commit is contained in:
Eric Allam
2023-10-10 15:47:53 +01:00
parent 476e2f035f
commit 773765f82d
4 changed files with 88 additions and 5 deletions
+73 -1
View File
@@ -14,7 +14,7 @@ import { run as graphileRun, parseCronItems } from "graphile-worker";
import omit from "lodash.omit";
import { z } from "zod";
import { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { logger } from "~/services/logger.server";
import { workerLogger as logger } from "~/services/logger.server";
export interface MessageCatalogSchema {
[key: string]: z.ZodFirstPartySchemaTypes | z.ZodDiscriminatedUnion<any, any>;
@@ -81,6 +81,14 @@ export type ZodWorkerDequeueOptions = {
tx?: PrismaClientOrTransaction;
};
const CLEANUP_TASK_NAME = "__cleanupOldJobs";
export type ZodWorkerCleanupOptions = {
frequencyExpression: string; // cron expression
ttl: number;
taskOptions?: CronItemOptions;
};
export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
name: string;
runnerOptions: RunnerOptions;
@@ -88,6 +96,7 @@ export type ZodWorkerOptions<TMessageCatalog extends MessageCatalogSchema> = {
schema: TMessageCatalog;
tasks: ZodTasks<TMessageCatalog>;
recurringTasks?: ZodRecurringTasks;
cleanup?: ZodWorkerCleanupOptions;
};
export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
@@ -98,6 +107,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
#tasks: ZodTasks<TMessageCatalog>;
#recurringTasks?: ZodRecurringTasks;
#runner?: GraphileRunner;
#cleanup: ZodWorkerCleanupOptions | undefined;
constructor(options: ZodWorkerOptions<TMessageCatalog>) {
this.#name = options.name;
@@ -106,6 +116,7 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
this.#runnerOptions = options.runnerOptions;
this.#tasks = options.tasks;
this.#recurringTasks = options.recurringTasks;
this.#cleanup = options.cleanup;
}
get graphileWorkerSchema() {
@@ -337,12 +348,29 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
taskList[key] = task;
}
if (this.#cleanup) {
const task: Task = (payload, helpers) => {
return this.#handleCleanup(payload, helpers);
};
taskList[CLEANUP_TASK_NAME] = task;
}
return taskList;
}
#createCronItemsFromRecurringTasks() {
const cronItems: CronItem[] = [];
if (this.#cleanup) {
cronItems.push({
pattern: this.#cleanup.frequencyExpression,
identifier: CLEANUP_TASK_NAME,
task: CLEANUP_TASK_NAME,
options: this.#cleanup.taskOptions,
});
}
if (!this.#recurringTasks) {
return cronItems;
}
@@ -434,6 +462,50 @@ export class ZodWorker<TMessageCatalog extends MessageCatalogSchema> {
}
}
async #handleCleanup(rawPayload: unknown, helpers: JobHelpers): Promise<void> {
if (!this.#cleanup) {
return;
}
const job = helpers.job;
logger.debug("Received cleanup task", {
payload: rawPayload,
job,
});
const parsedPayload = RawCronPayloadSchema.safeParse(rawPayload);
if (!parsedPayload.success) {
throw new Error(
`Failed to parse cleanup task payload: ${JSON.stringify(parsedPayload.error)}`
);
}
const payload = parsedPayload.data;
// Add the this.#cleanup.ttl to the payload._cron.ts
const expirationDate = new Date(payload._cron.ts.getTime() - this.#cleanup.ttl);
logger.debug("Cleaning up old jobs", {
expirationDate,
payload,
});
const rawResults = await this.#prisma.$queryRawUnsafe(
`DELETE FROM ${this.graphileWorkerSchema}.jobs WHERE run_at < $1 AND locked_at IS NULL AND max_attempts = attempts RETURNING id`,
expirationDate
);
const results = Array.isArray(rawResults) ? rawResults : [];
logger.debug("Cleaned up old jobs", {
count: results.length,
expirationDate,
payload,
});
}
#logDebug(message: string, args?: any) {
logger.debug(`[worker][${this.#name}] ${message}`, args);
}
@@ -305,7 +305,7 @@ export class RunTaskService {
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined }
{ tx, runAt: task.delayUntil ?? undefined, jobKey: `operation:${task.id}` }
);
} else if (task.status === "WAITING" && callbackUrl && taskBody.callback) {
if (taskBody.callback.timeoutInSeconds > 0) {
@@ -8,3 +8,10 @@ export const logger = new Logger(
["examples", "output", "connectionString", "payload"],
sensitiveDataReplacer
);
export const workerLogger = new Logger(
"worker",
(process.env.APP_LOG_LEVEL ?? "debug") as LogLevel,
["examples", "output", "connectionString"],
sensitiveDataReplacer
);
+7 -3
View File
@@ -128,6 +128,12 @@ function getWorkerQueue() {
return new ZodWorker({
name: "workerQueue",
prisma,
cleanup: {
// cleanup once per hour
frequencyExpression: "0 * * * *",
// delete jobs that have been completed for more than 7 days
ttl: 7 * 24 * 60 * 60 * 1000,
},
runnerOptions: {
connectionString: env.DATABASE_URL,
concurrency: env.WORKER_CONCURRENCY,
@@ -229,6 +235,7 @@ function getWorkerQueue() {
deliverHttpSourceRequest: {
priority: 1, // smaller number = higher priority
maxAttempts: 14,
queueName: (payload) => `sources:${payload.id}`,
handler: async (payload, job) => {
const service = new DeliverHttpSourceRequestService();
@@ -255,7 +262,6 @@ function getWorkerQueue() {
},
performTaskOperation: {
priority: 0, // smaller number = higher priority
queueName: (payload) => `tasks:${payload.id}`,
maxAttempts: 3,
handler: async (payload, job) => {
const service = new PerformTaskOperationService();
@@ -264,7 +270,6 @@ function getWorkerQueue() {
},
},
scheduleEmail: {
queueName: "internal-queue",
priority: 100,
maxAttempts: 3,
handler: async (payload, job) => {
@@ -291,7 +296,6 @@ function getWorkerQueue() {
},
refreshOAuthToken: {
priority: 8, // smaller number = higher priority
queueName: "internal-queue",
maxAttempts: 7,
handler: async (payload, job) => {
await integrationAuthRepository.refreshConnection({