@fluojs/cron 1.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +65 -9
- package/README.md +65 -9
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +4 -1
- package/dist/distributed-lock-manager.d.ts +13 -6
- package/dist/distributed-lock-manager.d.ts.map +1 -1
- package/dist/distributed-lock-manager.js +76 -34
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +37 -11
- package/dist/random-id.d.ts +3 -0
- package/dist/random-id.d.ts.map +1 -0
- package/dist/random-id.js +8 -0
- package/dist/service.d.ts +10 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +137 -63
- package/dist/task-discovery.d.ts +15 -1
- package/dist/task-discovery.d.ts.map +1 -1
- package/dist/task-discovery.js +28 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/dist/module.js
CHANGED
|
@@ -1,14 +1,36 @@
|
|
|
1
1
|
import { defineModule } from '@fluojs/runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { createCronRandomId } from './random-id.js';
|
|
3
3
|
import { defaultCronScheduler } from './scheduler.js';
|
|
4
|
+
import { CronLifecycleService } from './service.js';
|
|
4
5
|
import { CRON_OPTIONS, SCHEDULING_REGISTRY } from './tokens.js';
|
|
5
6
|
const DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
6
|
-
function
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
function normalizeRedisClientName(clientName) {
|
|
8
|
+
if (clientName === undefined) {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
const normalizedClientName = clientName.trim();
|
|
12
|
+
if (normalizedClientName.length === 0) {
|
|
13
|
+
throw new Error('Cron distributed clientName must be a non-empty string when provided.');
|
|
10
14
|
}
|
|
11
|
-
return
|
|
15
|
+
return normalizedClientName;
|
|
16
|
+
}
|
|
17
|
+
function assertValidDistributedLockTtlMs(lockTtlMs) {
|
|
18
|
+
if (!Number.isFinite(lockTtlMs) || !Number.isInteger(lockTtlMs) || lockTtlMs < 1_000) {
|
|
19
|
+
throw new Error('Cron distributed lockTtlMs must be a positive integer greater than or equal to 1000ms.');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function normalizeDistributedOwnerId(ownerId) {
|
|
23
|
+
if (ownerId === undefined) {
|
|
24
|
+
return createCronRandomId();
|
|
25
|
+
}
|
|
26
|
+
if (typeof ownerId !== 'string') {
|
|
27
|
+
throw new Error('Cron distributed ownerId must be a string when provided.');
|
|
28
|
+
}
|
|
29
|
+
const normalizedOwnerId = ownerId.trim();
|
|
30
|
+
if (normalizedOwnerId.length === 0) {
|
|
31
|
+
throw new Error('Cron distributed ownerId must be a non-empty string when provided.');
|
|
32
|
+
}
|
|
33
|
+
return normalizedOwnerId;
|
|
12
34
|
}
|
|
13
35
|
function normalizeDistributedOptions(distributed) {
|
|
14
36
|
if (distributed === undefined || distributed === false) {
|
|
@@ -17,7 +39,7 @@ function normalizeDistributedOptions(distributed) {
|
|
|
17
39
|
enabled: false,
|
|
18
40
|
keyPrefix: 'fluo:cron:lock',
|
|
19
41
|
lockTtlMs: 30_000,
|
|
20
|
-
ownerId:
|
|
42
|
+
ownerId: createCronRandomId()
|
|
21
43
|
};
|
|
22
44
|
}
|
|
23
45
|
if (distributed === true) {
|
|
@@ -26,16 +48,20 @@ function normalizeDistributedOptions(distributed) {
|
|
|
26
48
|
enabled: true,
|
|
27
49
|
keyPrefix: 'fluo:cron:lock',
|
|
28
50
|
lockTtlMs: 30_000,
|
|
29
|
-
ownerId:
|
|
51
|
+
ownerId: createCronRandomId()
|
|
30
52
|
};
|
|
31
53
|
}
|
|
32
|
-
|
|
33
|
-
clientName: distributed.clientName,
|
|
54
|
+
const normalizedDistributed = {
|
|
55
|
+
clientName: normalizeRedisClientName(distributed.clientName),
|
|
34
56
|
enabled: distributed.enabled ?? true,
|
|
35
57
|
keyPrefix: distributed.keyPrefix ?? 'fluo:cron:lock',
|
|
36
58
|
lockTtlMs: distributed.lockTtlMs ?? 30_000,
|
|
37
|
-
ownerId: distributed.ownerId
|
|
59
|
+
ownerId: normalizeDistributedOwnerId(distributed.ownerId)
|
|
38
60
|
};
|
|
61
|
+
if (normalizedDistributed.enabled) {
|
|
62
|
+
assertValidDistributedLockTtlMs(normalizedDistributed.lockTtlMs);
|
|
63
|
+
}
|
|
64
|
+
return normalizedDistributed;
|
|
39
65
|
}
|
|
40
66
|
function normalizeShutdownOptions(shutdown) {
|
|
41
67
|
const timeoutMs = shutdown?.timeoutMs ?? DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random-id.d.ts","sourceRoot":"","sources":["../src/random-id.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Creates a platform-neutral random identifier for Cron ownership boundaries. */
|
|
2
|
+
export function createCronRandomId() {
|
|
3
|
+
const randomUUID = globalThis.crypto?.randomUUID;
|
|
4
|
+
if (randomUUID) {
|
|
5
|
+
return randomUUID.call(globalThis.crypto);
|
|
6
|
+
}
|
|
7
|
+
return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
8
|
+
}
|
package/dist/service.d.ts
CHANGED
|
@@ -15,10 +15,11 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
15
15
|
private readonly logger;
|
|
16
16
|
private readonly tasks;
|
|
17
17
|
private readonly activeTasks;
|
|
18
|
-
private readonly
|
|
18
|
+
private readonly runningDistributedLeaseCounts;
|
|
19
19
|
private readonly distributedLocks;
|
|
20
20
|
private readonly taskRunner;
|
|
21
21
|
private lifecycleState;
|
|
22
|
+
private shutdownDeadlineMs;
|
|
22
23
|
private started;
|
|
23
24
|
private shutdownPromise;
|
|
24
25
|
constructor(options: NormalizedCronModuleOptions, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger);
|
|
@@ -53,7 +54,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
53
54
|
* Removes a registered task by name.
|
|
54
55
|
*
|
|
55
56
|
* @param name Task name to remove.
|
|
56
|
-
* @returns `true` when a task existed and was removed.
|
|
57
|
+
* @returns `true` when a task existed and was removed; `false` when it was absent or its handle could not stop.
|
|
57
58
|
*/
|
|
58
59
|
remove(name: string): boolean;
|
|
59
60
|
/**
|
|
@@ -67,7 +68,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
67
68
|
* Disables a task without removing its descriptor.
|
|
68
69
|
*
|
|
69
70
|
* @param name Task name to disable.
|
|
70
|
-
* @returns `true` when the task exists
|
|
71
|
+
* @returns `true` when the task exists and its handle stopped; `false` when it was absent or its handle could not stop.
|
|
71
72
|
*/
|
|
72
73
|
disable(name: string): boolean;
|
|
73
74
|
/**
|
|
@@ -88,6 +89,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
88
89
|
*
|
|
89
90
|
* @param name Name of the cron task to update.
|
|
90
91
|
* @param expression New cron expression to validate and schedule.
|
|
92
|
+
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
|
|
91
93
|
*/
|
|
92
94
|
updateCronExpression(name: string, expression: string): void;
|
|
93
95
|
/**
|
|
@@ -95,6 +97,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
95
97
|
*
|
|
96
98
|
* @param name Name of the interval task to update.
|
|
97
99
|
* @param ms New positive interval in milliseconds.
|
|
100
|
+
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
|
|
98
101
|
*/
|
|
99
102
|
updateIntervalMs(name: string, ms: number): void;
|
|
100
103
|
onApplicationBootstrap(): Promise<void>;
|
|
@@ -107,7 +110,10 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
107
110
|
private startLifecycle;
|
|
108
111
|
private validateDistributedLockConfiguration;
|
|
109
112
|
private handleStartupFailure;
|
|
113
|
+
private completeStartupFailureCleanupAfterActiveTasks;
|
|
114
|
+
private resetDistributedLocksAfterStartupFailure;
|
|
110
115
|
private runShutdownLifecycle;
|
|
116
|
+
private getRemainingShutdownTimeoutMs;
|
|
111
117
|
private getRunningDistributedLockKeys;
|
|
112
118
|
private registerDecoratorTasks;
|
|
113
119
|
private registerTask;
|
|
@@ -119,6 +125,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
119
125
|
private unscheduleTask;
|
|
120
126
|
private stopScheduledHandle;
|
|
121
127
|
private handleTaskTick;
|
|
128
|
+
private isTaskTickCurrent;
|
|
122
129
|
private runTaskTick;
|
|
123
130
|
private shouldUseDistributedExecution;
|
|
124
131
|
private runDistributedTaskTick;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.js
CHANGED
|
@@ -9,7 +9,7 @@ import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs
|
|
|
9
9
|
import { Cron as CronValidator } from 'croner';
|
|
10
10
|
import { CronDistributedLockManager } from './distributed-lock-manager.js';
|
|
11
11
|
import { createCronPlatformStatusSnapshot } from './status.js';
|
|
12
|
-
import { createLockKey, discoverCronTaskDescriptors } from './task-discovery.js';
|
|
12
|
+
import { assertValidSchedulingTaskName, createLockKey, discoverCronTaskDescriptors, resolveSchedulingTaskName } from './task-discovery.js';
|
|
13
13
|
import { CronTaskRunner } from './task-runner.js';
|
|
14
14
|
import { CRON_OPTIONS } from './tokens.js';
|
|
15
15
|
function assertValidLockTtlMs(lockTtlMs) {
|
|
@@ -17,18 +17,8 @@ function assertValidLockTtlMs(lockTtlMs) {
|
|
|
17
17
|
throw new Error('Cron distributed lockTtlMs must be a positive integer greater than or equal to 1000ms.');
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
function assertValidTaskName(name) {
|
|
21
|
-
if (name.trim().length === 0) {
|
|
22
|
-
throw new Error('Scheduling task name must be a non-empty string.');
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
20
|
function resolveDynamicTaskName(name, optionName) {
|
|
26
|
-
|
|
27
|
-
if (optionName !== undefined) {
|
|
28
|
-
assertValidTaskName(optionName);
|
|
29
|
-
return optionName;
|
|
30
|
-
}
|
|
31
|
-
return name;
|
|
21
|
+
return resolveSchedulingTaskName(name, optionName);
|
|
32
22
|
}
|
|
33
23
|
function assertValidMs(ms, context) {
|
|
34
24
|
if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
|
|
@@ -54,6 +44,9 @@ function createRedisDependencyId(name) {
|
|
|
54
44
|
}
|
|
55
45
|
return `redis.${normalizedName}`;
|
|
56
46
|
}
|
|
47
|
+
function getRemainingTimeoutMs(deadlineMs) {
|
|
48
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
49
|
+
}
|
|
57
50
|
|
|
58
51
|
/**
|
|
59
52
|
* Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
|
|
@@ -69,10 +62,11 @@ class CronLifecycleService {
|
|
|
69
62
|
}
|
|
70
63
|
tasks = new Map();
|
|
71
64
|
activeTasks = new Set();
|
|
72
|
-
|
|
65
|
+
runningDistributedLeaseCounts = new Map();
|
|
73
66
|
distributedLocks;
|
|
74
67
|
taskRunner;
|
|
75
68
|
lifecycleState = 'created';
|
|
69
|
+
shutdownDeadlineMs;
|
|
76
70
|
started = false;
|
|
77
71
|
shutdownPromise;
|
|
78
72
|
constructor(options, runtimeContainer, compiledModules, logger) {
|
|
@@ -167,14 +161,16 @@ class CronLifecycleService {
|
|
|
167
161
|
* Removes a registered task by name.
|
|
168
162
|
*
|
|
169
163
|
* @param name Task name to remove.
|
|
170
|
-
* @returns `true` when a task existed and was removed.
|
|
164
|
+
* @returns `true` when a task existed and was removed; `false` when it was absent or its handle could not stop.
|
|
171
165
|
*/
|
|
172
166
|
remove(name) {
|
|
173
167
|
const task = this.tasks.get(name);
|
|
174
168
|
if (!task) {
|
|
175
169
|
return false;
|
|
176
170
|
}
|
|
177
|
-
this.unscheduleTask(task)
|
|
171
|
+
if (!this.unscheduleTask(task)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
178
174
|
this.tasks.delete(name);
|
|
179
175
|
return true;
|
|
180
176
|
}
|
|
@@ -211,7 +207,7 @@ class CronLifecycleService {
|
|
|
211
207
|
* Disables a task without removing its descriptor.
|
|
212
208
|
*
|
|
213
209
|
* @param name Task name to disable.
|
|
214
|
-
* @returns `true` when the task exists
|
|
210
|
+
* @returns `true` when the task exists and its handle stopped; `false` when it was absent or its handle could not stop.
|
|
215
211
|
*/
|
|
216
212
|
disable(name) {
|
|
217
213
|
const task = this.tasks.get(name);
|
|
@@ -222,8 +218,7 @@ class CronLifecycleService {
|
|
|
222
218
|
return true;
|
|
223
219
|
}
|
|
224
220
|
task.enabled = false;
|
|
225
|
-
this.unscheduleTask(task);
|
|
226
|
-
return true;
|
|
221
|
+
return this.unscheduleTask(task);
|
|
227
222
|
}
|
|
228
223
|
|
|
229
224
|
/**
|
|
@@ -251,6 +246,7 @@ class CronLifecycleService {
|
|
|
251
246
|
*
|
|
252
247
|
* @param name Name of the cron task to update.
|
|
253
248
|
* @param expression New cron expression to validate and schedule.
|
|
249
|
+
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
|
|
254
250
|
*/
|
|
255
251
|
updateCronExpression(name, expression) {
|
|
256
252
|
assertValidCronExpression(expression);
|
|
@@ -267,16 +263,23 @@ class CronLifecycleService {
|
|
|
267
263
|
}
|
|
268
264
|
const previousExpression = task.descriptor.expression;
|
|
269
265
|
const previousHandle = task.scheduledHandle;
|
|
266
|
+
const previousToken = task.activeScheduleToken;
|
|
267
|
+
let nextHandle;
|
|
270
268
|
task.descriptor.expression = expression;
|
|
271
269
|
try {
|
|
272
|
-
|
|
273
|
-
task.scheduledHandle = nextHandle;
|
|
270
|
+
nextHandle = this.createScheduledHandle(task);
|
|
274
271
|
if (previousHandle) {
|
|
275
|
-
|
|
272
|
+
previousHandle.stop();
|
|
276
273
|
}
|
|
274
|
+
task.scheduledHandle = nextHandle;
|
|
275
|
+
task.activeScheduleToken = nextHandle.token;
|
|
277
276
|
} catch (error) {
|
|
277
|
+
if (nextHandle) {
|
|
278
|
+
this.stopScheduledHandle(nextHandle);
|
|
279
|
+
}
|
|
278
280
|
task.descriptor.expression = previousExpression;
|
|
279
281
|
task.scheduledHandle = previousHandle;
|
|
282
|
+
task.activeScheduleToken = previousToken;
|
|
280
283
|
throw error;
|
|
281
284
|
}
|
|
282
285
|
}
|
|
@@ -286,6 +289,7 @@ class CronLifecycleService {
|
|
|
286
289
|
*
|
|
287
290
|
* @param name Name of the interval task to update.
|
|
288
291
|
* @param ms New positive interval in milliseconds.
|
|
292
|
+
* @throws When validation, replacement scheduling, or previous-handle shutdown fails.
|
|
289
293
|
*/
|
|
290
294
|
updateIntervalMs(name, ms) {
|
|
291
295
|
assertValidMs(ms, 'scheduling registry');
|
|
@@ -302,16 +306,23 @@ class CronLifecycleService {
|
|
|
302
306
|
}
|
|
303
307
|
const previousMs = task.descriptor.ms;
|
|
304
308
|
const previousHandle = task.scheduledHandle;
|
|
309
|
+
const previousToken = task.activeScheduleToken;
|
|
310
|
+
let nextHandle;
|
|
305
311
|
task.descriptor.ms = ms;
|
|
306
312
|
try {
|
|
307
|
-
|
|
308
|
-
task.scheduledHandle = nextHandle;
|
|
313
|
+
nextHandle = this.createScheduledHandle(task);
|
|
309
314
|
if (previousHandle) {
|
|
310
|
-
|
|
315
|
+
previousHandle.stop();
|
|
311
316
|
}
|
|
317
|
+
task.scheduledHandle = nextHandle;
|
|
318
|
+
task.activeScheduleToken = nextHandle.token;
|
|
312
319
|
} catch (error) {
|
|
320
|
+
if (nextHandle) {
|
|
321
|
+
this.stopScheduledHandle(nextHandle);
|
|
322
|
+
}
|
|
313
323
|
task.descriptor.ms = previousMs;
|
|
314
324
|
task.scheduledHandle = previousHandle;
|
|
325
|
+
task.activeScheduleToken = previousToken;
|
|
315
326
|
throw error;
|
|
316
327
|
}
|
|
317
328
|
}
|
|
@@ -325,7 +336,7 @@ class CronLifecycleService {
|
|
|
325
336
|
this.lifecycleState = 'ready';
|
|
326
337
|
} catch (error) {
|
|
327
338
|
this.lifecycleState = 'failed';
|
|
328
|
-
this.handleStartupFailure();
|
|
339
|
+
await this.handleStartupFailure();
|
|
329
340
|
throw error;
|
|
330
341
|
}
|
|
331
342
|
}
|
|
@@ -362,7 +373,7 @@ class CronLifecycleService {
|
|
|
362
373
|
});
|
|
363
374
|
}
|
|
364
375
|
toSchedulingTaskDescriptor(task) {
|
|
365
|
-
return {
|
|
376
|
+
return Object.freeze({
|
|
366
377
|
distributed: task.descriptor.distributed,
|
|
367
378
|
enabled: task.enabled,
|
|
368
379
|
expression: task.descriptor.expression,
|
|
@@ -376,14 +387,16 @@ class CronLifecycleService {
|
|
|
376
387
|
source: task.source,
|
|
377
388
|
targetName: task.descriptor.targetName,
|
|
378
389
|
timezone: task.descriptor.timezone
|
|
379
|
-
};
|
|
390
|
+
});
|
|
380
391
|
}
|
|
381
392
|
async shutdown() {
|
|
382
393
|
if (this.shutdownPromise) {
|
|
383
394
|
await this.shutdownPromise;
|
|
395
|
+
this.stopAllScheduledTasks();
|
|
384
396
|
await this.retryReleasedDistributedLocksAfterShutdown();
|
|
385
397
|
return;
|
|
386
398
|
}
|
|
399
|
+
this.shutdownDeadlineMs = Date.now() + this.options.shutdown.timeoutMs;
|
|
387
400
|
this.shutdownPromise = this.runShutdownLifecycle();
|
|
388
401
|
await this.shutdownPromise;
|
|
389
402
|
}
|
|
@@ -391,11 +404,11 @@ class CronLifecycleService {
|
|
|
391
404
|
if (this.lifecycleState !== 'stopped' || this.activeTasks.size > 0) {
|
|
392
405
|
return;
|
|
393
406
|
}
|
|
394
|
-
await this.distributedLocks.releaseOwnedLocks();
|
|
407
|
+
await this.distributedLocks.releaseOwnedLocks(new Set(), this.getRemainingShutdownTimeoutMs());
|
|
395
408
|
}
|
|
396
409
|
async startLifecycle() {
|
|
397
|
-
await this.distributedLocks.resolveClient();
|
|
398
410
|
this.validateDistributedLockConfiguration();
|
|
411
|
+
await this.distributedLocks.resolveClient();
|
|
399
412
|
this.registerDecoratorTasks();
|
|
400
413
|
this.started = true;
|
|
401
414
|
this.scheduleEnabledTasks();
|
|
@@ -406,11 +419,30 @@ class CronLifecycleService {
|
|
|
406
419
|
}
|
|
407
420
|
assertValidLockTtlMs(this.options.distributed.lockTtlMs);
|
|
408
421
|
}
|
|
409
|
-
handleStartupFailure() {
|
|
422
|
+
async handleStartupFailure() {
|
|
410
423
|
this.started = false;
|
|
411
424
|
this.stopAllScheduledTasks();
|
|
425
|
+
const startupRollbackTimedOut = await this.waitForActiveTasks();
|
|
426
|
+
if (startupRollbackTimedOut) {
|
|
427
|
+
this.logger.warn(`Cron startup rollback timed out after ${String(this.options.shutdown.timeoutMs)}ms with ${String(this.activeTasks.size)} active task(s) still pending.`, 'CronLifecycleService');
|
|
428
|
+
}
|
|
429
|
+
await this.distributedLocks.releaseOwnedLocks(startupRollbackTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
|
|
412
430
|
this.tasks.clear();
|
|
413
|
-
this.
|
|
431
|
+
if (this.activeTasks.size > 0) {
|
|
432
|
+
void this.completeStartupFailureCleanupAfterActiveTasks();
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
this.resetDistributedLocksAfterStartupFailure();
|
|
436
|
+
}
|
|
437
|
+
async completeStartupFailureCleanupAfterActiveTasks() {
|
|
438
|
+
await this.drainActiveTasks();
|
|
439
|
+
await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
|
|
440
|
+
this.resetDistributedLocksAfterStartupFailure();
|
|
441
|
+
}
|
|
442
|
+
resetDistributedLocksAfterStartupFailure() {
|
|
443
|
+
if (this.distributedLocks.ownedLocks === 0) {
|
|
444
|
+
this.distributedLocks.reset();
|
|
445
|
+
}
|
|
414
446
|
}
|
|
415
447
|
async runShutdownLifecycle() {
|
|
416
448
|
this.lifecycleState = 'stopping';
|
|
@@ -420,11 +452,14 @@ class CronLifecycleService {
|
|
|
420
452
|
if (shutdownTimedOut) {
|
|
421
453
|
this.logger.warn(`Cron shutdown timed out after ${String(this.options.shutdown.timeoutMs)}ms with ${String(this.activeTasks.size)} active task(s) still pending.`, 'CronLifecycleService');
|
|
422
454
|
}
|
|
423
|
-
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set());
|
|
455
|
+
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.getRemainingShutdownTimeoutMs());
|
|
424
456
|
this.lifecycleState = 'stopped';
|
|
425
457
|
}
|
|
458
|
+
getRemainingShutdownTimeoutMs() {
|
|
459
|
+
return this.shutdownDeadlineMs === undefined ? this.options.shutdown.timeoutMs : getRemainingTimeoutMs(this.shutdownDeadlineMs);
|
|
460
|
+
}
|
|
426
461
|
getRunningDistributedLockKeys() {
|
|
427
|
-
return new Set(this.
|
|
462
|
+
return new Set(this.runningDistributedLeaseCounts.keys());
|
|
428
463
|
}
|
|
429
464
|
registerDecoratorTasks() {
|
|
430
465
|
const descriptors = discoverCronTaskDescriptors(this.compiledModules, this.options, this.logger);
|
|
@@ -433,11 +468,13 @@ class CronLifecycleService {
|
|
|
433
468
|
}
|
|
434
469
|
}
|
|
435
470
|
registerTask(descriptor, source) {
|
|
471
|
+
assertValidSchedulingTaskName(descriptor.taskName);
|
|
436
472
|
this.assertTaskNameAvailable(descriptor.taskName);
|
|
437
|
-
if (descriptor.distributed) {
|
|
473
|
+
if (this.options.distributed.enabled && descriptor.distributed) {
|
|
438
474
|
assertValidLockTtlMs(descriptor.lockTtlMs);
|
|
439
475
|
}
|
|
440
476
|
const task = {
|
|
477
|
+
activeScheduleToken: undefined,
|
|
441
478
|
descriptor,
|
|
442
479
|
enabled: true,
|
|
443
480
|
running: false,
|
|
@@ -445,7 +482,7 @@ class CronLifecycleService {
|
|
|
445
482
|
source
|
|
446
483
|
};
|
|
447
484
|
if (this.started) {
|
|
448
|
-
|
|
485
|
+
this.scheduleTask(task);
|
|
449
486
|
}
|
|
450
487
|
this.tasks.set(descriptor.taskName, task);
|
|
451
488
|
}
|
|
@@ -465,22 +502,36 @@ class CronLifecycleService {
|
|
|
465
502
|
if (!task.enabled || task.scheduledHandle) {
|
|
466
503
|
return;
|
|
467
504
|
}
|
|
468
|
-
|
|
505
|
+
const previousToken = task.activeScheduleToken;
|
|
506
|
+
const nextToken = {};
|
|
507
|
+
task.activeScheduleToken = nextToken;
|
|
508
|
+
try {
|
|
509
|
+
task.scheduledHandle = this.createScheduledHandle(task, nextToken);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
task.activeScheduleToken = previousToken;
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
469
514
|
}
|
|
470
|
-
createScheduledHandle(task) {
|
|
515
|
+
createScheduledHandle(task, token = {}) {
|
|
471
516
|
const taskName = task.descriptor.taskName;
|
|
472
517
|
if (task.descriptor.kind === 'cron') {
|
|
473
518
|
const expression = task.descriptor.expression;
|
|
474
519
|
if (!expression) {
|
|
475
520
|
throw new Error(`Cron task "${taskName}" is missing a cron expression.`);
|
|
476
521
|
}
|
|
477
|
-
|
|
522
|
+
const scheduledHandle = this.options.scheduler(expression, {
|
|
478
523
|
name: taskName,
|
|
479
524
|
protect: true,
|
|
480
525
|
timezone: task.descriptor.timezone
|
|
481
526
|
}, async () => {
|
|
482
|
-
await this.handleTaskTick(taskName);
|
|
527
|
+
await this.handleTaskTick(taskName, token);
|
|
483
528
|
});
|
|
529
|
+
return {
|
|
530
|
+
stop: () => {
|
|
531
|
+
scheduledHandle.stop();
|
|
532
|
+
},
|
|
533
|
+
token
|
|
534
|
+
};
|
|
484
535
|
}
|
|
485
536
|
const ms = task.descriptor.ms;
|
|
486
537
|
if (!ms) {
|
|
@@ -488,54 +539,63 @@ class CronLifecycleService {
|
|
|
488
539
|
}
|
|
489
540
|
if (task.descriptor.kind === 'interval') {
|
|
490
541
|
const timer = setInterval(() => {
|
|
491
|
-
void this.handleTaskTick(taskName);
|
|
542
|
+
void this.handleTaskTick(taskName, token);
|
|
492
543
|
}, ms);
|
|
493
544
|
return {
|
|
494
545
|
stop: () => {
|
|
495
546
|
clearInterval(timer);
|
|
496
|
-
}
|
|
547
|
+
},
|
|
548
|
+
token
|
|
497
549
|
};
|
|
498
550
|
}
|
|
499
551
|
const timer = setTimeout(() => {
|
|
500
|
-
void this.handleTaskTick(taskName).finally(() => {
|
|
501
|
-
this.completeTimeoutTask(taskName);
|
|
552
|
+
void this.handleTaskTick(taskName, token).finally(() => {
|
|
553
|
+
this.completeTimeoutTask(taskName, token);
|
|
502
554
|
});
|
|
503
555
|
}, ms);
|
|
504
556
|
return {
|
|
505
557
|
stop: () => {
|
|
506
558
|
clearTimeout(timer);
|
|
507
|
-
}
|
|
559
|
+
},
|
|
560
|
+
token
|
|
508
561
|
};
|
|
509
562
|
}
|
|
510
|
-
completeTimeoutTask(taskName) {
|
|
563
|
+
completeTimeoutTask(taskName, token) {
|
|
511
564
|
const task = this.tasks.get(taskName);
|
|
512
|
-
if (!task || task.descriptor.kind !== 'timeout') {
|
|
565
|
+
if (!task || task.descriptor.kind !== 'timeout' || task.activeScheduleToken !== token) {
|
|
513
566
|
return;
|
|
514
567
|
}
|
|
515
568
|
task.scheduledHandle = undefined;
|
|
569
|
+
task.activeScheduleToken = undefined;
|
|
516
570
|
task.enabled = false;
|
|
517
571
|
}
|
|
518
572
|
unscheduleTask(task) {
|
|
519
573
|
if (!task.scheduledHandle) {
|
|
520
|
-
return;
|
|
574
|
+
return true;
|
|
521
575
|
}
|
|
522
576
|
const scheduledHandle = task.scheduledHandle;
|
|
577
|
+
if (!this.stopScheduledHandle(scheduledHandle)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
523
580
|
task.scheduledHandle = undefined;
|
|
524
|
-
|
|
581
|
+
task.activeScheduleToken = undefined;
|
|
582
|
+
return true;
|
|
525
583
|
}
|
|
526
584
|
stopScheduledHandle(scheduledHandle) {
|
|
527
585
|
try {
|
|
528
586
|
scheduledHandle.stop();
|
|
587
|
+
return true;
|
|
529
588
|
} catch (error) {
|
|
530
589
|
this.logger.error('Failed to stop scheduled task.', error, 'CronLifecycleService');
|
|
590
|
+
return false;
|
|
531
591
|
}
|
|
532
592
|
}
|
|
533
|
-
async handleTaskTick(taskName) {
|
|
593
|
+
async handleTaskTick(taskName, token) {
|
|
534
594
|
const taskState = this.tasks.get(taskName);
|
|
535
|
-
if (!taskState
|
|
595
|
+
if (!this.started || !this.isTaskTickCurrent(taskName, token, taskState) || taskState.running) {
|
|
536
596
|
return;
|
|
537
597
|
}
|
|
538
|
-
const task = this.runTaskTick(taskState.descriptor, taskState);
|
|
598
|
+
const task = this.runTaskTick(taskState.descriptor, taskState, token);
|
|
539
599
|
taskState.running = true;
|
|
540
600
|
this.activeTasks.add(task);
|
|
541
601
|
try {
|
|
@@ -545,23 +605,30 @@ class CronLifecycleService {
|
|
|
545
605
|
this.activeTasks.delete(task);
|
|
546
606
|
}
|
|
547
607
|
}
|
|
548
|
-
|
|
608
|
+
isTaskTickCurrent(taskName, token, taskState) {
|
|
609
|
+
return this.lifecycleState !== 'stopping' && this.lifecycleState !== 'stopped' && taskState?.enabled === true && this.tasks.get(taskName) === taskState && taskState.activeScheduleToken === token;
|
|
610
|
+
}
|
|
611
|
+
async runTaskTick(descriptor, taskState, token) {
|
|
549
612
|
if (!this.shouldUseDistributedExecution(descriptor)) {
|
|
550
613
|
await this.executeTask(descriptor, taskState);
|
|
551
614
|
return;
|
|
552
615
|
}
|
|
553
|
-
await this.runDistributedTaskTick(descriptor, taskState);
|
|
616
|
+
await this.runDistributedTaskTick(descriptor, taskState, token);
|
|
554
617
|
}
|
|
555
618
|
shouldUseDistributedExecution(descriptor) {
|
|
556
619
|
return this.options.distributed.enabled && descriptor.distributed && this.distributedLocks.resolvedClient !== undefined;
|
|
557
620
|
}
|
|
558
|
-
async runDistributedTaskTick(descriptor, taskState) {
|
|
559
|
-
const
|
|
560
|
-
if (!
|
|
621
|
+
async runDistributedTaskTick(descriptor, taskState, token) {
|
|
622
|
+
const lease = await this.distributedLocks.tryAcquireLock(descriptor);
|
|
623
|
+
if (!lease) {
|
|
561
624
|
return;
|
|
562
625
|
}
|
|
563
|
-
|
|
564
|
-
|
|
626
|
+
if (this.lifecycleState !== 'failed' && !this.isTaskTickCurrent(descriptor.taskName, token, taskState)) {
|
|
627
|
+
await this.distributedLocks.releaseLock(lease, this.shutdownPromise ? this.getRemainingShutdownTimeoutMs() : undefined);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const lockRenewalMonitor = this.distributedLocks.startLockRenewalMonitor(descriptor, lease);
|
|
631
|
+
this.runningDistributedLeaseCounts.set(descriptor.lockKey, (this.runningDistributedLeaseCounts.get(descriptor.lockKey) ?? 0) + 1);
|
|
565
632
|
try {
|
|
566
633
|
await this.executeTask(descriptor, taskState, async () => {
|
|
567
634
|
lockRenewalMonitor.stop();
|
|
@@ -569,10 +636,15 @@ class CronLifecycleService {
|
|
|
569
636
|
});
|
|
570
637
|
} finally {
|
|
571
638
|
lockRenewalMonitor.stop();
|
|
572
|
-
this.
|
|
573
|
-
|
|
639
|
+
const runningLeaseCount = this.runningDistributedLeaseCounts.get(descriptor.lockKey);
|
|
640
|
+
if (runningLeaseCount === 1) {
|
|
641
|
+
this.runningDistributedLeaseCounts.delete(descriptor.lockKey);
|
|
642
|
+
} else if (runningLeaseCount !== undefined) {
|
|
643
|
+
this.runningDistributedLeaseCounts.set(descriptor.lockKey, runningLeaseCount - 1);
|
|
644
|
+
}
|
|
645
|
+
const released = await this.distributedLocks.releaseLock(lease, this.shutdownPromise ? this.getRemainingShutdownTimeoutMs() : undefined);
|
|
574
646
|
if (!released && this.lifecycleState === 'stopped') {
|
|
575
|
-
await this.distributedLocks.releaseOwnedLocks();
|
|
647
|
+
await this.distributedLocks.releaseOwnedLocks(this.getRunningDistributedLockKeys(), this.getRemainingShutdownTimeoutMs());
|
|
576
648
|
}
|
|
577
649
|
}
|
|
578
650
|
}
|
|
@@ -580,7 +652,8 @@ class CronLifecycleService {
|
|
|
580
652
|
if (this.activeTasks.size === 0) {
|
|
581
653
|
return false;
|
|
582
654
|
}
|
|
583
|
-
|
|
655
|
+
const timeoutMs = this.getRemainingShutdownTimeoutMs();
|
|
656
|
+
if (timeoutMs === 0) {
|
|
584
657
|
return true;
|
|
585
658
|
}
|
|
586
659
|
let timeoutHandle;
|
|
@@ -588,7 +661,7 @@ class CronLifecycleService {
|
|
|
588
661
|
return await Promise.race([this.drainActiveTasks().then(() => false), new Promise(resolve => {
|
|
589
662
|
timeoutHandle = setTimeout(() => {
|
|
590
663
|
resolve(true);
|
|
591
|
-
},
|
|
664
|
+
}, timeoutMs);
|
|
592
665
|
})]);
|
|
593
666
|
} finally {
|
|
594
667
|
if (timeoutHandle) {
|
|
@@ -606,6 +679,7 @@ class CronLifecycleService {
|
|
|
606
679
|
if (descriptor.kind === 'timeout') {
|
|
607
680
|
taskState.enabled = false;
|
|
608
681
|
taskState.scheduledHandle = undefined;
|
|
682
|
+
taskState.activeScheduleToken = undefined;
|
|
609
683
|
}
|
|
610
684
|
}
|
|
611
685
|
stopAllScheduledTasks() {
|
package/dist/task-discovery.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MetadataPropertyKey } from '@fluojs/core';
|
|
2
2
|
import type { ApplicationLogger, CompiledModule } from '@fluojs/runtime';
|
|
3
3
|
import type { CronTaskDescriptor, NormalizedCronModuleOptions } from './types.js';
|
|
4
4
|
/**
|
|
@@ -17,6 +17,20 @@ export declare function buildDefaultTaskName(targetName: string, methodName: str
|
|
|
17
17
|
* @returns The create lock key result.
|
|
18
18
|
*/
|
|
19
19
|
export declare function createLockKey(prefix: string, taskName: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Asserts that a scheduling task name can be used as a registry key.
|
|
22
|
+
*
|
|
23
|
+
* @param name Scheduling task name supplied by a decorator or registry call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertValidSchedulingTaskName(name: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the effective scheduling task name while preserving authored names.
|
|
28
|
+
*
|
|
29
|
+
* @param defaultName Name derived from the decorated target or registry argument.
|
|
30
|
+
* @param optionName Optional name override supplied in scheduling options.
|
|
31
|
+
* @returns The effective task name used by the scheduler registry.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveSchedulingTaskName(defaultName: string, optionName?: string): string;
|
|
20
34
|
/**
|
|
21
35
|
* Method key to name.
|
|
22
36
|
*
|