fix(cronmanager): improve cron scheduling and lifecycle handling; add wake/wakeCycle to promptly recalculate scheduling when jobs are added/removed/started/stopped, fix timeout handling, and update tests and deps
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smarttime',
|
||||
version: '4.2.0',
|
||||
version: '4.2.1',
|
||||
description: 'Provides utilities for advanced time handling including cron jobs, timestamps, intervals, and more.'
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as plugins from './smarttime.plugins.js';
|
||||
import { CronManager } from './smarttime.classes.cronmanager.js';
|
||||
|
||||
import { CronParser } from './smarttime.classes.cronparser.js';
|
||||
|
||||
export type TJobFunction =
|
||||
| ((triggerTimeArg?: number) => void)
|
||||
| ((triggerTimeArg?: number) => Promise<any>);
|
||||
@@ -24,6 +22,9 @@ export class CronJob {
|
||||
* checks wether the cronjob needs to be executed
|
||||
*/
|
||||
public checkExecution(): number {
|
||||
if (this.status === 'stopped') {
|
||||
return this.nextExecutionUnix;
|
||||
}
|
||||
if (this.nextExecutionUnix === 0) {
|
||||
this.getNextExecutionTime();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as plugins from './smarttime.plugins.js';
|
||||
import { CronJob, type TJobFunction } from './smarttime.classes.cronjob.js';
|
||||
import { getMilliSecondsAsHumanReadableString } from './smarttime.units.js';
|
||||
|
||||
export class CronManager {
|
||||
public executionTimeout: plugins.smartdelay.Timeout<void>;
|
||||
@@ -8,13 +7,26 @@ export class CronManager {
|
||||
public status: 'started' | 'stopped' = 'stopped';
|
||||
public cronjobs = new plugins.lik.ObjectMap<CronJob>();
|
||||
|
||||
private cycleWakeDeferred: plugins.smartpromise.Deferred<void> | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Resolves the current wake deferred, causing the sleeping cycle
|
||||
* to immediately recalculate instead of waiting for its timeout.
|
||||
*/
|
||||
private wakeCycle() {
|
||||
if (this.cycleWakeDeferred && this.cycleWakeDeferred.status === 'pending') {
|
||||
this.cycleWakeDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
public addCronjob(cronIdentifierArg: string, cronFunctionArg: TJobFunction) {
|
||||
const newCronJob = new CronJob(this, cronIdentifierArg, cronFunctionArg);
|
||||
this.cronjobs.add(newCronJob);
|
||||
if (this.status === 'started') {
|
||||
newCronJob.start();
|
||||
this.wakeCycle();
|
||||
}
|
||||
|
||||
return newCronJob;
|
||||
@@ -23,6 +35,9 @@ export class CronManager {
|
||||
public removeCronjob(cronjobArg: CronJob) {
|
||||
cronjobArg.stop();
|
||||
this.cronjobs.remove(cronjobArg);
|
||||
if (this.status === 'started') {
|
||||
this.wakeCycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,35 +54,39 @@ export class CronManager {
|
||||
}
|
||||
|
||||
private async runCronCycle() {
|
||||
this.executionTimeout = new plugins.smartdelay.Timeout(0);
|
||||
do {
|
||||
let nextRunningCronjob: CronJob;
|
||||
while (this.status === 'started') {
|
||||
// Create a fresh wake signal for this iteration
|
||||
this.cycleWakeDeferred = new plugins.smartpromise.Deferred<void>();
|
||||
|
||||
// Check all cronjobs and find the soonest next execution
|
||||
let soonestMs = Infinity;
|
||||
for (const cronJob of this.cronjobs.getArray()) {
|
||||
cronJob.checkExecution();
|
||||
if (
|
||||
!nextRunningCronjob ||
|
||||
cronJob.getTimeToNextExecution() < nextRunningCronjob.getTimeToNextExecution()
|
||||
) {
|
||||
nextRunningCronjob = cronJob;
|
||||
const msToNext = cronJob.getTimeToNextExecution();
|
||||
if (msToNext < soonestMs) {
|
||||
soonestMs = msToNext;
|
||||
}
|
||||
}
|
||||
if (nextRunningCronjob) {
|
||||
this.executionTimeout = new plugins.smartdelay.Timeout(
|
||||
nextRunningCronjob.getTimeToNextExecution()
|
||||
);
|
||||
console.log(
|
||||
`Next CronJob scheduled in ${getMilliSecondsAsHumanReadableString(
|
||||
this.executionTimeout.getTimeLeft()
|
||||
)}`
|
||||
);
|
||||
} else {
|
||||
this.executionTimeout = new plugins.smartdelay.Timeout(1000);
|
||||
console.log('no cronjobs specified! Checking again in 1 second');
|
||||
}
|
||||
|
||||
await this.executionTimeout.promise;
|
||||
} while (this.status === 'started');
|
||||
};
|
||||
// Sleep until the next job is due or until woken by a lifecycle event
|
||||
if (soonestMs < Infinity && soonestMs > 0) {
|
||||
this.executionTimeout = new plugins.smartdelay.Timeout(soonestMs);
|
||||
await Promise.race([
|
||||
this.executionTimeout.promise,
|
||||
this.cycleWakeDeferred.promise,
|
||||
]);
|
||||
// Cancel the timeout to avoid lingering timers
|
||||
this.executionTimeout.cancel();
|
||||
} else if (soonestMs <= 0) {
|
||||
// Job is overdue, loop immediately to execute it
|
||||
continue;
|
||||
} else {
|
||||
// No jobs — wait indefinitely until woken by addCronjob or stop
|
||||
await this.cycleWakeDeferred.promise;
|
||||
}
|
||||
}
|
||||
this.cycleWakeDeferred = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* stops all cronjobs
|
||||
@@ -75,9 +94,10 @@ export class CronManager {
|
||||
public stop() {
|
||||
if (this.status === 'started') {
|
||||
this.status = 'stopped';
|
||||
this.executionTimeout.cancel();
|
||||
} else {
|
||||
console.log(`You tried to stop a CronManager that was not actually started.`);
|
||||
if (this.executionTimeout) {
|
||||
this.executionTimeout.cancel();
|
||||
}
|
||||
this.wakeCycle();
|
||||
}
|
||||
for (const cron of this.cronjobs.getArray()) {
|
||||
cron.stop();
|
||||
|
||||
Reference in New Issue
Block a user