@fluojs/cron 1.0.3 → 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,6 +42,8 @@ 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
 
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와 동일하게 검증됩니다.
46
+
44
47
  ```typescript
45
48
  import { Module } from '@fluojs/core';
46
49
  import { CronModule, Cron, CronExpression, Interval, Timeout } from '@fluojs/cron';
@@ -71,6 +74,22 @@ class AppModule {}
71
74
 
72
75
  ## 공통 패턴
73
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
+
74
93
  ### 분산 락 사용하기
75
94
 
76
95
  여러 서버 인스턴스에서 스케줄링된 작업이 동시에 실행되는 것을 방지하려면 분산 모드를 활성화하세요. 이 기능은 `@fluojs/redis`가 필요하며, Redis peer는 `distributed.enabled`가 `true`일 때만 로드되고 resolve됩니다.
@@ -95,11 +114,11 @@ import { RedisModule } from '@fluojs/redis';
95
114
  class AppModule {}
96
115
  ```
97
116
 
98
- `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 값을 거부합니다.
99
118
 
100
- `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`도 포함됩니다.
101
120
 
102
- 각 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을 함께 사용해야 합니다.
103
122
 
104
123
  ```typescript
105
124
  @Module({
@@ -137,21 +156,25 @@ class TaskManager {
137
156
  });
138
157
  }
139
158
 
159
+ speedUpPolling() {
160
+ this.registry.updateIntervalMs('inventory.poll', 5_000);
161
+ }
162
+
140
163
  stopTask() {
141
164
  this.registry.remove('dynamic-job');
142
165
  }
143
166
  }
144
167
  ```
145
168
 
146
- Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`을 제공합니다. 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에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
147
170
 
148
- Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression update도 rollback-safe합니다. Rescheduling실패하면 이전 expression scheduled job이 그대로 유지됩니다. 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으로 실행되지 않습니다.
149
172
 
150
173
  ### 제한된 종료
151
174
 
152
175
  `CronModule`은 애플리케이션 종료 시 실행 중인 작업을 제한된 타임아웃 안에서 drain합니다. 따라서 하나의 hung task 때문에 프로세스 종료가 영원히 막히지 않습니다.
153
176
 
154
- 기본적으로 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을 지우지 않습니다. 이렇게 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
155
178
 
156
179
  ```typescript
157
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,6 +42,8 @@ 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
 
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.
46
+
44
47
  ```typescript
45
48
  import { Module } from '@fluojs/core';
46
49
  import { CronModule, Cron, CronExpression, Interval, Timeout } from '@fluojs/cron';
@@ -71,6 +74,22 @@ class AppModule {}
71
74
 
72
75
  ## Common Patterns
73
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
+
74
93
  ### Distributed Locking
75
94
 
76
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`.
@@ -95,11 +114,11 @@ import { RedisModule } from '@fluojs/redis';
95
114
  class AppModule {}
96
115
  ```
97
116
 
98
- 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.
99
118
 
100
- `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.
101
120
 
102
- 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.
103
122
 
104
123
  ```typescript
105
124
  @Module({
@@ -137,21 +156,25 @@ class TaskManager {
137
156
  });
138
157
  }
139
158
 
159
+ speedUpPolling() {
160
+ this.registry.updateIntervalMs('inventory.poll', 5_000);
161
+ }
162
+
140
163
  stopTask() {
141
164
  this.registry.remove('dynamic-job');
142
165
  }
143
166
  }
144
167
  ```
145
168
 
146
- The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, and `updateCronExpression`. 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.
147
170
 
148
- 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 is also rollback-safe. If rescheduling fails, the previous expression and scheduled job 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.
149
172
 
150
173
  ### Bounded Shutdown
151
174
 
152
175
  `CronModule` drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.
153
176
 
154
- 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.
155
178
 
156
179
  ```typescript
