Skip to content

Commit b58fb6b

Browse files
Zamoca42intcreator
andauthoredDec 10, 2024··
feat: support async handling and add CronJob status tracking (#894)
## Description This PR improves async handling in the CronJob class and adds status tracking functionality. - Modified the `fireOnTick()` method to return a Promise for better async callback handling - Added an `isCallbackRunning` flag to track the running state of CronJob instances - Updated the test suite to use the new async behavior and track the job's running state - Added `waitForCompletion` functionality to the `job.stop()` method - waits for running jobs to complete before executing the `onComplete` callback During test case writing, I encountered a type error with sinon. To resolve this, added `sinon.restore()` to the `afterEach` block. Reference: https://stackoverflow.com/questions/73232999/sinon-cant-install-fake-timers-twice-on-the-same-global-object <img width="811" alt="스크린샷 2024-09-03 오후 7 10 58" src="https://github.com/user-attachments/assets/b87deee7-14b2-4407-8fea-a1cb469ef44b"> ## Related Issue Closes #713 Closes #556 ## Motivation and Context These changes allow the CronJob class to handle asynchronous callbacks more effectively and provide a way to track the running state of jobs. ## How Has This Been Tested? - Updated existing test suite to verify the new async behavior - Adjusted test timeouts to use the `tickAsync` method - Added new test cases to check for proper waiting of running callbacks before stopping ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [x] My code follows the code style of this project. - [x] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [x] I have added tests to cover my changes. - [x] All new and existing tests passed. - [ ] If my change introduces a breaking change, I have added a `!` after the type/scope in the title (see the Conventional Commits standard). --------- Co-authored-by: Brandon der Blätter <intcreator@users.noreply.github.com>
1 parent 94465ae commit b58fb6b

File tree

4 files changed

+251
-131
lines changed

4 files changed

+251
-131
lines changed
 

‎README.md

+19
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ day of week 0-7 (0 or 7 is Sunday, or use names)
194194

195195
- `unrefTimeout`: [OPTIONAL] - Useful for controlling event loop behavior. More details [here](https://nodejs.org/api/timers.html#timers_timeout_unref).
196196

197+
- `waitForCompletion`: [OPTIONAL] - If `true`, no additional instances of the `onTick` callback function will run until the current onTick callback has completed. Any new scheduled executions that occur while the current callback is running will be skipped entirely. Default is `false`.
198+
197199
#### Methods
198200

199201
- `from` (static): Create a new CronJob object providing arguments as an object. See argument names and descriptions above.
@@ -214,6 +216,23 @@ day of week 0-7 (0 or 7 is Sunday, or use names)
214216

215217
- `addCallback`: Permits addition of `onTick` callbacks.
216218

219+
#### Properties
220+
221+
- `isCallbackRunning`: [READ-ONLY] Indicates if a callback is currently executing.
222+
223+
```javascript
224+
const job = new CronJob('* * * * * *', async () => {
225+
console.log(job.isCallbackRunning); // true during callback execution
226+
await someAsyncTask();
227+
console.log(job.isCallbackRunning); // still true until callback completes
228+
});
229+
230+
console.log(job.isCallbackRunning); // false
231+
job.start();
232+
console.log(job.running); // true (schedule is active)
233+
console.log(job.isCallbackRunning); // false (no callback executing)
234+
```
235+
217236
### CronTime Class
218237

219238
#### Constructor

‎src/job.ts

+68-24
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,16 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
2121
onComplete?: WithOnComplete<OC> extends true
2222
? CronOnCompleteCallback
2323
: undefined;
24+
waitForCompletion = false;
2425

26+
private _isCallbackRunning = false;
2527
private _timeout?: NodeJS.Timeout;
2628
private _callbacks: CronCallback<C, WithOnComplete<OC>>[] = [];
2729

30+
get isCallbackRunning() {
31+
return this._isCallbackRunning;
32+
}
33+
2834
constructor(
2935
cronTime: CronJobParams<OC, C>['cronTime'],
3036
onTick: CronJobParams<OC, C>['onTick'],
@@ -34,7 +40,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
3440
context?: CronJobParams<OC, C>['context'],
3541
runOnInit?: CronJobParams<OC, C>['runOnInit'],
3642
utcOffset?: null,
37-
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout']
43+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
44+
waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
3845
);
3946
constructor(
4047
cronTime: CronJobParams<OC, C>['cronTime'],
@@ -45,7 +52,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
4552
context?: CronJobParams<OC, C>['context'],
4653
runOnInit?: CronJobParams<OC, C>['runOnInit'],
4754
utcOffset?: CronJobParams<OC, C>['utcOffset'],
48-
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout']
55+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
56+
waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
4957
);
5058
constructor(
5159
cronTime: CronJobParams<OC, C>['cronTime'],
@@ -56,9 +64,11 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
5664
context?: CronJobParams<OC, C>['context'],
5765
runOnInit?: CronJobParams<OC, C>['runOnInit'],
5866
utcOffset?: CronJobParams<OC, C>['utcOffset'],
59-
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout']
67+
unrefTimeout?: CronJobParams<OC, C>['unrefTimeout'],
68+
waitForCompletion?: CronJobParams<OC, C>['waitForCompletion']
6069
) {
6170
this.context = (context ?? this) as CronContext<C>;
71+
this.waitForCompletion = Boolean(waitForCompletion);
6272

6373
// runtime check for JS users
6474
if (timeZone != null && utcOffset != null) {
@@ -92,7 +102,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
92102

93103
if (runOnInit) {
94104
this.lastExecution = new Date();
95-
this.fireOnTick();
105+
void this.fireOnTick();
96106
}
97107

98108
if (start) this.start();
@@ -117,7 +127,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
117127
params.context,
118128
params.runOnInit,
119129
params.utcOffset,
120-
params.unrefTimeout
130+
params.unrefTimeout,
131+
params.waitForCompletion
121132
);
122133
} else if (params.utcOffset != null) {
123134
return new CronJob<OC, C>(
@@ -129,7 +140,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
129140
params.context,
130141
params.runOnInit,
131142
params.utcOffset,
132-
params.unrefTimeout
143+
params.unrefTimeout,
144+
params.waitForCompletion
133145
);
134146
} else {
135147
return new CronJob<OC, C>(
@@ -141,7 +153,8 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
141153
params.context,
142154
params.runOnInit,
143155
params.utcOffset,
144-
params.unrefTimeout
156+
params.unrefTimeout,
157+
params.waitForCompletion
145158
);
146159
}
147160
}
@@ -193,14 +206,26 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
193206
return this.cronTime.sendAt();
194207
}
195208

