feat: implement background job system with queue, worker, scheduler, and handlers
- Add job queue abstraction (InMemoryQueue and Redis/BullMQ adapter) - Add polling worker with retry logic and exponential backoff - Add 6 job handlers: darkwatch.scan, voiceprint.batch, hometitle.scan, removebrokers.process, reports.generate, notifications.send - Add cron-based scheduler with tier-appropriate frequencies (Basic/Plus/Premium) - Add tRPC scheduler router for admin (runJobNow, getJobStatus, etc.) - Add entry point with graceful shutdown support - Achieve 100% test pass rate for new job system
This commit is contained in:
@@ -9,6 +9,7 @@ import { hometitleRouter } from "./routers/hometitle";
|
||||
import { removebrokersRouter } from "./routers/removebrokers";
|
||||
import { correlationRouter } from "./routers/correlation";
|
||||
import { reportsRouter } from "./routers/reports";
|
||||
import { schedulerRouter } from "./routers/scheduler";
|
||||
import { createTRPCRouter } from "./utils";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
@@ -23,6 +24,7 @@ export const appRouter = createTRPCRouter({
|
||||
removebrokers: removebrokersRouter,
|
||||
correlation: correlationRouter,
|
||||
reports: reportsRouter,
|
||||
scheduler: schedulerRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
82
web/src/server/api/routers/scheduler.ts
Normal file
82
web/src/server/api/routers/scheduler.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { wrap } from "@typeschema/valibot";
|
||||
import { object, string } from "valibot";
|
||||
import { createTRPCRouter, adminProcedure, publicProcedure } from "../utils";
|
||||
import { RunJobNowSchema, JobStatusSchema } from "../schemas/scheduler";
|
||||
import { getQueue } from "~/server/jobs";
|
||||
import { registerSchedules, scheduleForSubscription, removeSchedulesForSubscription, getCronOverview } from "~/server/jobs";
|
||||
import { JOB_TYPES, type JobType } from "~/server/jobs/queue";
|
||||
|
||||
export const schedulerRouter = createTRPCRouter({
|
||||
getCronOverview: adminProcedure.query(async () => {
|
||||
return { overview: getCronOverview() };
|
||||
}),
|
||||
|
||||
runJobNow: adminProcedure
|
||||
.input(wrap(RunJobNowSchema))
|
||||
.mutation(async ({ input }) => {
|
||||
const { type, payload } = input;
|
||||
|
||||
if (!JOB_TYPES.includes(type as JobType)) {
|
||||
throw new Error(`Invalid job type: ${type}. Must be one of: ${JOB_TYPES.join(", ")}`);
|
||||
}
|
||||
|
||||
const queue = getQueue();
|
||||
const job = await queue.enqueue(type as JobType, payload as any);
|
||||
return { jobId: job.id, status: job.status };
|
||||
}),
|
||||
|
||||
getJobStatus: adminProcedure
|
||||
.input(wrap(JobStatusSchema))
|
||||
.query(async ({ input }) => {
|
||||
const queue = getQueue();
|
||||
const job = await queue.getJob(input.jobId);
|
||||
if (!job) {
|
||||
throw new Error(`Job not found: ${input.jobId}`);
|
||||
}
|
||||
return {
|
||||
id: job.id,
|
||||
type: job.type,
|
||||
status: job.status,
|
||||
attempts: job.attempts,
|
||||
maxAttempts: job.maxAttempts,
|
||||
error: job.error ?? null,
|
||||
createdAt: job.createdAt,
|
||||
updatedAt: job.updatedAt,
|
||||
};
|
||||
}),
|
||||
|
||||
listJobs: adminProcedure
|
||||
.query(async () => {
|
||||
const queue = getQueue();
|
||||
const jobs = await queue.getJobs();
|
||||
return jobs.map((j) => ({
|
||||
id: j.id,
|
||||
type: j.type,
|
||||
status: j.status,
|
||||
attempts: j.attempts,
|
||||
maxAttempts: j.maxAttempts,
|
||||
error: j.error ?? null,
|
||||
createdAt: j.createdAt,
|
||||
updatedAt: j.updatedAt,
|
||||
}));
|
||||
}),
|
||||
|
||||
reloadSchedules: adminProcedure.mutation(async () => {
|
||||
await registerSchedules();
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
scheduleForTier: adminProcedure
|
||||
.input(wrap(object({ subscriptionId: string(), userId: string(), tier: string() })))
|
||||
.mutation(async ({ input }) => {
|
||||
scheduleForSubscription(input);
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
removeSchedules: adminProcedure
|
||||
.input(wrap(object({ subscriptionId: string() })))
|
||||
.mutation(async ({ input }) => {
|
||||
removeSchedulesForSubscription(input.subscriptionId);
|
||||
return { success: true };
|
||||
}),
|
||||
});
|
||||
19
web/src/server/api/schemas/scheduler.ts
Normal file
19
web/src/server/api/schemas/scheduler.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { object, string, optional, enumType } from "valibot";
|
||||
|
||||
export const RunJobNowSchema = object({
|
||||
type: string(),
|
||||
payload: object({
|
||||
userId: optional(string()),
|
||||
subscriptionId: optional(string()),
|
||||
requestId: optional(string()),
|
||||
reportType: optional(string()),
|
||||
reportScheduleId: optional(string()),
|
||||
channel: optional(string()),
|
||||
alertId: optional(string()),
|
||||
jobId: optional(string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const JobStatusSchema = object({
|
||||
jobId: string(),
|
||||
});
|
||||
Reference in New Issue
Block a user