168 lines
5.3 KiB
TypeScript
168 lines
5.3 KiB
TypeScript
import { Queue, Worker, type Job } from 'bullmq';
|
|
import { env } from '../config/env';
|
|
import { logger } from '../utils/logger';
|
|
|
|
const QUEUE_NAME = 'scheduled-jobs';
|
|
|
|
type ScheduledJobType =
|
|
| 'reengagement-scan'
|
|
| 'social-digest-scan'
|
|
| 'close-abandoned-canvass-sessions'
|
|
| 'close-stale-tracking-sessions'
|
|
| 'cleanup-tracking-data'
|
|
| 'cleanup-docs-analytics'
|
|
| 'cleanup-verification-tokens'
|
|
| 'listmonk-full-sync'
|
|
| 'validate-mkdocs-exports'
|
|
| 'cleanup-docs-collab-states'
|
|
| 'purge-expired-participant-needs';
|
|
|
|
interface ScheduledJobData {
|
|
type: ScheduledJobType;
|
|
}
|
|
|
|
const HOUR = 60 * 60 * 1000;
|
|
|
|
const JOB_DEFINITIONS: Array<{ type: ScheduledJobType; every: number; conditional?: boolean }> = [
|
|
{ type: 'reengagement-scan', every: 24 * HOUR },
|
|
{ type: 'social-digest-scan', every: 24 * HOUR },
|
|
{ type: 'close-abandoned-canvass-sessions', every: HOUR },
|
|
{ type: 'close-stale-tracking-sessions', every: HOUR },
|
|
{ type: 'cleanup-tracking-data', every: 24 * HOUR },
|
|
{ type: 'cleanup-docs-analytics', every: 24 * HOUR },
|
|
{ type: 'cleanup-verification-tokens', every: HOUR },
|
|
{ type: 'listmonk-full-sync', every: 6 * HOUR, conditional: true },
|
|
{ type: 'validate-mkdocs-exports', every: 24 * HOUR },
|
|
{ type: 'cleanup-docs-collab-states', every: 24 * HOUR },
|
|
{ type: 'purge-expired-participant-needs', every: 24 * HOUR },
|
|
];
|
|
|
|
async function executeJob(type: ScheduledJobType): Promise<void> {
|
|
switch (type) {
|
|
case 'reengagement-scan': {
|
|
const { reengagementService } = await import('./reengagement.service');
|
|
await reengagementService.scan();
|
|
break;
|
|
}
|
|
case 'social-digest-scan': {
|
|
const { socialDigestService } = await import('./social-digest.service');
|
|
await socialDigestService.scan();
|
|
break;
|
|
}
|
|
case 'close-abandoned-canvass-sessions': {
|
|
const { canvassService } = await import('../modules/map/canvass/canvass.service');
|
|
await canvassService.closeAbandonedSessions();
|
|
break;
|
|
}
|
|
case 'close-stale-tracking-sessions': {
|
|
const { trackingService } = await import('../modules/map/tracking/tracking.service');
|
|
await trackingService.closeStaleTrackingSessions(120);
|
|
break;
|
|
}
|
|
case 'cleanup-tracking-data': {
|
|
const { trackingService } = await import('../modules/map/tracking/tracking.service');
|
|
await trackingService.cleanupOldData(30);
|
|
break;
|
|
}
|
|
case 'cleanup-docs-analytics': {
|
|
const { docsAnalyticsService } = await import('../modules/docs-analytics/docs-analytics.service');
|
|
await docsAnalyticsService.cleanupOldData(90);
|
|
break;
|
|
}
|
|
case 'cleanup-verification-tokens': {
|
|
const { verificationTokenService } = await import('./verification-token.service');
|
|
const { passwordResetTokenService } = await import('./password-reset-token.service');
|
|
await verificationTokenService.cleanupExpiredTokens();
|
|
await passwordResetTokenService.cleanupExpiredTokens();
|
|
break;
|
|
}
|
|
case 'listmonk-full-sync': {
|
|
const { listmonkSyncService } = await import('./listmonk-sync.service');
|
|
await listmonkSyncService.syncAll();
|
|
break;
|
|
}
|
|
case 'validate-mkdocs-exports': {
|
|
const { pagesService } = await import('../modules/pages/pages.service');
|
|
await pagesService.validateExports();
|
|
break;
|
|
}
|
|
case 'cleanup-docs-collab-states': {
|
|
const { docsCollabService } = await import('../modules/docs/docs-collab.service');
|
|
await docsCollabService.cleanupStaleStates();
|
|
break;
|
|
}
|
|
case 'purge-expired-participant-needs': {
|
|
const { participantNeedsService } = await import('../modules/people/participant-needs.service');
|
|
await participantNeedsService.purgeExpired();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
class ScheduledJobsQueueService {
|
|
private queue: Queue;
|
|
private worker: Worker | null = null;
|
|
|
|
constructor() {
|
|
this.queue = new Queue(QUEUE_NAME, {
|
|
connection: { url: env.REDIS_URL },
|
|
defaultJobOptions: {
|
|
removeOnComplete: { age: 60 * 60, count: 200 },
|
|
removeOnFail: { age: 24 * 60 * 60 },
|
|
},
|
|
});
|
|
}
|
|
|
|
startWorker() {
|
|
// Register repeatable jobs
|
|
for (const def of JOB_DEFINITIONS) {
|
|
// Skip conditional jobs when their feature is disabled
|
|
if (def.type === 'listmonk-full-sync' && env.LISTMONK_SYNC_ENABLED !== 'true') {
|
|
continue;
|
|
}
|
|
|
|
this.queue.add(
|
|
def.type,
|
|
{ type: def.type } satisfies ScheduledJobData,
|
|
{
|
|
repeat: { every: def.every },
|
|
jobId: `scheduled-${def.type}`,
|
|
}
|
|
);
|
|
}
|
|
|
|
this.worker = new Worker(
|
|
QUEUE_NAME,
|
|
async (job: Job<ScheduledJobData>) => {
|
|
const { type } = job.data;
|
|
logger.debug(`Scheduled job starting: ${type}`);
|
|
await executeJob(type);
|
|
},
|
|
{
|
|
connection: { url: env.REDIS_URL },
|
|
concurrency: 2,
|
|
}
|
|
);
|
|
|
|
this.worker.on('completed', (job) => {
|
|
logger.debug(`Scheduled job ${job.name} completed`);
|
|
});
|
|
|
|
this.worker.on('failed', (job, err) => {
|
|
logger.error(`Scheduled job ${job?.name} failed: ${err.message}`);
|
|
});
|
|
|
|
logger.info('Scheduled jobs queue worker started (11 job types)');
|
|
}
|
|
|
|
async close() {
|
|
if (this.worker) {
|
|
await this.worker.close();
|
|
}
|
|
await this.queue.close();
|
|
logger.info('Scheduled jobs queue closed');
|
|
}
|
|
}
|
|
|
|
export const scheduledJobsQueueService = new ScheduledJobsQueueService();
|