Skip to main content

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;
}
});