240 lines
5.8 KiB
JavaScript
240 lines
5.8 KiB
JavaScript
// node_api/jobs/scheduler.js
|
|
const cron = require('node-cron');
|
|
const { logger } = require('../utils/logger');
|
|
const { depreciationJob } = require('./depreciationJob');
|
|
const { JobStatus } = require('../models/JobStatus');
|
|
|
|
class JobScheduler {
|
|
constructor() {
|
|
this.jobs = new Map();
|
|
this.isInitialized = false;
|
|
}
|
|
|
|
async initialize() {
|
|
if (this.isInitialized) {
|
|
logger.warn('Job scheduler already initialized');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.setupJobs();
|
|
this.isInitialized = true;
|
|
logger.info('Job scheduler initialized successfully');
|
|
} catch (error) {
|
|
logger.error('Failed to initialize job scheduler:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async setupJobs() {
|
|
// Monthly depreciation job - runs on 1st of each month at 2 AM
|
|
const depreciationJobSchedule = '0 2 1 * *';
|
|
|
|
const depreciationCronJob = cron.schedule(
|
|
depreciationJobSchedule,
|
|
async () => {
|
|
await this.runJob('depreciation', depreciationJob);
|
|
},
|
|
{
|
|
scheduled: false,
|
|
timezone: 'America/New_York'
|
|
}
|
|
);
|
|
|
|
this.jobs.set('depreciation', {
|
|
cronJob: depreciationCronJob,
|
|
schedule: depreciationJobSchedule,
|
|
name: 'Monthly Depreciation Calculation',
|
|
lastRun: null,
|
|
nextRun: null,
|
|
status: 'stopped'
|
|
});
|
|
|
|
logger.info('Jobs configured:', Array.from(this.jobs.keys()));
|
|
}
|
|
|
|
async runJob(jobName, jobFunction) {
|
|
const job = this.jobs.get(jobName);
|
|
if (!job) {
|
|
logger.error(`Job ${jobName} not found`);
|
|
return;
|
|
}
|
|
|
|
const startTime = new Date();
|
|
const jobExecutionId = `${jobName}_${startTime.getTime()}`;
|
|
|
|
try {
|
|
logger.info(`Starting job: ${jobName} (ID: ${jobExecutionId})`);
|
|
|
|
// Update job status
|
|
job.status = 'running';
|
|
job.lastRun = startTime;
|
|
|
|
// Log job start in database
|
|
await JobStatus.create({
|
|
job_name: jobName,
|
|
execution_id: jobExecutionId,
|
|
status: 'running',
|
|
started_at: startTime,
|
|
details: { message: 'Job started' }
|
|
});
|
|
|
|
// Execute the job function
|
|
const result = await jobFunction();
|
|
|
|
// Job completed successfully
|
|
const endTime = new Date();
|
|
const duration = endTime - startTime;
|
|
|
|
job.status = 'completed';
|
|
job.lastRun = endTime;
|
|
|
|
await JobStatus.update(
|
|
{
|
|
status: 'completed',
|
|
completed_at: endTime,
|
|
duration_ms: duration,
|
|
details: result
|
|
},
|
|
{
|
|
where: { execution_id: jobExecutionId }
|
|
}
|
|
);
|
|
|
|
logger.info(`Job ${jobName} completed successfully in ${duration}ms`, result);
|
|
|
|
} catch (error) {
|
|
// Job failed
|
|
const endTime = new Date();
|
|
const duration = endTime - startTime;
|
|
|
|
job.status = 'failed';
|
|
job.lastRun = endTime;
|
|
|
|
await JobStatus.update(
|
|
{
|
|
status: 'failed',
|
|
completed_at: endTime,
|
|
duration_ms: duration,
|
|
error_message: error.message,
|
|
details: { error: error.stack }
|
|
},
|
|
{
|
|
where: { execution_id: jobExecutionId }
|
|
}
|
|
);
|
|
|
|
logger.error(`Job ${jobName} failed:`, error);
|
|
|
|
// Send notification about job failure
|
|
await this.notifyJobFailure(jobName, error);
|
|
}
|
|
}
|
|
|
|
async notifyJobFailure(jobName, error) {
|
|
// TODO: Implement email notifications or webhook calls
|
|
logger.error(`Job failure notification for ${jobName}:`, error.message);
|
|
}
|
|
|
|
startAll() {
|
|
for (const [name, job] of this.jobs) {
|
|
job.cronJob.start();
|
|
job.status = 'scheduled';
|
|
job.nextRun = this.getNextRunTime(job.schedule);
|
|
logger.info(`Started job: ${name}, next run: ${job.nextRun}`);
|
|
}
|
|
}
|
|
|
|
stopAll() {
|
|
for (const [name, job] of this.jobs) {
|
|
job.cronJob.stop();
|
|
job.status = 'stopped';
|
|
job.nextRun = null;
|
|
logger.info(`Stopped job: ${name}`);
|
|
}
|
|
}
|
|
|
|
async runManual(jobName) {
|
|
const job = this.jobs.get(jobName);
|
|
if (!job) {
|
|
throw new Error(`Job ${jobName} not found`);
|
|
}
|
|
|
|
if (job.status === 'running') {
|
|
throw new Error(`Job ${jobName} is already running`);
|
|
}
|
|
|
|
// Get the job function
|
|
const jobFunction = this.getJobFunction(jobName);
|
|
if (!jobFunction) {
|
|
throw new Error(`Job function for ${jobName} not found`);
|
|
}
|
|
|
|
return await this.runJob(jobName, jobFunction);
|
|
}
|
|
|
|
getJobFunction(jobName) {
|
|
const jobFunctions = {
|
|
'depreciation': depreciationJob
|
|
};
|
|
return jobFunctions[jobName];
|
|
}
|
|
|
|
getNextRunTime(schedule) {
|
|
try {
|
|
const task = cron.schedule(schedule, () => {}, { scheduled: false });
|
|
return task.nextDate();
|
|
} catch (error) {
|
|
logger.error('Error calculating next run time:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
getJobStatus(jobName) {
|
|
const job = this.jobs.get(jobName);
|
|
if (!job) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name: job.name,
|
|
status: job.status,
|
|
schedule: job.schedule,
|
|
lastRun: job.lastRun,
|
|
nextRun: job.nextRun
|
|
};
|
|
}
|
|
|
|
getAllJobStatuses() {
|
|
const statuses = {};
|
|
for (const [name, job] of this.jobs) {
|
|
statuses[name] = this.getJobStatus(name);
|
|
}
|
|
return statuses;
|
|
}
|
|
|
|
async getJobHistory(jobName, limit = 50) {
|
|
try {
|
|
const history = await JobStatus.findAll({
|
|
where: { job_name: jobName },
|
|
order: [['started_at', 'DESC']],
|
|
limit: limit
|
|
});
|
|
return history;
|
|
} catch (error) {
|
|
logger.error('Error fetching job history:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
destroy() {
|
|
this.stopAll();
|
|
this.jobs.clear();
|
|
this.isInitialized = false;
|
|
logger.info('Job scheduler destroyed');
|
|
}
|
|
}
|
|
|
|
// Export singleton instance
|
|
const jobScheduler = new JobScheduler();
|
|
module.exports = { jobScheduler }; |