@fluojs/cron 1.1.0 → 2.0.1

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 CHANGED
@@ -10,6 +10,7 @@ fluo 애플리케이션을 위한 데코레이터 기반 스케줄링 패키지
10
10
  - [사용 시점](#사용-시점)
11
11
  - [빠른 시작](#빠른-시작)
12
12
  - [공통 패턴](#공통-패턴)
13
+ - [NestJS Cron 옵션 마이그레이션](#nestjs-cron-옵션-마이그레이션)
13
14
  - [분산 락 사용하기](#분산-락-사용하기)
14
15
  - [동적 스케줄링](#동적-스케줄링)
15
16
  - [제한된 종료](#제한된-종료)
@@ -20,10 +21,10 @@ fluo 애플리케이션을 위한 데코레이터 기반 스케줄링 패키지
20
21
  ## 설치
21
22
 
22
23
  ```bash
23
- npm install @fluojs/cron croner
24
+ npm install @fluojs/cron
24
25
  ```
25
26
 
26
- `croner`는 `@fluojs/cron`이 사용하는 scheduler engine입니다. 애플리케이션과 배포 감사에서 runtime scheduler dependency ownership이 명확히 보이도록 패키지와 함께 설치하세요.
27
+ `@fluojs/cron`이 `croner`를 runtime dependency로 소유하므로 소비자가 scheduler engine을 직접 설치할 필요가 없습니다.
27
28
 
28
29
  `@fluojs/redis`는 Redis distributed locking을 활성화할 때만 필요합니다. Non-distributed scheduling 경로는 package import, module registration, bootstrap, status snapshot 생성 중 Redis integration을 로드하지 않습니다.
29
30
 
@@ -41,7 +42,7 @@ npm install @fluojs/cron croner
41
42
  애플리케이션 모듈의 스케줄링 등록은 `CronModule.forRoot(...)`로 구성합니다.
42
43
  Cron 표현식은 다섯 필드(`minute hour day month weekday`) 또는 여섯 필드(`second minute hour day month weekday`)를 사용할 수 있습니다. 내장 `CronExpression` preset은 sub-minute 정밀도가 필요할 때 여섯 필드 표현식을 사용합니다. Cron task는 application bootstrap 이후에만 시작되고, 이미 시작된 registry에 동적으로 등록한 cron task는 등록 시점에 시작되며, fluo는 `timezone`과 no-overlap 보호를 scheduler에 전달해 같은 task instance가 자기 자신과 겹쳐 실행되지 않게 합니다.
43
44
 
44
- Scheduling decorator는 public instance method에만 적용됩니다. NestJS에서 사용하던 private scheduled method, static helper, legacy decorator metadata 가정 뒤에 숨은 method name을 그대로 옮기지 마세요. 공개 provider/controller method를 노출하고 private 구현 세부사항은 그 method 뒤에 두세요.
45
+ Scheduling decorator는 public instance method에만 적용됩니다. NestJS에서 사용하던 private scheduled method, static helper, legacy decorator metadata 가정 뒤에 숨은 method name을 그대로 옮기지 마세요. 공개 provider/controller method를 노출하고 private 구현 세부사항은 그 method 뒤에 두세요. 명시적인 decorator `name` 값은 non-empty string이어야 하며, dynamic registry validation contract와 동일하게 검증됩니다.
45
46
 
46
47
  ```typescript
47
48
  import { Module } from '@fluojs/core';
@@ -73,6 +74,22 @@ class AppModule {}
73
74
 
74
75
  ## 공통 패턴
75
76
 
77
+ ### NestJS Cron 옵션 마이그레이션
78
+
79
+ NestJS `@Cron()` 옵션은 `CronTaskOptions`에 그대로 전달할 수 없습니다. NestJS `timeZone`을 fluo `timezone`으로 바꾸세요:
80
+
81
+ ```typescript
82
+ // NestJS
83
+ @Cron('0 9 * * *', { timeZone: 'Asia/Seoul', waitForCompletion: true })
84
+
85
+ // fluo
86
+ @Cron('0 9 * * *', { timezone: 'Asia/Seoul' })
87
+ ```
88
+
89
+ `waitForCompletion`을 복사하거나 overlap flag를 만들지 마세요. fluo는 두 옵션을 모두 노출하지 않으며, 모든 cron task에 scheduler-level no-overlap protection과 in-process running guard를 함께 적용합니다. 같은 task instance가 아직 실행 중일 때 다음 tick이 도착하면 fluo는 새 실행을 queue하지 않고 해당 tick을 건너뜁니다. 따라서 NestJS에서 `waitForCompletion: true`였던 task는 마이그레이션할 때 이 옵션을 생략합니다. NestJS task가 `waitForCompletion`을 생략하거나 `false`로 설정해 의도적으로 overlapping run에 의존했다면 fluo에서 overlap을 활성화할 수 있다고 가정하지 말고 application-owned queue 또는 worker 뒤로 작업을 재설계하세요.
90
+
91
+ 이 guard는 한 application process 안의 같은 task instance만 보호합니다. 여러 application instance가 같은 task를 동시에 실행하지 않아야 한다면 [분산 락 사용하기](#분산-락-사용하기)를 적용하세요.
92
+
76
93
  ### 분산 락 사용하기
77
94
 
78
95
  여러 서버 인스턴스에서 스케줄링된 작업이 동시에 실행되는 것을 방지하려면 분산 모드를 활성화하세요. 이 기능은 `@fluojs/redis`가 필요하며, Redis peer는 `distributed.enabled`가 `true`일 때만 로드되고 resolve됩니다.
@@ -97,11 +114,11 @@ import { RedisModule } from '@fluojs/redis';
97
114
  class AppModule {}
98
115
  ```
99
116
 
100
- `distributed.clientName`을 생략하면 위의 기본 Redis 등록을 계속 사용합니다. 분산 락에 기본 Redis가 아닌 다른 연결을 쓰려면 `RedisModule.forRoot({ name, ... })`로 등록한 이름을 `distributed.clientName`에 지정하세요.
117
+ `distributed.clientName`을 생략하면 위의 기본 Redis 등록을 계속 사용합니다. 분산 락에 기본 Redis가 아닌 다른 연결을 쓰려면 `RedisModule.forRoot({ name, ... })`로 등록한 이름을 `distributed.clientName`에 지정하세요. fluo는 module option normalization 중 configured client name을 trim하고, lifecycle 또는 status reporting이 Redis dependency name을 사용하기 전에 blank 값을 거부합니다.
101
118
 
102
- `distributed.lockTtlMs`는 `1_000ms` 이상이어야 합니다. fluo는 최소 지원 경계인 `1_000ms`를 포함해 TTL이 만료되기 전에 Redis 락을 갱신합니다.
119
+ `distributed.lockTtlMs`는 `1_000ms` 이상이어야 합니다. Distributed locking이 활성화된 경우 fluo는 Redis를 load, resolve, probe하기 전에 module option normalization 중 module-level TTL을 검증합니다. Task-level `lockTtlMs` override는 module distributed mode와 해당 task의 distributed locking이 모두 활성화된 경우에만 검증됩니다. Module 또는 task locking이 비활성화되어 있으면 사용되지 않는 TTL이 distributed 최소값보다 낮다는 이유만으로 실패하지 않습니다. fluo는 활성 TTL이 만료되기 전에 Redis 락을 갱신하며, 최소 지원 경계인 `1_000ms`도 포함됩니다.
103
120
 
104
- 각 scheduler instance는 platform-neutral 기본 `distributed.ownerId`를 사용합니다. 배포 환경에 더 강한 stable-owner 규칙이 있을 때만 `distributed.ownerId`를 명시적으로 지정하세요. Lock release는 task 실행 뒤 `finally` 경로에서 수행됩니다. Redis release가 실패하면 fluo는 status snapshot의 local ownership을 유지하고 shutdown 중 다시 release를 시도합니다. Redis가 다른 owner의 key라고 응답하면 fencing이 이미 다른 곳으로 이동한 것이므로 local ownership을 정리합니다. Redis TTL과 renewal timing은 drift 영향을 받는 coordination primitive이지 강한 fencing token 자체는 아니므로, stale work가 위험한 long-running job은 idempotent하게 작성하고 application-level fencing을 함께 사용해야 합니다.
121
+ 각 scheduler instance는 platform-neutral 기본 `distributed.ownerId`를 사용합니다. 배포 환경에 더 강한 stable-owner 규칙이 있을 때만 `distributed.ownerId`를 명시적으로 지정하세요. `distributed.ownerId`를 제공한 경우 fluo는 module option normalization 중에 값을 trim하고, scheduler 또는 Redis lifecycle setup 전에 blank 또는 non-string 값을 거부합니다. 따라서 유효하지 않거나 빈 owner 식별자가 Redis lock ownership 상태로 들어갈 수 없습니다. Lock release는 task 실행 뒤 `finally` 경로에서 수행됩니다. Distributed tick이 이미 실행 중인 상태에서 bootstrap이 나중에 실패하면 startup rollback은 해당 active task가 drain되어 락을 release할 수 있을 때까지 Redis lock client를 유지합니다. Redis release가 실패하면 fluo는 status snapshot의 local ownership을 유지하고 shutdown 중 다시 release를 시도합니다. Redis가 다른 owner의 key라고 응답하면 fencing이 이미 다른 곳으로 이동한 것이므로 local ownership을 정리합니다. Redis TTL과 renewal timing은 drift 영향을 받는 coordination primitive이지 강한 fencing token 자체는 아니므로, stale work가 위험한 long-running job은 idempotent하게 작성하고 application-level fencing을 함께 사용해야 합니다.
105
122
 
106
123
  ```typescript
107
124
  @Module({
@@ -149,15 +166,15 @@ class TaskManager {
149
166
  }
150
167
  ```
151
168
 
152
- Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, `updateIntervalMs`를 제공합니다. 첫 번째 `name` 인자는 기본 registry key이며, `options.name`을 전달하면 dynamic task의 실제 registry key, scheduler metadata name, 기본 distributed lock key가 이를 사용해 decorator naming semantics와 일치합니다. `get`과 `getAll`은 live `CronJob` handle 아니라 read-only `SchedulingTaskDescriptor` 값을 반환합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
169
+ Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, `updateIntervalMs`를 제공합니다. 첫 번째 `name` 인자는 기본 registry key이며, `options.name`을 전달하면 dynamic task의 실제 registry key, scheduler metadata name, 기본 distributed lock key가 이를 사용해 decorator naming semantics와 일치합니다. Registry, decorator, dynamic `options.name` task name은 non-empty string이어야 합니다. Blank dynamic override name은 scheduler 또는 registry state를 남기기 전에 거부됩니다. `get`과 `getAll`은 live `CronJob` handle이나 mutable registry state가 아니라 immutable `SchedulingTaskDescriptor` snapshot을 반환합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
153
170
 
154
- Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. Rescheduling실패하면 이전 expression 또는 interval milliseconds와 scheduled handle 그대로 유지됩니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
171
+ Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. 이전 scheduled handle의 stop성공해야만 replacement를 commit합니다. Replacement scheduling이 실패하거나 이전 handle을 stop할 수 없으면 fluo는 provisional replacement를 stop하고 이전 expression 또는 interval milliseconds와 handle 복원한 failure를 다시 throw하므로 duplicate schedule을 조용히 남기지 않습니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
155
172
 
156
173
  ### 제한된 종료
157
174
 
158
175
  `CronModule`은 애플리케이션 종료 시 실행 중인 작업을 제한된 타임아웃 안에서 drain합니다. 따라서 하나의 hung task 때문에 프로세스 종료가 영원히 막히지 않습니다.
159
176
 
160
- 기본적으로 shutdown drain은 최대 `10_000ms` 동안 기다립니다. 이 시간이 지나면 스케줄러는 경고 로그를 남기고 hung task가 끝나기를 더 기다리지 않은 채 종료를 계속합니다. 분산 락을 사용하는 경우 아직 실행 중인 작업이 보유한 락은 timeout 시점에 즉시 해제하지 않습니다. 해당 작업이 정상적으로 끝날 때까지 락 소유권을 유지하거나, 프로세스가 종료된 뒤 Redis TTL로 만료되게 두어 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
177
+ 기본적으로 shutdown drain은 최대 `10_000ms` 동안 기다립니다. 이 시간이 지나면 스케줄러는 경고 로그를 남기고 hung task가 끝나기를 더 기다리지 않은 채 종료를 계속합니다. 같은 `shutdown.timeoutMs` 경계는 shutdown 중 Redis owned-lock release I/O에도 적용되므로, 멈춘 Redis release가 process termination을 무기한 막지 못합니다. 분산 락을 사용하는 경우 아직 실행 중인 작업이 보유한 락은 timeout 시점에 즉시 해제하지 않습니다. 해당 작업이 정상적으로 끝날 때까지 락 소유권을 유지하거나, 프로세스가 종료된 뒤 Redis TTL로 만료되게 둡니다. fluo는 lock renewal timer에 `unref()`를 호출하므로 다른 작업이 Node.js event loop를 활성 상태로 유지하는 동안에는 갱신을 계속하지만 timer 자체만으로 process를 유지하지 않으며, task가 settle되면 timer를 clear합니다. Release I/O 자체가 timeout되면 fluo는 Redis가 release를 확인하거나 다른 owner가 key를 보유한다고 응답할 때까지 local owned-lock visibility/status를 보존하고 ownership을 지우지 않습니다. 이렇게 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
161
178
 
162
179
  ```typescript
163
180
  @Module({
package/README.md CHANGED
@@ -10,6 +10,7 @@ Decorator-based scheduling for fluo applications with lifecycle-managed startup/
10
10
  - [When to Use](#when-to-use)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Common Patterns](#common-patterns)
13
+ - [Migrating NestJS Cron Options](#migrating-nestjs-cron-options)
13
14
  - [Distributed Locking](#distributed-locking)
14
15
  - [Dynamic Scheduling](#dynamic-scheduling)
15
16
  - [Bounded Shutdown](#bounded-shutdown)
@@ -20,10 +21,10 @@ Decorator-based scheduling for fluo applications with lifecycle-managed startup/
20
21
  ## Installation
21
22
 
22
23
  ```bash
23
- npm install @fluojs/cron croner
24
+ npm install @fluojs/cron
24
25
  ```
25
26
 
26
- `croner` is the scheduler engine used by `@fluojs/cron`. Install it alongside the package so lockfiles make the runtime scheduler dependency explicit for applications and deployment audits.
27
+ `@fluojs/cron` owns `croner` as a runtime dependency, so consumers do not need to install the scheduler engine directly.
27
28
 
28
29
  `@fluojs/redis` is needed only when Redis distributed locking is enabled. Non-distributed scheduling paths do not load the Redis integration during package import, module registration, bootstrap, or status snapshot creation.
29
30
 
@@ -41,7 +42,7 @@ Register the `CronModule` and use decorators to schedule your methods.
41
42
  Use `CronModule.forRoot(...)` to register scheduling for an application module.
42
43
  Cron expressions may use either five fields (`minute hour day month weekday`) or six fields (`second minute hour day month weekday`). The built-in `CronExpression` presets use six-field expressions when sub-minute precision is needed. Cron tasks start only after application bootstrap, dynamically registered cron tasks start when added to a started registry, and fluo forwards `timezone` plus no-overlap protection to the scheduler so one task instance does not overlap itself.
43
44
 
44
- Scheduling decorators apply to public instance methods only. Do not migrate NestJS private scheduled methods, static helpers, or method names that are hidden behind legacy decorator metadata assumptions as-is; expose a public provider/controller method and keep any private implementation details behind that method.
45
+ Scheduling decorators apply to public instance methods only. Do not migrate NestJS private scheduled methods, static helpers, or method names that are hidden behind legacy decorator metadata assumptions as-is; expose a public provider/controller method and keep any private implementation details behind that method. Explicit decorator `name` values must be non-empty strings, matching the dynamic registry validation contract.
45
46
 
46
47
  ```typescript
47
48
  import { Module } from '@fluojs/core';
@@ -73,6 +74,22 @@ class AppModule {}
73
74
 
74
75
  ## Common Patterns
75
76
 
77
+ ### Migrating NestJS Cron Options
78
+
79
+ NestJS `@Cron()` options are not a drop-in `CronTaskOptions` object. Rename NestJS `timeZone` to fluo `timezone`:
80
+
81
+ ```typescript
82
+ // NestJS
83
+ @Cron('0 9 * * *', { timeZone: 'Asia/Seoul', waitForCompletion: true })
84
+
85
+ // fluo
86
+ @Cron('0 9 * * *', { timezone: 'Asia/Seoul' })
87
+ ```
88
+
89
+ Do not copy `waitForCompletion` or invent an overlap flag. fluo does not expose either option: every cron task uses scheduler-level no-overlap protection plus an in-process running guard. If another tick arrives while the same task instance is still running, fluo skips that tick instead of queueing another run. A NestJS task with `waitForCompletion: true` therefore omits the option when migrated. If the NestJS task left `waitForCompletion` unset or set it to `false` and intentionally depended on overlapping runs, redesign that work behind an application-owned queue or worker rather than expecting fluo to enable overlap.
90
+
91
+ This guard covers one task instance in one application process. Use [Distributed Locking](#distributed-locking) when multiple application instances must not run the same task concurrently.
92
+
76
93
  ### Distributed Locking
77
94
 
78
95
  To prevent scheduled tasks from running concurrently across multiple server instances, enable distributed mode. This requires `@fluojs/redis`; the Redis peer is loaded and resolved only when `distributed.enabled` is `true`.
@@ -97,11 +114,11 @@ import { RedisModule } from '@fluojs/redis';
97
114
  class AppModule {}
98
115
  ```
99
116
 
100
- Leave `distributed.clientName` unset to keep using the default Redis registration above. To use a non-default Redis connection for distributed locks, set `distributed.clientName` to the name registered through `RedisModule.forRoot({ name, ... })`.
117
+ Leave `distributed.clientName` unset to keep using the default Redis registration above. To use a non-default Redis connection for distributed locks, set `distributed.clientName` to the name registered through `RedisModule.forRoot({ name, ... })`. fluo trims the configured client name during module option normalization and rejects blank values before lifecycle or status reporting uses the Redis dependency name.
101
118
 
102
- `distributed.lockTtlMs` must stay at or above `1_000ms`. fluo renews the Redis lock before that TTL expires, including the minimum supported `1_000ms` boundary.
119
+ `distributed.lockTtlMs` must stay at or above `1_000ms`. When distributed locking is enabled, fluo validates that module-level TTL during option normalization before loading, resolving, or probing Redis. Task-level `lockTtlMs` overrides are validated only when module distributed mode and that task's distributed locking are both enabled. Disabled module or task locking does not fail solely because an inactive TTL is below the distributed minimum. fluo renews the Redis lock before the active TTL expires, including the minimum supported `1_000ms` boundary.
103
120
 
104
- Each scheduler instance uses a platform-neutral default `distributed.ownerId`; set `distributed.ownerId` explicitly only when your deployment has a stronger stable-owner convention. Lock release runs in a `finally` path after task execution. If Redis release fails, fluo keeps local ownership in status snapshots and retries during shutdown; if Redis reports that another owner holds the key, local ownership is cleared because fencing has already moved elsewhere. Redis TTL and renewal timing are still drift-sensitive coordination primitives rather than hard fencing tokens, so long-running jobs should remain idempotent and use application-level fencing when stale work would be unsafe.
121
+ Each scheduler instance uses a platform-neutral default `distributed.ownerId`; set `distributed.ownerId` explicitly only when your deployment has a stronger stable-owner convention. When `distributed.ownerId` is provided, fluo trims it during module option normalization and rejects blank or non-string values before scheduler or Redis lifecycle setup, so invalid or empty owner identifiers cannot enter Redis lock ownership state. Lock release runs in a `finally` path after task execution. If bootstrap later fails while a distributed tick is already running, startup rollback keeps the Redis lock client available until that active task can drain and release its lock. If Redis release fails, fluo keeps local ownership in status snapshots and retries during shutdown; if Redis reports that another owner holds the key, local ownership is cleared because fencing has already moved elsewhere. Redis TTL and renewal timing are still drift-sensitive coordination primitives rather than hard fencing tokens, so long-running jobs should remain idempotent and use application-level fencing when stale work would be unsafe.
105
122
 
106
123
  ```typescript
107
124
  @Module({
@@ -149,15 +166,15 @@ class TaskManager {
149
166
  }
150
167
  ```
151
168
 
152
- The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, and `updateIntervalMs`. The first `name` argument is the default registry key; passing `options.name` overrides the actual registry key, scheduler metadata name, and default distributed lock key for dynamic tasks so dynamic registration matches decorator naming semantics. `get` and `getAll` return read-only `SchedulingTaskDescriptor` values, not live `CronJob` handles. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
169
+ The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`, and `updateIntervalMs`. The first `name` argument is the default registry key; passing `options.name` overrides the actual registry key, scheduler metadata name, and default distributed lock key for dynamic tasks so dynamic registration matches decorator naming semantics. Registry, decorator, and dynamic `options.name` task names must be non-empty strings; blank dynamic override names are rejected before scheduler or registry state is retained. `get` and `getAll` return immutable `SchedulingTaskDescriptor` snapshots, not live `CronJob` handles or mutable registry state. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
153
170
 
154
- Dynamic cron registration is atomic with scheduler startup: if the scheduler rejects a new cron job, the registry does not retain a half-registered task. Updating a running cron expression or interval cadence is also rollback-safe. If rescheduling fails, the previous expression or interval milliseconds and scheduled handle remain active. Cron tasks use both scheduler-level no-overlap protection and fluo's in-process running guard, so the same task instance will not run overlapping ticks.
171
+ Dynamic cron registration is atomic with scheduler startup: if the scheduler rejects a new cron job, the registry does not retain a half-registered task. Updating a running cron expression or interval cadence is also rollback-safe. A replacement is committed only after the previous scheduled handle stops successfully. If replacement scheduling fails or the previous handle cannot be stopped, fluo stops the provisional replacement, restores the previous expression or interval milliseconds and handle, and rethrows the failure instead of silently retaining duplicate schedules. Cron tasks use both scheduler-level no-overlap protection and fluo's in-process running guard, so the same task instance will not run overlapping ticks.
155
172
 
156
173
  ### Bounded Shutdown
157
174
 
158
175
  `CronModule` drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.
159
176
 
160
- By default the shutdown drain waits up to `10_000ms`. If that timeout expires, the scheduler logs a warning and continues shutdown without waiting for the hung task to settle. When distributed locking is enabled, locks held by still-running tasks are not eagerly released on timeout; they remain owned by that task until it settles normally, or until Redis expires the lock after the process exits. This prevents another node from starting the same job while the original task is still running.
177
+ By default the shutdown drain waits up to `10_000ms`. If that timeout expires, the scheduler logs a warning and continues shutdown without waiting for the hung task to settle. The same `shutdown.timeoutMs` boundary also applies to Redis owned-lock release I/O during shutdown, so a stuck Redis release cannot block process termination indefinitely. When distributed locking is enabled, locks held by still-running tasks are not eagerly released on timeout; they remain owned by that task until it settles normally, or until Redis expires the lock after the process exits. fluo calls `unref()` on lock renewal timers so they continue renewing while other work keeps the Node.js event loop active without retaining the process by themselves, and it clears them when the task settles. If release I/O itself times out, fluo preserves local owned-lock visibility/status and does not clear ownership until Redis confirms release or reports that another owner holds the key. This prevents another node from starting the same job while the original task is still running.
161
178
 
162
179
  ```typescript
163
180
  @Module({
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,eAAe,EAEf,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAGpB,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AAqCrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,CAoB3F;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,mBAAmB,CAgB3F;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,mBAAmB,CAgBzF"}
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,eAAe,EAEf,mBAAmB,EAEnB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAGpB,KAAK,yBAAyB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,2BAA2B,KAAK,IAAI,CAAC;AACjG,KAAK,mBAAmB,GAAG,yBAAyB,CAAC;AAyCrD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,mBAAmB,CAoB3F;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,mBAAmB,CAgB3F;AAED;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,mBAAmB,CAgBzF"}
@@ -26,6 +26,9 @@ function assertMethodIsPublic(context, decoratorName) {
26
26
  if (context.private) {
27
27
  throw new Error(`${decoratorName}() cannot be used on private methods.`);
28
28
  }
29
+ if (context.static) {
30
+ throw new Error(`${decoratorName}() cannot be used on static methods.`);
31
+ }
29
32
  }
30
33
 
31
34
  /**
@@ -32,7 +32,7 @@ export declare class CronDistributedLockManager {
32
32
  tryAcquireLock(descriptor: CronTaskDescriptor): Promise<boolean>;
33
33
  startLockRenewalMonitor(descriptor: CronTaskDescriptor): LockRenewalMonitor;
34
34
  releaseLock(descriptor: CronTaskDescriptor): Promise<boolean>;
35
- releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>): Promise<void>;
35
+ releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>, timeoutMs?: number): Promise<void>;
36
36
  private createLockRenewalState;
37
37
  private queueDueLockRenewalAttempts;
38
38
  private runLockRenewalAttempt;
@@ -1 +1 @@
1
- {"version":3,"file":"distributed-lock-manager.d.ts","sourceRoot":"","sources":["../src/distributed-lock-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAElF,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC7G;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC9C,IAAI,IAAI,IAAI,CAAC;CACd;AAoDD,yFAAyF;AACzF,qBAAa,0BAA0B;IAQnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,mBAAmB,CAAK;gBAGb,OAAO,EAAE,2BAA2B,EACpC,gBAAgB,EAAE,SAAS,EAC3B,MAAM,EAAE,iBAAiB;IAG5C,IAAI,cAAc,IAAI,eAAe,GAAG,SAAS,CAEhD;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,eAAe,IAAI,OAAO,CAM7B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBpC,KAAK,IAAI,IAAI;IAKP,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCtE,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,GAAG,kBAAkB;IA8BrE,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,iBAAiB,CAAC,gBAAgB,GAAE,WAAW,CAAC,MAAM,CAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBzF,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,2BAA2B;YAcrB,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;YAYZ,SAAS;YA2CT,cAAc;YAsCd,wBAAwB;IAmBtC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;CAG9B"}
1
+ {"version":3,"file":"distributed-lock-manager.d.ts","sourceRoot":"","sources":["../src/distributed-lock-manager.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAElF,yEAAyE;AACzE,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,WAAW,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACrF,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CAC7G;AAED,mEAAmE;AACnE,MAAM,WAAW,kBAAkB;IACjC,eAAe,IAAI,OAAO,CAAC,KAAK,GAAG,SAAS,CAAC,CAAC;IAC9C,IAAI,IAAI,IAAI,CAAC;CACd;AA+ED,yFAAyF;AACzF,qBAAa,0BAA0B;IAQnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IATzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,mBAAmB,CAAK;gBAGb,OAAO,EAAE,2BAA2B,EACpC,gBAAgB,EAAE,SAAS,EAC3B,MAAM,EAAE,iBAAiB;IAG5C,IAAI,cAAc,IAAI,eAAe,GAAG,SAAS,CAEhD;IAED,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,IAAI,eAAe,IAAI,OAAO,CAM7B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAsBpC,KAAK,IAAI,IAAI;IAKP,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAkCtE,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,GAAG,kBAAkB;IA8BrE,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAI7D,iBAAiB,CAAC,gBAAgB,GAAE,WAAW,CAAC,MAAM,CAAa,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7G,OAAO,CAAC,sBAAsB;IAY9B,OAAO,CAAC,2BAA2B;YAcrB,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;YAYZ,SAAS;YA2CT,cAAc;YAyCd,wBAAwB;IAmBtC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;CAG9B"}
@@ -16,6 +16,26 @@ function isMissingRedisPeer(error) {
16
16
  function createRedisBootstrapError() {
17
17
  return new Error(['Cron distributed mode requires @fluojs/redis to be installed and registered.', 'Install and import @fluojs/redis, or disable distributed locking with distributed.enabled: false.'].join(' '));
18
18
  }
19
+ function createLockReleaseTimeoutError(timeoutMs) {
20
+ return new Error(`Distributed cron lock release timed out after ${String(timeoutMs)}ms.`);
21
+ }
22
+ async function withTimeout(operation, timeoutMs) {
23
+ if (timeoutMs === undefined) {
24
+ return await operation;
25
+ }
26
+ let timeoutHandle;
27
+ try {
28
+ return await Promise.race([operation, new Promise((_resolve, reject) => {
29
+ timeoutHandle = setTimeout(() => {
30
+ reject(createLockReleaseTimeoutError(timeoutMs));
31
+ }, timeoutMs);
32
+ })]);
33
+ } finally {
34
+ if (timeoutHandle) {
35
+ clearTimeout(timeoutHandle);
36
+ }
37
+ }
38
+ }
19
39
  async function resolveRedisPeerModule() {
20
40
  try {
21
41
  return await loadRedisPeerModule();
@@ -107,7 +127,7 @@ export class CronDistributedLockManager {
107
127
  renewalState.renewalChain = renewalState.renewalChain.then(async () => {
108
128
  await this.runLockRenewalAttempt(descriptor, renewalState);
109
129
  });
110
- }, renewalState.renewalIntervalMs);
130
+ }, renewalState.renewalIntervalMs).unref();
111
131
  return {
112
132
  getPostRunError: async () => {
113
133
  this.queueDueLockRenewalAttempts(descriptor, renewalState);
@@ -126,7 +146,7 @@ export class CronDistributedLockManager {
126
146
  async releaseLock(descriptor) {
127
147
  return await this.releaseLockKey(descriptor.lockKey, descriptor.taskName);
128
148
  }
129
- async releaseOwnedLocks(excludedLockKeys = new Set()) {
149
+ async releaseOwnedLocks(excludedLockKeys = new Set(), timeoutMs) {
130
150
  if (!this.redisClient || this.ownedLockKeys.size === 0) {
131
151
  return;
132
152
  }
@@ -135,7 +155,7 @@ export class CronDistributedLockManager {
135
155
  return;
136
156
  }
137
157
  await Promise.all(lockKeys.map(async lockKey => {
138
- await this.releaseLockKey(lockKey, lockKey);
158
+ await this.releaseLockKey(lockKey, lockKey, timeoutMs);
139
159
  }));
140
160
  }
141
161
  createLockRenewalState(lockTtlMs) {
@@ -200,13 +220,13 @@ export class CronDistributedLockManager {
200
220
  return 'renewal-failed';
201
221
  }
202
222
  }
203
- async releaseLockKey(lockKey, taskName) {
223
+ async releaseLockKey(lockKey, taskName, timeoutMs) {
204
224
  const redis = this.redisClient;
205
225
  if (!redis) {
206
226
  return true;
207
227
  }
208
228
  try {
209
- const result = await redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, this.options.distributed.ownerId);
229
+ const result = await withTimeout(redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey, this.options.distributed.ownerId), timeoutMs);
210
230
  if (typeof result === 'number' && result <= 0) {
211
231
  this.markLockIoAvailable();
212
232
  this.logger.warn(`Distributed cron lock for ${taskName} was already released or owned by another node.`, 'CronLifecycleService');
@@ -1 +1 @@
1
- {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAwDjF;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,iBAAsB,GAAG,2BAA2B,CAMvG;AAeD,iEAAiE;AACjE,qBAAa,UAAU;IACrB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAS5D"}
1
+ {"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAKhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAoGjF;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,GAAE,iBAAsB,GAAG,2BAA2B,CAMvG;AAeD,iEAAiE;AACjE,qBAAa,UAAU;IACrB;;;;;;;;;;;;;;;;OAgBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU;CAS5D"}
package/dist/module.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineModule } from '@fluojs/runtime';
2
- import { CronLifecycleService } from './service.js';
3
2
  import { defaultCronScheduler } from './scheduler.js';
3
+ import { CronLifecycleService } from './service.js';
4
4
  import { CRON_OPTIONS, SCHEDULING_REGISTRY } from './tokens.js';
5
5
  const DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS = 10_000;
6
6
  function randomId() {
@@ -10,6 +10,34 @@ function randomId() {
10
10
  }
11
11
  return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
12
12
  }
13
+ function normalizeRedisClientName(clientName) {
14
+ if (clientName === undefined) {
15
+ return undefined;
16
+ }
17
+ const normalizedClientName = clientName.trim();
18
+ if (normalizedClientName.length === 0) {
19
+ throw new Error('Cron distributed clientName must be a non-empty string when provided.');
20
+ }
21
+ return normalizedClientName;
22
+ }
23
+ function assertValidDistributedLockTtlMs(lockTtlMs) {
24
+ if (!Number.isFinite(lockTtlMs) || !Number.isInteger(lockTtlMs) || lockTtlMs < 1_000) {
25
+ throw new Error('Cron distributed lockTtlMs must be a positive integer greater than or equal to 1000ms.');
26
+ }
27
+ }
28
+ function normalizeDistributedOwnerId(ownerId) {
29
+ if (ownerId === undefined) {
30
+ return randomId();
31
+ }
32
+ if (typeof ownerId !== 'string') {
33
+ throw new Error('Cron distributed ownerId must be a string when provided.');
34
+ }
35
+ const normalizedOwnerId = ownerId.trim();
36
+ if (normalizedOwnerId.length === 0) {
37
+ throw new Error('Cron distributed ownerId must be a non-empty string when provided.');
38
+ }
39
+ return normalizedOwnerId;
40
+ }
13
41
  function normalizeDistributedOptions(distributed) {
14
42
  if (distributed === undefined || distributed === false) {
15
43
  return {
@@ -29,13 +57,17 @@ function normalizeDistributedOptions(distributed) {
29
57
  ownerId: randomId()
30
58
  };
31
59
  }
32
- return {
33
- clientName: distributed.clientName,
60
+ const normalizedDistributed = {
61
+ clientName: normalizeRedisClientName(distributed.clientName),
34
62
  enabled: distributed.enabled ?? true,
35
63
  keyPrefix: distributed.keyPrefix ?? 'fluo:cron:lock',
36
64
  lockTtlMs: distributed.lockTtlMs ?? 30_000,
37
- ownerId: distributed.ownerId ?? randomId()
65
+ ownerId: normalizeDistributedOwnerId(distributed.ownerId)
38
66
  };
67
+ if (normalizedDistributed.enabled) {
68
+ assertValidDistributedLockTtlMs(normalizedDistributed.lockTtlMs);
69
+ }
70
+ return normalizedDistributed;
39
71
  }
40
72
  function normalizeShutdownOptions(shutdown) {
41
73
  const timeoutMs = shutdown?.timeoutMs ?? DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS;
package/dist/service.d.ts CHANGED
@@ -88,6 +88,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
88
88
  *
89
89
  * @param name Name of the cron task to update.
90
90
  * @param expression New cron expression to validate and schedule.
91
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
91
92
  */
92
93
  updateCronExpression(name: string, expression: string): void;
93
94
  /**
@@ -95,6 +96,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
95
96
  *
96
97
  * @param name Name of the interval task to update.
97
98
  * @param ms New positive interval in milliseconds.
99
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
98
100
  */
99
101
  updateIntervalMs(name: string, ms: number): void;
100
102
  onApplicationBootstrap(): Promise<void>;
@@ -107,6 +109,8 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
107
109
  private startLifecycle;
108
110
  private validateDistributedLockConfiguration;
109
111
  private handleStartupFailure;
112
+ private completeStartupFailureCleanupAfterActiveTasks;
113
+ private resetDistributedLocksAfterStartupFailure;
110
114
  private runShutdownLifecycle;
111
115
  private getRunningDistributedLockKeys;
112
116
  private registerDecoratorTasks;
@@ -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) {
@@ -251,6 +241,7 @@ class CronLifecycleService {
251
241
  *
252
242
  * @param name Name of the cron task to update.
253
243
  * @param expression New cron expression to validate and schedule.
244
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
254
245
  */
255
246
  updateCronExpression(name, expression) {
256
247
  assertValidCronExpression(expression);
@@ -267,14 +258,18 @@ class CronLifecycleService {
267
258
  }
268
259
  const previousExpression = task.descriptor.expression;
269
260
  const previousHandle = task.scheduledHandle;
261
+ let nextHandle;
270
262
  task.descriptor.expression = expression;
271
263
  try {
272
- const nextHandle = this.createScheduledHandle(task);
273
- task.scheduledHandle = nextHandle;
264
+ nextHandle = this.createScheduledHandle(task);
274
265
  if (previousHandle) {
275
- this.stopScheduledHandle(previousHandle);
266
+ previousHandle.stop();
276
267
  }
268
+ task.scheduledHandle = nextHandle;
277
269
  } catch (error) {
270
+ if (nextHandle) {
271
+ this.stopScheduledHandle(nextHandle);
272
+ }
278
273
  task.descriptor.expression = previousExpression;
279
274
  task.scheduledHandle = previousHandle;
280
275
  throw error;
@@ -286,6 +281,7 @@ class CronLifecycleService {
286
281
  *
287
282
  * @param name Name of the interval task to update.
288
283
  * @param ms New positive interval in milliseconds.
284
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
289
285
  */
290
286
  updateIntervalMs(name, ms) {
291
287
  assertValidMs(ms, 'scheduling registry');
@@ -302,14 +298,18 @@ class CronLifecycleService {
302
298
  }
303
299
  const previousMs = task.descriptor.ms;
304
300
  const previousHandle = task.scheduledHandle;
301
+ let nextHandle;
305
302
  task.descriptor.ms = ms;
306
303
  try {
307
- const nextHandle = this.createScheduledHandle(task);
308
- task.scheduledHandle = nextHandle;
304
+ nextHandle = this.createScheduledHandle(task);
309
305
  if (previousHandle) {
310
- this.stopScheduledHandle(previousHandle);
306
+ previousHandle.stop();
311
307
  }
308
+ task.scheduledHandle = nextHandle;
312
309
  } catch (error) {
310
+ if (nextHandle) {
311
+ this.stopScheduledHandle(nextHandle);
312
+ }
313
313
  task.descriptor.ms = previousMs;
314
314
  task.scheduledHandle = previousHandle;
315
315
  throw error;
@@ -325,7 +325,7 @@ class CronLifecycleService {
325
325
  this.lifecycleState = 'ready';
326
326
  } catch (error) {
327
327
  this.lifecycleState = 'failed';
328
- this.handleStartupFailure();
328
+ await this.handleStartupFailure();
329
329
  throw error;
330
330
  }
331
331
  }
@@ -362,7 +362,7 @@ class CronLifecycleService {
362
362
  });
363
363
  }
364
364
  toSchedulingTaskDescriptor(task) {
365
- return {
365
+ return Object.freeze({
366
366
  distributed: task.descriptor.distributed,
367
367
  enabled: task.enabled,
368
368
  expression: task.descriptor.expression,
@@ -376,7 +376,7 @@ class CronLifecycleService {
376
376
  source: task.source,
377
377
  targetName: task.descriptor.targetName,
378
378
  timezone: task.descriptor.timezone
379
- };
379
+ });
380
380
  }
381
381
  async shutdown() {
382
382
  if (this.shutdownPromise) {
@@ -391,11 +391,11 @@ class CronLifecycleService {
391
391
  if (this.lifecycleState !== 'stopped' || this.activeTasks.size > 0) {
392
392
  return;
393
393
  }
394
- await this.distributedLocks.releaseOwnedLocks();
394
+ await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
395
395
  }
396
396
  async startLifecycle() {
397
- await this.distributedLocks.resolveClient();
398
397
  this.validateDistributedLockConfiguration();
398
+ await this.distributedLocks.resolveClient();
399
399
  this.registerDecoratorTasks();
400
400
  this.started = true;
401
401
  this.scheduleEnabledTasks();
@@ -406,11 +406,30 @@ class CronLifecycleService {
406
406
  }
407
407
  assertValidLockTtlMs(this.options.distributed.lockTtlMs);
408
408
  }
409
- handleStartupFailure() {
409
+ async handleStartupFailure() {
410
410
  this.started = false;
411
411
  this.stopAllScheduledTasks();
412
+ const startupRollbackTimedOut = await this.waitForActiveTasks();
413
+ if (startupRollbackTimedOut) {
414
+ 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');
415
+ }
416
+ await this.distributedLocks.releaseOwnedLocks(startupRollbackTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
412
417
  this.tasks.clear();
413
- this.distributedLocks.reset();
418
+ if (this.activeTasks.size > 0) {
419
+ void this.completeStartupFailureCleanupAfterActiveTasks();
420
+ return;
421
+ }
422
+ this.resetDistributedLocksAfterStartupFailure();
423
+ }
424
+ async completeStartupFailureCleanupAfterActiveTasks() {
425
+ await this.drainActiveTasks();
426
+ await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
427
+ this.resetDistributedLocksAfterStartupFailure();
428
+ }
429
+ resetDistributedLocksAfterStartupFailure() {
430
+ if (this.distributedLocks.ownedLocks === 0) {
431
+ this.distributedLocks.reset();
432
+ }
414
433
  }
415
434
  async runShutdownLifecycle() {
416
435
  this.lifecycleState = 'stopping';
@@ -420,7 +439,7 @@ class CronLifecycleService {
420
439
  if (shutdownTimedOut) {
421
440
  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
441
  }
423
- await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set());
442
+ await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
424
443
  this.lifecycleState = 'stopped';
425
444
  }
426
445
  getRunningDistributedLockKeys() {
@@ -433,8 +452,9 @@ class CronLifecycleService {
433
452
  }
434
453
  }
435
454
  registerTask(descriptor, source) {
455
+ assertValidSchedulingTaskName(descriptor.taskName);
436
456
  this.assertTaskNameAvailable(descriptor.taskName);
437
- if (descriptor.distributed) {
457
+ if (this.options.distributed.enabled && descriptor.distributed) {
438
458
  assertValidLockTtlMs(descriptor.lockTtlMs);
439
459
  }
440
460
  const task = {
@@ -572,7 +592,7 @@ class CronLifecycleService {
572
592
  this.runningDistributedLockKeys.delete(descriptor.lockKey);
573
593
  const released = await this.distributedLocks.releaseLock(descriptor);
574
594
  if (!released && this.lifecycleState === 'stopped') {
575
- await this.distributedLocks.releaseOwnedLocks();
595
+ await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
576
596
  }
577
597
  }
578
598
  }
@@ -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
  *
@@ -1 +1 @@
1
- {"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,mBAAmB,EAAc,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AASlF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAEtE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,iBAAiB,GACxB,kBAAkB,EAAE,CA4DtB"}
1
+ {"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAS,MAAM,cAAc,CAAC;AAG/D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGzE,OAAO,KAAK,EAAE,kBAAkB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AASlF;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;GAIG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIhE;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAS1F;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,mBAAmB,GAAG,MAAM,CAEtE;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,OAAO,EAAE,2BAA2B,EACpC,MAAM,EAAE,iBAAiB,GACxB,kBAAkB,EAAE,CA+DtB"}
@@ -22,6 +22,33 @@ export function createLockKey(prefix, taskName) {
22
22
  return `${prefix}:${taskName}`;
23
23
  }
24
24
 
25
+ /**
26
+ * Asserts that a scheduling task name can be used as a registry key.
27
+ *
28
+ * @param name Scheduling task name supplied by a decorator or registry call.
29
+ */
30
+ export function assertValidSchedulingTaskName(name) {
31
+ if (name.trim().length === 0) {
32
+ throw new Error('Scheduling task name must be a non-empty string.');
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Resolves the effective scheduling task name while preserving authored names.
38
+ *
39
+ * @param defaultName Name derived from the decorated target or registry argument.
40
+ * @param optionName Optional name override supplied in scheduling options.
41
+ * @returns The effective task name used by the scheduler registry.
42
+ */
43
+ export function resolveSchedulingTaskName(defaultName, optionName) {
44
+ assertValidSchedulingTaskName(defaultName);
45
+ if (optionName !== undefined) {
46
+ assertValidSchedulingTaskName(optionName);
47
+ return optionName;
48
+ }
49
+ return defaultName;
50
+ }
51
+
25
52
  /**
26
53
  * Method key to name.
27
54
  *
@@ -53,7 +80,7 @@ export function discoverCronTaskDescriptors(compiledModules, options, logger) {
53
80
  }
54
81
  for (const entry of entries) {
55
82
  const methodName = methodKeyToName(entry.propertyKey);
56
- const taskName = entry.metadata.options.name ?? buildDefaultTaskName(candidate.targetType.name, methodName);
83
+ const taskName = resolveSchedulingTaskName(buildDefaultTaskName(candidate.targetType.name, methodName), entry.metadata.options.name);
57
84
  const seenMethods = seen.get(candidate.targetType) ?? new Set();
58
85
  const lockTtlMs = entry.metadata.options.lockTtlMs ?? options.distributed.lockTtlMs;
59
86
  if (seenMethods.has(methodName)) {
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "timeout",
10
10
  "distributed-lock"
11
11
  ],
12
- "version": "1.1.0",
12
+ "version": "2.0.1",
13
13
  "private": false,
14
14
  "license": "MIT",
15
15
  "repository": {
@@ -37,12 +37,12 @@
37
37
  ],
38
38
  "dependencies": {
39
39
  "croner": "^8.1.2",
40
- "@fluojs/core": "^1.0.3",
41
- "@fluojs/di": "^1.1.0",
42
- "@fluojs/runtime": "^1.1.8"
40
+ "@fluojs/core": "^1.1.0",
41
+ "@fluojs/di": "^2.0.0",
42
+ "@fluojs/runtime": "^2.0.1"
43
43
  },
44
44
  "peerDependencies": {
45
- "@fluojs/redis": "^1.0.2"
45
+ "@fluojs/redis": "^1.1.0"
46
46
  },
47
47
  "peerDependenciesMeta": {
48
48
  "@fluojs/redis": {
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "devDependencies": {
53
53
  "vitest": "^3.2.4",
54
- "@fluojs/redis": "^1.0.2"
54
+ "@fluojs/redis": "^1.1.0"
55
55
  },
56
56
  "scripts": {
57
57
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",