Files
dokploy/apps/schedules/src/queue.ts
T
Mauricio Siu 4e38925119 fix: pass timezone when removing repeatable schedule job
removeJob() for type 'schedule' computed the BullMQ repeatable key
using only the cron pattern, without the timezone. scheduleJob()
always registers the repeatable job with tz set (job.timezone || UTC),
so BullMQ's repeat key (name:jobId:endDate:tz:pattern) never matched
on removal — removeRepeatable silently no-op'd and the job kept
firing forever, surviving disable, delete, and reboots.

Forward timezone through removeJob (queue.ts), the /update-backup
lookup (index.ts), and the disable/delete call sites in the
schedule router.
2026-09-01 01:31:59 -06:00

112 lines
2.8 KiB
TypeScript

import { Queue, type RepeatableJob } from "bullmq";
import { logger } from "./logger.js";
import type { QueueJob } from "./schema.js";
export const jobQueue = new Queue("backupQueue", {
connection: {
url: process.env.REDIS_URL!,
},
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: true,
},
});
export const cleanQueue = async () => {
try {
await jobQueue.obliterate({ force: true });
logger.info("Queue Cleaned");
} catch (error) {
logger.error("Error cleaning queue:", error);
}
};
export const scheduleJob = async (job: QueueJob) => {
if (job.type === "backup") {
await jobQueue.add(job.backupId, job, {
repeat: {
pattern: job.cronSchedule,
},
});
} else if (job.type === "server") {
await jobQueue.add(`${job.serverId}-cleanup`, job, {
repeat: {
pattern: job.cronSchedule,
},
});
} else if (job.type === "schedule") {
await jobQueue.add(job.scheduleId, job, {
repeat: {
pattern: job.cronSchedule,
tz: job.timezone || "UTC",
},
});
} else if (job.type === "volume-backup") {
await jobQueue.add(job.volumeBackupId, job, {
repeat: {
pattern: job.cronSchedule,
},
});
}
};
export const removeJob = async (data: QueueJob) => {
if (data.type === "backup") {
const { backupId, cronSchedule } = data;
const result = await jobQueue.removeRepeatable(backupId, {
pattern: cronSchedule,
});
return result;
}
if (data.type === "server") {
const { serverId, cronSchedule } = data;
const result = await jobQueue.removeRepeatable(`${serverId}-cleanup`, {
pattern: cronSchedule,
});
return result;
}
if (data.type === "schedule") {
const { scheduleId, cronSchedule, timezone } = data;
const result = await jobQueue.removeRepeatable(scheduleId, {
pattern: cronSchedule,
tz: timezone || "UTC",
});
return result;
}
if (data.type === "volume-backup") {
const { volumeBackupId, cronSchedule } = data;
const result = await jobQueue.removeRepeatable(volumeBackupId, {
pattern: cronSchedule,
});
return result;
}
return false;
};
export const getJobRepeatable = async (
data: QueueJob,
): Promise<RepeatableJob | null> => {
const repeatableJobs = await jobQueue.getRepeatableJobs();
if (data.type === "backup") {
const { backupId } = data;
const job = repeatableJobs.find((j) => j.name === backupId);
return job ? job : null;
}
if (data.type === "server") {
const { serverId } = data;
const job = repeatableJobs.find((j) => j.name === `${serverId}-cleanup`);
return job ? job : null;
}
if (data.type === "schedule") {
const { scheduleId } = data;
const job = repeatableJobs.find((j) => j.name === scheduleId);
return job ? job : null;
}
if (data.type === "volume-backup") {
const { volumeBackupId } = data;
const job = repeatableJobs.find((j) => j.name === volumeBackupId);
return job ? job : null;
}
return null;
};