157
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
  /**
@@ -17,25 +17,30 @@ export declare class CronDistributedLockManager {
17
17
  private readonly runtimeContainer;
18
18
  private readonly logger;
19
19
  private readonly ownedLockKeys;
20
+ private lockIoError;
20
21
  private redisClient;
21
22
  private lockOwnershipLosses;
22
23
  private lockRenewalFailures;
23
24
  constructor(options: NormalizedCronModuleOptions, runtimeContainer: Container, logger: ApplicationLogger);
24
25
  get resolvedClient(): RedisLockClient | undefined;
25
26
  get ownedLocks(): number;
27
+ get lockIoAvailable(): boolean;
26
28
  get ownershipLosses(): number;
27
29
  get renewalFailures(): number;
28
30
  resolveClient(): Promise<void>;
29
31
  reset(): void;
30
32
  tryAcquireLock(descriptor: CronTaskDescriptor): Promise<boolean>;
31
33
  startLockRenewalMonitor(descriptor: CronTaskDescriptor): LockRenewalMonitor;
32
- releaseLock(descriptor: CronTaskDescriptor): Promise<void>;
33
- releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>): Promise<void>;
34
+ releaseLock(descriptor: CronTaskDescriptor): Promise<boolean>;
35
+ releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>, timeoutMs?: number): Promise<void>;
34
36
  private createLockRenewalState;
35
37
  private queueDueLockRenewalAttempts;
36
38
  private runLockRenewalAttempt;
37
39
  private toLockPostRunError;
38
40
  private renewLock;
39
41
  private releaseLockKey;
42
+ private verifyLockIoAvailability;
43
+ private markLockIoAvailable;
44
+ private markLockIoUnavailable;
40
45
  }
41
46
  //# sourceMappingURL=distributed-lock-manager.d.ts.map
@@ -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;IAOnC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IARzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,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,MAAM,CAE5B;IAED,IAAI,eAAe,IAAI,MAAM,CAE5B;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAqBpC,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IA+BtE,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,GAAG,kBAAkB;IA8BrE,WAAW,CAAC,UAAU,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,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;YAwCT,cAAc;CAgC7B"}
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();
@@ -30,6 +50,7 @@ async function resolveRedisPeerModule() {
30
50
  /** Coordinates Redis lock acquisition, renewal, and release for scheduled cron tasks. */
