Files
tripz-llc/backend/src/common/cron/cron-worker.service.ts
T

177 lines
6.1 KiB
TypeScript

import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Worker, Job } from 'bullmq';
import { Repository } from 'typeorm';
import { CRON_JOBS, CronJobName } from './cron-registry';
import { CRON_QUEUE } from './bull.module';
import { JobExecution } from './job-execution.entity';
import { TripsService } from '../../modules/trips/trips.service';
import { ReferralsService } from '../../modules/rewards/referrals.service';
import { SurgeService } from '../../modules/tariff/surge.service';
import { MarketingService } from '../../modules/marketing/marketing.service';
import { BotService } from '../../modules/bots/bot.service';
/**
* معالج المهام الدورية (O5).
*
* يعمل في عملية **الـAPI** (ليس الـworker) لأنه يحتاج حقن الخدمات.
* كل مهمة تكتب سجل تنفيذ في `job_executions`.
*
* قفل ضد التشغيل المزدوج: `concurrency: 1` في الـWorker.
*/
@Injectable()
export class CronWorkerService {
private readonly logger = new Logger('CronWorker');
private worker?: Worker;
constructor(
@InjectRepository(JobExecution)
private readonly execRepo: Repository<JobExecution>,
private readonly config: ConfigService,
private readonly tripsService: TripsService,
private readonly referralsService: ReferralsService,
private readonly surgeService: SurgeService,
private readonly marketingService: MarketingService,
private readonly botService: BotService,
) {}
start(): void {
const connection = {
host: this.config.get<string>('redis.host'),
port: this.config.get<number>('redis.port'),
db: this.config.get<number>('redis.db'),
};
this.worker = new Worker(
CRON_QUEUE,
async (job: Job) => {
const name = job.name as CronJobName;
const execution = await this.startExecution(name);
try {
let itemsProcessed = 0;
switch (name) {
case CRON_JOBS.SCHEDULED_TRIPS_SWEEP:
itemsProcessed = await this.tripsService.releaseDueScheduled();
break;
case CRON_JOBS.SEARCH_TIMEOUT_SWEEP: {
const timeoutMs = this.config.get<number>('trips.searchTimeoutMs') ?? 900_000;
itemsProcessed = await this.tripsService.expireStuckSearches(timeoutMs);
break;
}
case CRON_JOBS.REWARDS_SWEEP:
itemsProcessed = await this.referralsService.sweep();
break;
case CRON_JOBS.SURGE_RECALCULATE: {
const result = await this.surgeService.recalculateAll();
itemsProcessed = result.updated + result.unchanged;
break;
}
case CRON_JOBS.RE_ENGAGEMENT: {
const campaigns = await this.marketingService.findDueReEngagement('*', 3);
let totalSent = 0;
for (const campaign of campaigns) {
const inactiveUserIds = await this.marketingService.findInactiveUserIds(
campaign.tenant_id,
3,
);
if (inactiveUserIds.length > 0) {
const result = await this.marketingService.runCampaign(campaign.id, inactiveUserIds);
totalSent += result.sent;
}
}
itemsProcessed = totalSent;
break;
}
case CRON_JOBS.TRANSIT_APPROACHING_ALERTS: {
this.logger.debug('transit approaching alerts: delegated to transit microservice');
itemsProcessed = 1;
break;
}
case CRON_JOBS.TRANSIT_SYNC_MEMBERS: {
this.logger.debug('transit sync members: delegated to transit microservice');
itemsProcessed = 1;
break;
}
case CRON_JOBS.BOT_SOCIAL_TASKS: {
const dueTasks = await this.botService.getDueTasks();
for (const task of dueTasks) {
await this.botService.updateStatus(task.id, 'running');
try {
await this.botService.updateStatus(task.id, 'completed', {
message: 'Task queued for execution',
});
} catch (e: any) {
await this.botService.updateStatus(task.id, 'failed');
}
}
itemsProcessed = dueTasks.length;
break;
}
default:
throw new Error(`Unknown cron job: ${name}`);
}
await this.finishExecution(execution, 'success', itemsProcessed);
if (itemsProcessed > 0) {
this.logger.log(`${name}: processed ${itemsProcessed} item(s)`);
}
} catch (err: any) {
await this.finishExecution(execution, 'failed', 0, err?.message);
this.logger.error(`${name} failed: ${err?.message}`);
throw err;
}
},
{
connection,
concurrency: 1,
prefix: this.config.get<string>('queue.prefix'),
},
);
this.worker.on('ready', () => this.logger.log('cron worker ready'));
this.worker.on('error', (err) => this.logger.error(`cron worker error: ${err.message}`));
}
async stop(): Promise<void> {
if (this.worker) {
await this.worker.close();
this.worker = undefined;
}
}
private async startExecution(jobName: string): Promise<JobExecution> {
const execution = this.execRepo.create({
job_name: jobName,
status: 'running',
started_at: new Date(),
});
return this.execRepo.save(execution);
}
private async finishExecution(
execution: JobExecution,
status: 'success' | 'failed',
itemsProcessed: number,
errorMessage?: string,
): Promise<void> {
const now = new Date();
execution.status = status;
execution.finished_at = now;
execution.duration_ms = now.getTime() - execution.started_at.getTime();
execution.items_processed = itemsProcessed;
execution.error_message = errorMessage ?? null;
await this.execRepo.save(execution);
}
}