@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/dist/module.js CHANGED
@@ -1,14 +1,36 @@
1
1
  import { defineModule } from '@fluojs/runtime';
2
- import { CronLifecycleService } from './service.js';
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 randomId() {
7
- const randomUUID = globalThis.crypto?.randomUUID;
8
- if (randomUUID) {
9
- return randomUUID.call(globalThis.crypto);
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 `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
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: randomId()
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: randomId()
51
+ ownerId: createCronRandomId()
30
52
  };
31
53
  }
32
- return {
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 ?? randomId()
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,3 @@
1
+ /** Creates a platform-neutral random identifier for Cron ownership boundaries. */
2
+ export declare function createCronRandomId(): string;
3
+ //# sourceMappingURL=random-id.d.ts.map
@@ -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 runningDistributedLockKeys;
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 after the operation.
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;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
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
- assertValidTaskName(name);
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
- runningDistributedLockKeys = new Set();
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 after the operation.
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
- const nextHandle = this.createScheduledHandle(task);
273
- task.scheduledHandle = nextHandle;
270
+ nextHandle = this.createScheduledHandle(task);
274
271
  if (previousHandle) {
275
- this.stopScheduledHandle(previousHandle);
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
- const nextHandle = this.createScheduledHandle(task);
308
- task.scheduledHandle = nextHandle;
313
+ nextHandle = this.createScheduledHandle(task);
309
314
  if (previousHandle) {
310
- this.stopScheduledHandle(previousHandle);
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.distributedLocks.reset();
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.runningDistributedLockKeys);
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
- task.scheduledHandle = this.createScheduledHandle(task);
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
- task.scheduledHandle = this.createScheduledHandle(task);
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
- return this.options.scheduler(expression, {
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
- this.stopScheduledHandle(scheduledHandle);
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?.enabled || taskState.running) {
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
- async runTaskTick(descriptor, taskState) {
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 lockAcquired = await this.distributedLocks.tryAcquireLock(descriptor);
560
- if (!lockAcquired) {
621
+ async runDistributedTaskTick(descriptor, taskState, token) {
622
+ const lease = await this.distributedLocks.tryAcquireLock(descriptor);
623
+ if (!lease) {
561
624
  return;
562
625
  }
563
- const lockRenewalMonitor = this.distributedLocks.startLockRenewalMonitor(descriptor);
564
- this.runningDistributedLockKeys.add(descriptor.lockKey);
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.runningDistributedLockKeys.delete(descriptor.lockKey);
573
- const released = await this.distributedLocks.releaseLock(descriptor);
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
- if (this.options.shutdown.timeoutMs === 0) {
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
- }, this.options.shutdown.timeoutMs);
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() {
@@ -1,4 +1,4 @@
1
- import { type MetadataPropertyKey } from '@fluojs/core';
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
  *