Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
this.addCallback(this._fnWrap(onTick));

if (runOnInit) {
this.lastExecution = new Date();
void this.fireOnTick();
}

Expand Down Expand Up @@ -245,6 +244,10 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
// skip job if previous callback is still running
if (this.waitForCompletion && this._isCallbackRunning) return;

// record the moment the callback actually executes, so that lastDate()
// reflects the last real execution rather than the last tick check
// (ticks skipped above must not advance it). See #1048.
this.lastExecution = new Date();
this._isCallbackRunning = true;

// handle errors in synchronous and asynchronous callbacks
Expand Down Expand Up @@ -334,8 +337,6 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
setCronTimeout(timeout);
} else {
// we have arrived at the correct point in time.
this.lastExecution = new Date();

this._isActive = false;

// start before calling back so the callbacks have the ability to stop the cron job
Expand Down Expand Up @@ -364,7 +365,6 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
// execute immediately if within threshold
console.warn(`${message}. Executing immediately.`);

this.lastExecution = new Date();
void this.fireOnTick();
} else {
// skip job if beyond threshold
Expand Down
39 changes: 39 additions & 0 deletions tests/cron.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1436,6 +1436,45 @@ describe('cron', () => {
expect(job.isCallbackRunning).toBe(false);
expect(job.isActive).toBe(false);
});

it('should not advance lastDate() for ticks skipped due to waitForCompletion (#1048)', async () => {
const clock = sinon.useFakeTimers();

const job = new CronJob(
'* * * * * *',
async () => {
await new Promise<void>(resolve => {
setTimeout(resolve, 1500);
});
},
null,
true,
null,
null,
false,
null,
false,
true // waitForCompletion: true
);

// first execution starts at 1000ms
await clock.tickAsync(1000);
expect(job.isCallbackRunning).toBe(true);
expect(job.lastDate()?.getTime()).toBe(1000);

// the tick at 2000ms is skipped because the callback is still
// running, so lastDate() must still reflect the 1000ms execution.
await clock.tickAsync(1000);
expect(job.isCallbackRunning).toBe(true);
expect(job.lastDate()?.getTime()).toBe(1000);

// first callback completes at 2500ms, next execution starts at 3000ms
await clock.tickAsync(1000);
expect(job.isCallbackRunning).toBe(true);
expect(job.lastDate()?.getTime()).toBe(3000);

job.stop();
});
});

describe('Daylight Saving Time', () => {
Expand Down
Loading