31
51
  export class CronDistributedLockManager {
32
52
  ownedLockKeys = new Set();
53
+ lockIoError;
33
54
  redisClient;
34
55
  lockOwnershipLosses = 0;
35
56
  lockRenewalFailures = 0;
@@ -44,6 +65,12 @@ export class CronDistributedLockManager {
44
65
  get ownedLocks() {
45
66
  return this.ownedLockKeys.size;
46
67
  }
68
+ get lockIoAvailable() {
69
+ if (!this.options.distributed.enabled) {
70
+ return true;
71
+ }
72
+ return this.redisClient !== undefined && this.lockIoError === undefined;
73
+ }
47
74
  get ownershipLosses() {
48
75
  return this.lockOwnershipLosses;
49
76
  }
@@ -66,8 +93,10 @@ export class CronDistributedLockManager {
66
93
  throw new Error('Cron distributed mode requires the configured Redis client to implement set/eval lock operations.');
67
94
  }
68
95
  this.redisClient = redisClient;
96
+ await this.verifyLockIoAvailability();
69
97
  }
70
98
  reset() {
99
+ this.lockIoError = undefined;
71
100
  this.redisClient = undefined;
72
101
  }
73
102
  async tryAcquireLock(descriptor) {
@@ -77,11 +106,13 @@ export class CronDistributedLockManager {
77
106
  }
78
107
  try {
79
108
  const result = await redis.set(descriptor.lockKey, this.options.distributed.ownerId, 'PX', descriptor.lockTtlMs, 'NX');
109
+ this.markLockIoAvailable();
80
110
  if (result === 'OK') {
81
111
  this.ownedLockKeys.add(descriptor.lockKey);
82
112
  }
83
113
  return result === 'OK';
84
114
  } catch (error) {
115
+ this.markLockIoUnavailable(error);
85
116
  this.logger.error(`Failed to acquire distributed cron lock for ${descriptor.taskName}.`, error, 'CronLifecycleService');
86
117
  return false;
87
118
  }
@@ -96,7 +127,7 @@ export class CronDistributedLockManager {
96
127
  renewalState.renewalChain = renewalState.renewalChain.then(async () => {
97
128
  await this.runLockRenewalAttempt(descriptor, renewalState);
98
129
  });
99
- }, renewalState.renewalIntervalMs);
130
+ }, renewalState.renewalIntervalMs).unref();
100
131
  return {
101
132
  getPostRunError: async () => {
102
133
  this.queueDueLockRenewalAttempts(descriptor, renewalState);
@@ -113,9 +144,9 @@ export class CronDistributedLockManager {
113
144
  };
114
145
  }
115
146
  async releaseLock(descriptor) {
116
- await this.releaseLockKey(descriptor.lockKey, descriptor.taskName);
147
+ return await this.releaseLockKey(descriptor.lockKey, descriptor.taskName);
117
148
  }
118
- async releaseOwnedLocks(excludedLockKeys = new Set()) {
149
+ async releaseOwnedLocks(excludedLockKeys = new Set(), timeoutMs) {
119
150
  if (!this.redisClient || this.ownedLockKeys.size === 0) {
120
151
  return;
121
152
  }
@@ -124,7 +155,7 @@ export class CronDistributedLockManager {
124
155
  return;
125
156
  }
126
157
  await Promise.all(lockKeys.map(async lockKey => {
127
- await this.releaseLockKey(lockKey, lockKey);
158
+ await this.releaseLockKey(lockKey, lockKey, timeoutMs);
128
159
  }));
129
160
  }
130
161
  createLockRenewalState(lockTtlMs) {
@@ -176,34 +207,63 @@ export class CronDistributedLockManager {
176
207
  try {
177
208
  const result = await redis.eval(RENEW_LOCK_SCRIPT, 1, descriptor.lockKey, this.options.distributed.ownerId, String(descriptor.lockTtlMs));
178
209
  if (typeof result === 'number' && result <= 0) {
210
+ this.markLockIoAvailable();
179
211
  this.logger.warn(`Distributed cron lock ownership was lost for ${descriptor.taskName}.`, 'CronLifecycleService');
180
212
  return 'ownership-lost';
181
213
  }
214
+ this.markLockIoAvailable();
182
215
  this.logger.log(`Renewed distributed cron lock for ${descriptor.taskName}.`, 'CronLifecycleService');
183
216
  return 'renewed';
184
217
  } catch (error) {
218
+ this.markLockIoUnavailable(error);
185
219
  this.logger.error(`Failed to renew distributed cron lock for ${descriptor.taskName}.`, error, 'CronLifecycleService');
186
220
  return 'renewal-failed';
187
221
  }
188
222
  }
189
- async releaseLockKey(lockKey, taskName) {
223
+ async releaseLockKey(lockKey, taskName, timeoutMs) {
190
224
  const redis = this.redisClient;
191
225
  if (!redis) {
192
- return;
226
+ return true;
193
227
  }
194
228
  try {
195
- 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);
196
230
  if (typeof result === 'number' && result <= 0) {
231
+ this.markLockIoAvailable();
197
232
  this.logger.warn(`Distributed cron lock for ${taskName} was already released or owned by another node.`, 'CronLifecycleService');
198
233
  this.ownedLockKeys.delete(lockKey);
199
- return;
234
+ return true;
200
235
  }
236
+ this.markLockIoAvailable();
201
237
  this.logger.log(`Released distributed cron lock for ${taskName}.`, 'CronLifecycleService');
202
238
  this.ownedLockKeys.delete(lockKey);
239
+ return true;
203
240
  } catch (error) {
241
+ this.markLockIoUnavailable(error);
204
242
  this.logger.error(`Failed to release distributed cron lock for ${taskName}.`, error, 'CronLifecycleService');
243
+ return false;
244
+ }
245
+ }
246
+ async verifyLockIoAvailability() {
247
+ const redis = this.redisClient;
248
+ if (!redis) {
249
+ return;
250
+ }
251
+ const probeKey = `${this.options.distributed.keyPrefix}:__probe:${this.options.distributed.ownerId}`;
252
+ try {
253
+ await redis.set(probeKey, this.options.distributed.ownerId, 'PX', 1_000, 'NX');
254
+ await redis.eval(RELEASE_LOCK_SCRIPT, 1, probeKey, this.options.distributed.ownerId);
255
+ this.markLockIoAvailable();
256
+ } catch (error) {
257
+ this.markLockIoUnavailable(error);
258
+ throw new Error('Cron distributed mode requires Redis lock I/O to be available.');
205
259
  }
206
260
  }
261
+ markLockIoAvailable() {
262
+ this.lockIoError = undefined;
263
+ }
264
+ markLockIoUnavailable(error) {
265
+ this.lockIoError = error instanceof Error ? error : new Error('Redis lock I/O failed.');
266
+ }
207
267
  }
208
268
  function hasRedisLockClient(value) {
209
269
  if (typeof value !== 'object' || value === null) {
@@ -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,17 +88,29 @@ 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;
94
+ /**
95
+ * Replaces the millisecond cadence of one existing interval task.
96
+ *
97
+ * @param name Name of the interval task to update.
98
+ * @param ms New positive interval in milliseconds.
99
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
100
+ */
101
+ updateIntervalMs(name: string, ms: number): void;
93
102
  onApplicationBootstrap(): Promise<void>;
94
103
  onApplicationShutdown(): Promise<void>;
95
104
  onModuleDestroy(): Promise<void>;
96
105
  createPlatformStatusSnapshot(): import("./status.js").CronPlatformStatusSnapshot;
97
106
  private toSchedulingTaskDescriptor;
98
107
  private shutdown;
108
+ private retryReleasedDistributedLocksAfterShutdown;
99
109
  private startLifecycle;
100
110
  private validateDistributedLockConfiguration;
101
111
  private handleStartupFailure;
112
+ private completeStartupFailureCleanupAfterActiveTasks;
113
+ private resetDistributedLocksAfterStartupFailure;
102
114
  private runShutdownLifecycle;
103
115
  private getRunningDistributedLockKeys;
104
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
@@ -5,11 +5,11 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
5
5
  function _setFunctionName(e, t, n) { "symbol" == typeof t && (t = (t = t.description) ? "[" + t + "]" : ""); try { Object.defineProperty(e, "name", { configurable: !0, value: n ? n + " " + t : t }); } catch (e) {} return e; }
6
6
  function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side of 'in' should be an object, got " + (null !== e ? typeof e : "null")); return e; }
7
7
  import { Inject } from '@fluojs/core';
8
- import { Cron as CronValidator } from 'croner';
9
8
  import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
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,10 +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
- }
20
+ function resolveDynamicTaskName(name, optionName) {
21
+ return resolveSchedulingTaskName(name, optionName);
24
22
  }
25
23
  function assertValidMs(ms, context) {
26
24
  if (!Number.isFinite(ms) || !Number.isInteger(ms) || ms <= 0) {
@@ -85,8 +83,8 @@ class CronLifecycleService {
85
83
  * @param options Optional hooks, distributed lock overrides, and timezone.
86
84
  */
87
85
  addCron(name, expression, callback, options = {}) {
88
- assertValidTaskName(name);
89
86
  assertValidCronExpression(expression);
87
+ const taskName = resolveDynamicTaskName(name, options.name);
90
88
  this.registerTask({
91
89
  afterRun: options.afterRun,
92
90
  beforeRun: options.beforeRun,
@@ -94,11 +92,11 @@ class CronLifecycleService {
94
92
  distributed: options.distributed ?? true,
95
93
  expression,
96
94
  kind: 'cron',
97
- lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
95
+ lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? taskName),
98
96
  lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
99
97
  onError: options.onError,
100
98
  onSuccess: options.onSuccess,
101
- taskName: name,
99
+ taskName,
102
100
  timezone: options.timezone
103
101
  }, 'dynamic');
104
102
  }
@@ -112,20 +110,20 @@ class CronLifecycleService {
112
110
  * @param options Optional hooks and distributed lock overrides.
113
111
  */
114
112
  addInterval(name, ms, callback, options = {}) {
115
- assertValidTaskName(name);
116
113
  assertValidMs(ms, 'scheduling registry');
114
+ const taskName = resolveDynamicTaskName(name, options.name);
117
115
  this.registerTask({
118
116
  afterRun: options.afterRun,
119
117
  beforeRun: options.beforeRun,
120
118
  callback,
121
119
  distributed: options.distributed ?? true,
122
120
  kind: 'interval',
123
- lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
121
+ lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? taskName),
124
122
  lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
125
123
  ms,
126
124
  onError: options.onError,
127
125
  onSuccess: options.onSuccess,
128
- taskName: name
126
+ taskName
129
127
  }, 'dynamic');
130
128
  }
131
129
 
@@ -138,20 +136,20 @@ class CronLifecycleService {
138
136
  * @param options Optional hooks and distributed lock overrides.
139
137
  */
140
138
  addTimeout(name, ms, callback, options = {}) {
141
- assertValidTaskName(name);
142
139
  assertValidMs(ms, 'scheduling registry');
140
+ const taskName = resolveDynamicTaskName(name, options.name);
143
141
  this.registerTask({
144
142
  afterRun: options.afterRun,
145
143
  beforeRun: options.beforeRun,
146
144
  callback,
147
145
  distributed: options.distributed ?? true,
148
146
  kind: 'timeout',
149
- lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? name),
147
+ lockKey: createLockKey(this.options.distributed.keyPrefix, options.key ?? taskName),
150
148
  lockTtlMs: options.lockTtlMs ?? this.options.distributed.lockTtlMs,
151
149
  ms,
152
150
  onError: options.onError,
153
151
  onSuccess: options.onSuccess,
154
- taskName: name
152
+ taskName
155
153
  }, 'dynamic');
156
154
  }
157
155
 
@@ -243,6 +241,7 @@ class CronLifecycleService {
243
241
  *
244
242
  * @param name Name of the cron task to update.
245
243
  * @param expression New cron expression to validate and schedule.
244
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
246
245
  */
247
246
  updateCronExpression(name, expression) {
248
247
  assertValidCronExpression(expression);
@@ -259,19 +258,63 @@ class CronLifecycleService {
259
258
  }
260
259
  const previousExpression = task.descriptor.expression;
261
260
  const previousHandle = task.scheduledHandle;
261
+ let nextHandle;
262
262
  task.descriptor.expression = expression;
263
263
  try {
264
- const nextHandle = this.createScheduledHandle(task);
265
- task.scheduledHandle = nextHandle;
264
+ nextHandle = this.createScheduledHandle(task);
266
265
  if (previousHandle) {
267
- this.stopScheduledHandle(previousHandle);
266
+ previousHandle.stop();
268
267
  }
268
+ task.scheduledHandle = nextHandle;
269
269
  } catch (error) {
270
+ if (nextHandle) {
271
+ this.stopScheduledHandle(nextHandle);
272
+ }
270
273
  task.descriptor.expression = previousExpression;
271
274
  task.scheduledHandle = previousHandle;
272
275
  throw error;
273
276
  }
274
277
  }
278
+
279
+ /**
280
+ * Replaces the millisecond cadence of one existing interval task.
281
+ *
282
+ * @param name Name of the interval task to update.
283
+ * @param ms New positive interval in milliseconds.
284
+ * @throws When validation, replacement scheduling, or previous-handle shutdown fails.
285
+ */
286
+ updateIntervalMs(name, ms) {
287
+ assertValidMs(ms, 'scheduling registry');
288
+ const task = this.tasks.get(name);
289
+ if (!task) {
290
+ throw new Error(`Scheduling task "${name}" does not exist.`);
291
+ }
292
+ if (task.descriptor.kind !== 'interval') {
293
+ throw new Error(`updateIntervalMs() supports only interval tasks. Received ${task.descriptor.kind} task "${name}".`);
294
+ }
295
+ if (!task.enabled || !this.started) {
296
+ task.descriptor.ms = ms;
297
+ return;
298
+ }
299
+ const previousMs = task.descriptor.ms;
300
+ const previousHandle = task.scheduledHandle;
301
+ let nextHandle;
302
+ task.descriptor.ms = ms;
303
+ try {
304
+ nextHandle = this.createScheduledHandle(task);
305
+ if (previousHandle) {
306
+ previousHandle.stop();
307
+ }
308
+ task.scheduledHandle = nextHandle;
309
+ } catch (error) {
310
+ if (nextHandle) {
311
+ this.stopScheduledHandle(nextHandle);
312
+ }
313
+ task.descriptor.ms = previousMs;
314
+ task.scheduledHandle = previousHandle;
315
+ throw error;
316
+ }
317
+ }
275
318
  async onApplicationBootstrap() {
276
319
  if (this.started) {
277
320
  return;
@@ -282,7 +325,7 @@ class CronLifecycleService {
282
325
  this.lifecycleState = 'ready';
283
326
  } catch (error) {
284
327
  this.lifecycleState = 'failed';
285
- this.handleStartupFailure();
328
+ await this.handleStartupFailure();
286
329
  throw error;
287
330
  }
288
331
  }
@@ -313,12 +356,13 @@ class CronLifecycleService {
313
356
  lockRenewalFailures: this.distributedLocks.renewalFailures,
314
357
  ownedLocks: this.distributedLocks.ownedLocks,
315
358
  redisDependencyResolved: this.distributedLocks.resolvedClient !== undefined,
359
+ redisLockIoAvailable: this.distributedLocks.lockIoAvailable,
316
360
  runningTasks,
317
361
  totalTasks: this.tasks.size
318
362
  });
319
363
  }
320
364
  toSchedulingTaskDescriptor(task) {
321
- return {
365
+ return Object.freeze({
322
366
  distributed: task.descriptor.distributed,
323
367
  enabled: task.enabled,
324
368
  expression: task.descriptor.expression,
@@ -332,19 +376,26 @@ class CronLifecycleService {
332
376
  source: task.source,
333
377
  targetName: task.descriptor.targetName,
334
378
  timezone: task.descriptor.timezone
335
- };
379
+ });
336
380
  }
337
381
  async shutdown() {
338
382
  if (this.shutdownPromise) {
339
383
  await this.shutdownPromise;
384
+ await this.retryReleasedDistributedLocksAfterShutdown();
340
385
  return;
341
386
  }
342
387
  this.shutdownPromise = this.runShutdownLifecycle();
343
388
  await this.shutdownPromise;
344
389
  }
390
+ async retryReleasedDistributedLocksAfterShutdown() {
391
+ if (this.lifecycleState !== 'stopped' || this.activeTasks.size > 0) {
392
+ return;
393
+ }
394
+ await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
395
+ }
345
396
  async startLifecycle() {
346
- await this.distributedLocks.resolveClient();
347
397
  this.validateDistributedLockConfiguration();
398
+ await this.distributedLocks.resolveClient();
348
399
  this.registerDecoratorTasks();
349
400
  this.started = true;
350
401
  this.scheduleEnabledTasks();
@@ -355,11 +406,30 @@ class CronLifecycleService {
355
406
  }
356
407
  assertValidLockTtlMs(this.options.distributed.lockTtlMs);
357
408
  }
358
- handleStartupFailure() {
409
+ async handleStartupFailure() {
359
410
  this.started = false;
360
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);
361
417
  this.tasks.clear();
362
- 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
+ }
363
433
  }
364
434
  async runShutdownLifecycle() {
365
435
  this.lifecycleState = 'stopping';
@@ -369,7 +439,7 @@ class CronLifecycleService {
369
439
  if (shutdownTimedOut) {
370
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');
371
441
  }
372
- await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set());
442
+ await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.options.shutdown.timeoutMs);
373
443
  this.lifecycleState = 'stopped';
374
444
  }
375
445
  getRunningDistributedLockKeys() {
@@ -382,8 +452,9 @@ class CronLifecycleService {
382
452
  }
383
453
  }
384
454
  registerTask(descriptor, source) {
455
+ assertValidSchedulingTaskName(descriptor.taskName);
385
456
  this.assertTaskNameAvailable(descriptor.taskName);
386
- if (descriptor.distributed) {
457
+ if (this.options.distributed.enabled && descriptor.distributed) {
387
458
  assertValidLockTtlMs(descriptor.lockTtlMs);
388
459
  }
389
460
  const task = {
@@ -518,8 +589,11 @@ class CronLifecycleService {
518
589
  });
519
590
  } finally {
520
591
  lockRenewalMonitor.stop();
521
- await this.distributedLocks.releaseLock(descriptor);
522
592
  this.runningDistributedLockKeys.delete(descriptor.lockKey);
593
+ const released = await this.distributedLocks.releaseLock(descriptor);
594
+ if (!released && this.lifecycleState === 'stopped') {
595
+ await this.distributedLocks.releaseOwnedLocks(new Set(), this.options.shutdown.timeoutMs);
596
+ }
523
597
  }
524
598
  }
525
599
  async waitForActiveTasks() {
package/dist/status.d.ts CHANGED
@@ -12,6 +12,7 @@ export interface CronStatusAdapterInput {
12
12
  lockRenewalFailures: number;
13
13
  ownedLocks: number;
14
14
  redisDependencyResolved: boolean;
15
+ redisLockIoAvailable?: boolean;
15
16
  runningTasks: number;
16
17
  totalTasks: number;
17
18
  }
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtG,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,kBAAkB,CAAC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,OAAO,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAoFD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,sBAAsB,GAAG,0BAA0B,CAsB1G"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEvG,qEAAqE;AACrE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtG,mFAAmF;AACnF,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,kBAAkB,CAAC;IACnC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,uBAAuB,EAAE,OAAO,CAAC;IACjC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qFAAqF;AACrF,MAAM,WAAW,0BAA0B;IACzC,SAAS,EAAE,uBAAuB,CAAC;IACnC,MAAM,EAAE,oBAAoB,CAAC;IAC7B,SAAS,EAAE,gBAAgB,CAAC,WAAW,CAAC,CAAC;IACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAuGD;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,sBAAsB,GAAG,0BAA0B,CAyB1G"}
package/dist/status.js CHANGED
@@ -5,8 +5,9 @@
5
5
  /** Cron-specific platform snapshot returned to health and readiness integrations. */
6
6
 
7
7
  function createReadiness(input) {
8
+ const redisLockIoAvailable = resolveRedisLockIoAvailable(input);
8
9
  if (input.lifecycleState === 'ready') {
9
- if (input.distributedEnabled && !input.redisDependencyResolved) {
10
+ if (input.distributedEnabled && (!input.redisDependencyResolved || !redisLockIoAvailable)) {
10
11
  return {
11
12
  critical: true,
12
13
  reason: 'Distributed cron mode requires a ready Redis lock client.',
@@ -53,6 +54,7 @@ function createReadiness(input) {
53
54
  };
54
55
  }
55
56
  function createHealth(input) {
57
+ const redisLockIoAvailable = resolveRedisLockIoAvailable(input);
56
58
  if (input.lifecycleState === 'failed' || input.lifecycleState === 'stopped') {
57
59
  return {
58
60
  reason: 'Cron scheduler is unavailable.',
@@ -65,6 +67,12 @@ function createHealth(input) {
65
67
  status: 'degraded'
66
68
  };
67
69
  }
70
+ if (input.distributedEnabled && (!input.redisDependencyResolved || !redisLockIoAvailable)) {
71
+ return {
72
+ reason: 'Distributed cron Redis lock I/O is unavailable.',
73
+ status: 'unhealthy'
74
+ };
75
+ }
68
76
  if (input.lockRenewalFailures > 0 || input.lockOwnershipLosses > 0) {
69
77
  return {
70
78
  reason: 'Distributed cron lock renewal reported recoverable failures.',
@@ -75,6 +83,12 @@ function createHealth(input) {
75
83
  status: 'healthy'
76
84
  };
77
85
  }
86
+ function resolveRedisLockIoAvailable(input) {
87
+ if (!input.distributedEnabled) {
88
+ return true;
89
+ }
90
+ return input.redisLockIoAvailable ?? input.redisDependencyResolved;
91
+ }
78
92
 
79
93
  /**
80
94
  * Creates the cron platform snapshot consumed by status reporters.
@@ -83,6 +97,7 @@ function createHealth(input) {
83
97
  * @returns Readiness, health, ownership, and cron detail fields.
84
98
  */
85
99
  export function createCronPlatformStatusSnapshot(input) {
100
+ const redisLockIoAvailable = resolveRedisLockIoAvailable(input);
86
101
  return {
87
102
  details: {
88
103
  activeTicks: input.activeTicks,
@@ -94,6 +109,7 @@ export function createCronPlatformStatusSnapshot(input) {
94
109
  lockRenewalFailures: input.lockRenewalFailures,
95
110
  ownedLocks: input.ownedLocks,
96
111
  redisDependencyResolved: input.redisDependencyResolved,
112
+ redisLockIoAvailable,
97
113
  runningTasks: input.runningTasks,
98
114
  totalTasks: input.totalTasks
99
115
  },
@@ -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/dist/types.d.ts CHANGED
@@ -204,5 +204,12 @@ export interface SchedulingRegistry {
204
204
  * @param expression New cron expression validated before rescheduling.
205
205
  */
206
206
  updateCronExpression(name: string, expression: string): void;
207
+ /**
208
+ * Replaces the millisecond cadence for an existing interval task.
209
+ *
210
+ * @param name Task name to update.
211
+ * @param ms New positive interval in milliseconds.
212
+ */
213
+ updateIntervalMs(name: string, ms: number): void;
207
214
  }
208
215
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "timeout",
10
10
  "distributed-lock"
11
11
  ],
12
- "version": "1.0.3",
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/runtime": "^1.1.7",
42
- "@fluojs/di": "^1.1.0"
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.1"
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.1"
54
+ "@fluojs/redis": "^1.1.0"
55
55
  },
56
56
  "scripts": {
57
57
  "prebuild": "node ../../tooling/scripts/clean-dist.mjs",