feat(schedules): add schedules server

This commit is contained in:
Mauricio Siu
2024-10-05 22:11:38 -06:00
parent 651bf3a303
commit 43555cdabe
19 changed files with 584 additions and 122 deletions

View File

@@ -0,0 +1,55 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import "dotenv/config";
import { zValidator } from "@hono/zod-validator";
import { jobQueueSchema } from "./schema";
import { firstWorker, secondWorker } from "./workers";
import { logger } from "./logger";
import { cleanQueue, removeJob, scheduleJob } from "./queue";
const app = new Hono();
cleanQueue();
app.post("/create-backup", zValidator("json", jobQueueSchema), (c) => {
const data = c.req.valid("json");
scheduleJob(data);
logger.info("Backup created successfully", data);
return c.json({ message: "Backup created successfully" });
});
app.post("/remove-job", zValidator("json", jobQueueSchema), async (c) => {
const data = c.req.valid("json");
const result = await removeJob(data);
logger.info("Job removed successfully", data);
return c.json({ message: "Job removed successfully", result });
});
app.get("/health", async (c) => {
return c.json({ status: "ok" });
});
export const gracefulShutdown = async (signal: string) => {
logger.warn(`Received ${signal}, closing server...`);
await firstWorker.close();
await secondWorker.close();
process.exit(0);
};
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
process.on("uncaughtException", (err) => {
logger.error(err, "Uncaught exception");
});
process.on("unhandledRejection", (reason, promise) => {
logger.error({ promise, reason }, "Unhandled Rejection at: Promise");
});
const port = Number.parseInt(process.env.PORT || "3000");
logger.info("Starting Schedules Server ✅", port);
serve({ fetch: app.fetch, port });

View File

@@ -0,0 +1,10 @@
import pino from "pino";
export const logger = pino({
transport: {
target: "pino-pretty",
options: {
colorize: true,
},
},
});

View File

@@ -0,0 +1,55 @@
import { Queue } from "bullmq";
import type { QueueJob } from "./schema";
import { logger } from "./logger";
export const jobQueue = new Queue("backupQueue", {
connection: {
host: 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 = (job: QueueJob) => {
if (job.type === "backup") {
jobQueue.add(job.backupId, job, {
repeat: {
pattern: job.cronSchedule,
},
});
} else if (job.type === "server") {
jobQueue.add(`${job.serverId}-cleanup`, 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;
}
};

View File

@@ -0,0 +1,16 @@
import { z } from "zod";
export const jobQueueSchema = z.discriminatedUnion("type", [
z.object({
cronSchedule: z.string(),
type: z.literal("backup"),
backupId: z.string(),
}),
z.object({
cronSchedule: z.string(),
type: z.literal("server"),
serverId: z.string(),
}),
]);
export type QueueJob = z.infer<typeof jobQueueSchema>;

View File

@@ -0,0 +1,42 @@
import {
cleanUpDockerBuilder,
cleanUpSystemPrune,
cleanUpUnusedImages,
findBackupById,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
runPostgresBackup,
} from "@dokploy/builders";
import type { QueueJob } from "./schema";
import { logger } from "./logger";
export const runJobs = async (job: QueueJob) => {
try {
if (job.type === "backup") {
const { backupId } = job;
const backup = await findBackupById(backupId);
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
if (databaseType === "postgres" && postgres) {
await runPostgresBackup(postgres, backup);
} else if (databaseType === "mysql" && mysql) {
await runMySqlBackup(mysql, backup);
} else if (databaseType === "mongo" && mongo) {
await runMongoBackup(mongo, backup);
} else if (databaseType === "mariadb" && mariadb) {
await runMariadbBackup(mariadb, backup);
}
}
if (job.type === "server") {
const { serverId } = job;
await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
// await sendDockerCleanupNotifications();
}
} catch (error) {
logger.error(error);
}
return true;
};

View File

@@ -0,0 +1,28 @@
import type { QueueJob } from "./schema";
import { type Job, Worker } from "bullmq";
import { runJobs } from "./utils";
export const firstWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
await runJobs(job.data);
},
{
concurrency: 50,
connection: {
host: process.env.REDIS_URL,
},
},
);
export const secondWorker = new Worker(
"backupQueue",
async (job: Job<QueueJob>) => {
await runJobs(job.data);
},
{
concurrency: 50,
connection: {
host: process.env.REDIS_URL,
},
},
);