196-
fireOnTick() {
197-
for (const callback of this._callbacks) {
198-
void callback.call(
199-
this.context,
200-
this.onComplete as WithOnComplete<OC> extends true
201-
? CronOnCompleteCallback
202-
: never
203-
);
209+
async fireOnTick() {
210+
if (!this.waitForCompletion && this._isCallbackRunning) return;
211+
212+
this._isCallbackRunning = true;
213+
214+
try {
215+
for (const callback of this._callbacks) {
216+
const result = callback.call(
217+
this.context,
218+
this.onComplete as WithOnComplete<OC> extends true
219+
? CronOnCompleteCallback
220+
: never
221+
);
222+
223+
if (this.waitForCompletion) await result;
224+
}
225+
} catch (error) {
226+
console.error('[Cron] error in callback', error);
227+
} finally {
228+
this._isCallbackRunning = false;
204229
}
205230
}
206231

@@ -209,9 +234,7 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
209234
}
210235

211236
start() {
212-
if (this.running) {
213-
return;
214-
}
237+
if (this.running) return;
215238

216239
const MAXDELAY = 2147483647; // The maximum number of milliseconds setTimeout will wait.
217240
let timeout = this.cronTime.getTimeout();
@@ -262,11 +285,9 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
262285
this.running = false;
263286

264287
// start before calling back so the callbacks have the ability to stop the cron job
265-
if (!this.runOnce) {
266-
this.start();
267-
}
288+
if (!this.runOnce) this.start();
268289

269-
this.fireOnTick();
290+
void this.fireOnTick();
270291
}
271292
};
272293

@@ -290,14 +311,37 @@ export class CronJob<OC extends CronOnCompleteCommand | null = null, C = null> {
290311
return this.lastExecution;
291312
}
292313

314+
private async _executeOnComplete() {
315+
if (typeof this.onComplete !== 'function') return;
316+
317+
try {
318+
await this.onComplete.call(this.context);
319+
} catch (error) {
320+
console.error('[Cron] error in onComplete callback:', error);
321+
}
322+
}
323+
324+
private async _waitForJobCompletion() {
325+
while (this._isCallbackRunning) {
326+
await new Promise(resolve => setTimeout(resolve, 100));
327+
}
328+
}
329+
293330
/**
294331
* Stop the cronjob.
295332
*/
296333
stop() {
297334
if (this._timeout) clearTimeout(this._timeout);
298335
this.running = false;
299-
if (typeof this.onComplete === 'function') {
300-
void this.onComplete.call(this.context);
336+
337+
if (!this.waitForCompletion) {
338+
void this._executeOnComplete();
339+
return;
301340
}
341+
342+
void Promise.resolve().then(async () => {
343+
await this._waitForJobCompletion();
344+
await this._executeOnComplete();
345+
});
302346
}
303347
}

‎src/types/cron.types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ interface BaseCronJobParams<
1515
context?: C;
1616
runOnInit?: boolean | null;
1717
unrefTimeout?: boolean | null;
18+
waitForCompletion?: boolean | null;
1819
}
1920

2021
export type CronJobParams<

‎tests/cron.test.ts

+163-107
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.