Job Lifecycle Management
Understanding the complete job lifecycle in Chronos helps you build robust job processing systems.
Job States
A job in Chronos goes through several states during its lifecycle:
- Scheduled - Job is created and waiting to run
- Locked - Job is picked up by a processor
- Running - Job is currently executing
- Completed - Job finished successfully
- Failed - Job threw an error
- Disabled - Job is temporarily disabled
Advanced Job Methods
touch()
Resets the lock on the job. Useful to indicate that the job hasn't timed out when you have very long running jobs.
scheduler.define('super long job', async job => {
await doSomeLongTask();
await job.touch(); // Reset lock timer
await doAnotherLongTask();
await job.touch(); // Reset lock timer again
await finishOurLongTasks();
});
setShouldSaveResult(boolean)
Specifies whether the result of the job should be stored in the database. Defaults to false
.
const job = scheduler.create('data processing job', { input: 'data' });
job.setShouldSaveResult(true);
await job.save();
// After job completes, result will be available on job.attrs.result
unique(properties, [options])
Ensure that only one instance of this job exists with the specified properties.
const job = scheduler.create('user notification', { userId: '123' });
job.unique({
'data.userId': '123',
'name': 'user notification'
});
await job.save();
Options:
insertOnly
:boolean
- Prevents properties from persisting if job already exists
Important: Create MongoDB indexes on unique fields to avoid high CPU usage:
db.chronosJobs.createIndex({ "data.userId": 1, "name": 1 });
fail(reason)
Manually mark a job as failed with a specific reason.
scheduler.define('risky job', async job => {
try {
await riskyOperation();
} catch (error) {
if (error.code === 'INSUFFICIENT_DISK_SPACE') {
job.fail('insufficient disk space');
await job.save();
return;
}
throw error; // Let Chronos handle other errors
}
});
disable() / enable()
Control job execution programmatically.
// Disable a job
const jobs = await scheduler.jobs({ name: 'maintenance task' });
for (const job of jobs) {
job.disable();
await job.save();
}
// Enable later
for (const job of jobs) {
job.enable();
await job.save();
}
Job Options
When defining jobs, you can specify advanced options:
scheduler.define('advanced job', async job => {
// Job logic here
}, {
concurrency: 3, // Max 3 instances running simultaneously
lockLimit: 10, // Max 10 jobs locked at once
lockLifetime: 30000, // 30 second lock timeout
priority: 'high', // Job priority
shouldSaveResult: true // Save job results to database
});
Long Running Jobs
For jobs that run longer than the default lock lifetime:
scheduler.define('data migration', async job => {
const records = await getRecordsToMigrate();
for (let i = 0; i < records.length; i++) {
await migrateRecord(records[i]);
// Touch every 100 records to keep lock alive
if (i % 100 === 0) {
await job.touch();
}
}
}, {
lockLifetime: 300000 // 5 minutes
});
Error Handling Best Practices
scheduler.define('robust job', async job => {
const maxRetries = 3;
const currentAttempt = job.attrs.data.attempt || 1;
try {
await performWork(job.attrs.data);
} catch (error) {
if (currentAttempt < maxRetries) {
// Schedule retry with exponential backoff
const delay = Math.pow(2, currentAttempt) * 1000; // 2s, 4s, 8s
await scheduler.schedule(
new Date(Date.now() + delay),
'robust job',
{ ...job.attrs.data, attempt: currentAttempt + 1 }
);
} else {
// Max retries reached, fail permanently
job.fail(`Failed after ${maxRetries} attempts: ${error.message}`);
await job.save();
}
throw error;
}
});