@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 +31 -8
- package/README.md +31 -8
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +3 -0
- package/dist/distributed-lock-manager.d.ts +7 -2
- package/dist/distributed-lock-manager.d.ts.map +1 -1
- package/dist/distributed-lock-manager.js +68 -8
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +36 -4
- package/dist/service.d.ts +12 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +101 -27
- package/dist/status.d.ts +1 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +17 -1
- package/dist/task-discovery.d.ts +15 -1
- package/dist/task-discovery.d.ts.map +1 -1
- package/dist/task-discovery.js +28 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -6
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
|
|
24
|
+
npm install @fluojs/cron
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
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는
|
|
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
|
|
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합니다.
|
|
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
|
|
24
|
+
npm install @fluojs/cron
|
|
24
25
|
```
|
|
25
26
|
|
|
26
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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({
|
package/dist/decorators.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/decorators.js
CHANGED
|
@@ -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<
|
|
33
|
-
releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string
|
|
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;
|
|
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) {
|
package/dist/module.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;
|
|
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
|
|
21
|
-
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
265
|
-
task.scheduledHandle = nextHandle;
|
|
264
|
+
nextHandle = this.createScheduledHandle(task);
|
|
266
265
|
if (previousHandle) {
|
|
267
|
-
|
|
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.
|
|
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
package/dist/status.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
},
|
package/dist/task-discovery.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { MetadataPropertyKey } from '@fluojs/core';
|
|
2
2
|
import type { ApplicationLogger, CompiledModule } from '@fluojs/runtime';
|
|
3
3
|
import type { CronTaskDescriptor, NormalizedCronModuleOptions } from './types.js';
|
|
4
4
|
/**
|
|
@@ -17,6 +17,20 @@ export declare function buildDefaultTaskName(targetName: string, methodName: str
|
|
|
17
17
|
* @returns The create lock key result.
|
|
18
18
|
*/
|
|
19
19
|
export declare function createLockKey(prefix: string, taskName: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Asserts that a scheduling task name can be used as a registry key.
|
|
22
|
+
*
|
|
23
|
+
* @param name Scheduling task name supplied by a decorator or registry call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function assertValidSchedulingTaskName(name: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the effective scheduling task name while preserving authored names.
|
|
28
|
+
*
|
|
29
|
+
* @param defaultName Name derived from the decorated target or registry argument.
|
|
30
|
+
* @param optionName Optional name override supplied in scheduling options.
|
|
31
|
+
* @returns The effective task name used by the scheduler registry.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveSchedulingTaskName(defaultName: string, optionName?: string): string;
|
|
20
34
|
/**
|
|
21
35
|
* Method key to name.
|
|
22
36
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"task-discovery.d.ts","sourceRoot":"","sources":["../src/task-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
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"}
|
package/dist/task-discovery.js
CHANGED
|
@@ -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 =
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -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": "
|
|
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
|
|
41
|
-
"@fluojs/
|
|
42
|
-
"@fluojs/
|
|
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
|
|
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
|
|
54
|
+
"@fluojs/redis": "^1.1.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|