@fluojs/cron 2.0.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +42 -3
- package/README.md +42 -3
- package/dist/decorators.d.ts.map +1 -1
- package/dist/decorators.js +1 -1
- package/dist/distributed-lock-manager.d.ts +12 -5
- package/dist/distributed-lock-manager.d.ts.map +1 -1
- package/dist/distributed-lock-manager.js +54 -32
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +4 -10
- package/dist/random-id.d.ts +3 -0
- package/dist/random-id.d.ts.map +1 -0
- package/dist/random-id.js +8 -0
- package/dist/service.d.ts +6 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +92 -38
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -8
package/README.ko.md
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
# @fluojs/cron
|
|
2
2
|
|
|
3
3
|
<p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
|
|
4
|
+
<!-- fluo:cron-nestjs-migration: timezone-mapping -->
|
|
5
|
+
<!-- fluo:cron-nestjs-migration: wait-for-completion -->
|
|
6
|
+
<!-- fluo:cron-nestjs-migration: unsupported-options -->
|
|
7
|
+
<!-- fluo:cron-nestjs-migration: absolute-time -->
|
|
8
|
+
<!-- fluo:cron-nestjs-migration: named-interval-timeout -->
|
|
9
|
+
<!-- fluo:cron-nestjs-migration: async-configuration -->
|
|
10
|
+
<!-- fluo:cron-nestjs-migration: global-visibility -->
|
|
11
|
+
<!-- fluo:cron-nestjs-migration: category-switches -->
|
|
12
|
+
| Migration proposition | Governed fluo rule |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `timezone-mapping` | `timeZone`은 `timezone`으로 매핑되며 `CronTaskOptions.timezone`은 문자열입니다. |
|
|
15
|
+
| `wait-for-completion` | `protect: true`가 Croner 호출의 중복을 막고 `CronLifecycleService`는 작업 실행 중 tick을 거부합니다. |
|
|
16
|
+
| `unsupported-options` | 문서화된 fluo 옵션 외 NestJS scheduler 옵션은 지원되지 않습니다. |
|
|
17
|
+
| `absolute-time` | `@Cron`은 cron-expression 문자열만 받고 `Date`와 `DateTime` overload는 지원되지 않습니다. |
|
|
18
|
+
| `named-interval-timeout` | `@Interval(ms, options)`와 `@Timeout(ms, options)`는 millisecond와 선택적 named task option을 받습니다. |
|
|
19
|
+
| `async-configuration` | `CronModule.forRoot(...)`는 동기식이며 async configuration은 호출 전에 해석합니다. |
|
|
20
|
+
| `global-visibility` | `CronModule.forRoot(...)`는 기본적으로 local이고 필요할 때 `global: true`를 명시합니다. |
|
|
21
|
+
| `category-switches` | `cronJobs`, `intervals`, `timeouts` category switch는 지원되지 않습니다. |
|
|
4
22
|
|
|
5
23
|
fluo 애플리케이션을 위한 데코레이터 기반 스케줄링 패키지입니다. 앱 라이프사이클에 맞춰 시작/종료를 관리하고, Redis 기반 분산 락(Distributed Locking) 기능을 제공합니다.
|
|
6
24
|
|
|
7
25
|
## 목차
|
|
8
26
|
|
|
9
27
|
- [설치](#설치)
|
|
28
|
+
- [Node.js 호환성](#nodejs-호환성)
|
|
10
29
|
- [사용 시점](#사용-시점)
|
|
11
30
|
- [빠른 시작](#빠른-시작)
|
|
12
31
|
- [공통 패턴](#공통-패턴)
|
|
@@ -28,6 +47,14 @@ npm install @fluojs/cron
|
|
|
28
47
|
|
|
29
48
|
`@fluojs/redis`는 Redis distributed locking을 활성화할 때만 필요합니다. Non-distributed scheduling 경로는 package import, module registration, bootstrap, status snapshot 생성 중 Redis integration을 로드하지 않습니다.
|
|
30
49
|
|
|
50
|
+
## Node.js 호환성
|
|
51
|
+
|
|
52
|
+
`@fluojs/cron`은 패키지 자체의 지원 정책으로 Node.js `>=24.0.0 <27`을 요구합니다. Node.js 24 미만과 Node.js 27+는 지원하지 않습니다. Portable `@fluojs/runtime`의 engine omission은 별도 계약입니다.
|
|
53
|
+
|
|
54
|
+
### 3으로 마이그레이션
|
|
55
|
+
|
|
56
|
+
`@fluojs/cron` 2에서 업그레이드하기 전에 배포 host를 Node.js `>=24.0.0 <27`로 옮기세요. `@fluojs/cron` 3을 설치하기 전에 제거된 Node.js version을 사용하는 runtime image, CI matrix, local development environment를 업데이트해야 합니다.
|
|
57
|
+
|
|
31
58
|
## 사용 시점
|
|
32
59
|
|
|
33
60
|
- 정기적인 백그라운드 작업(예: 데이터베이스 정리, 리포트 생성)이 필요할 때 사용합니다.
|
|
@@ -90,6 +117,18 @@ NestJS `@Cron()` 옵션은 `CronTaskOptions`에 그대로 전달할 수 없습
|
|
|
90
117
|
|
|
91
118
|
이 guard는 한 application process 안의 같은 task instance만 보호합니다. 여러 application instance가 같은 task를 동시에 실행하지 않아야 한다면 [분산 락 사용하기](#분산-락-사용하기)를 적용하세요.
|
|
92
119
|
|
|
120
|
+
나머지 NestJS scheduling option에는 `CronTaskOptions` 대응 항목이 없습니다. `utcOffset`, `unrefTimeout`, `disabled`, `threshold`, `initialDelay`을 복사하지 마세요. fluo는 이를 받거나 흉내 내지 않습니다. Disabled 또는 category별 schedule은 해당 provider/module을 생략하는 application composition으로 소유하거나, application-owned condition이 만족된 뒤에만 dynamic task를 등록하세요. Threshold/recovery policy도 application-owned work로 다루세요. `@Timeout(ms, ...)`은 application startup 기준 지연이 의도한 동작일 때만 사용합니다. NestJS `@Cron(Date)` 또는 `@Cron(DateTime)`의 absolute-time 대체 수단이 아닙니다.
|
|
121
|
+
|
|
122
|
+
| NestJS scheduling form | fluo 마이그레이션 |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `@Cron(expression, { timeZone })` | `@Cron(expression, { timezone })` |
|
|
125
|
+
| `@Cron(Date)` 또는 `@Cron(DateTime)` | 직접 대응 없음. Application code에서 absolute-time plan을 해석하고 startup-relative `@Timeout`으로 대체하지 마세요. |
|
|
126
|
+
| `@Interval('name', ms)` | `@Interval(ms, { name: 'name' })` |
|
|
127
|
+
| `@Timeout('name', ms)` | `@Timeout(ms, { name: 'name' })` |
|
|
128
|
+
| `ScheduleModule.forRootAsync(...)` | Application bootstrap/composition boundary에서 async configuration을 해석한 뒤 동기 `CronModule.forRoot(...)`를 호출합니다. |
|
|
129
|
+
| NestJS의 global-by-default schedule registration | Global visibility가 필요하면 `CronModule.forRoot({ global: true })`를 사용합니다. fluo 기본값은 `false`입니다. |
|
|
130
|
+
| `cronJobs`, `intervals`, `timeouts` category switch | 직접 대응 없음. Application에서 conditional provider/module composition 또는 명시적 dynamic registration을 소유하세요. |
|
|
131
|
+
|
|
93
132
|
### 분산 락 사용하기
|
|
94
133
|
|
|
95
134
|
여러 서버 인스턴스에서 스케줄링된 작업이 동시에 실행되는 것을 방지하려면 분산 모드를 활성화하세요. 이 기능은 `@fluojs/redis`가 필요하며, Redis peer는 `distributed.enabled`가 `true`일 때만 로드되고 resolve됩니다.
|
|
@@ -118,7 +157,7 @@ class AppModule {}
|
|
|
118
157
|
|
|
119
158
|
`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`도 포함됩니다.
|
|
120
159
|
|
|
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을 함께 사용해야 합니다.
|
|
160
|
+
각 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 상태로 들어갈 수 없습니다. 성공한 acquisition마다 이 owner identity에서 고유한 lease token을 만들고 renewal과 release는 정확히 그 token을 비교합니다. 따라서 만료된 이전 acquisition의 늦은 release는 두 acquisition이 같은 configured `ownerId`를 재사용해도 더 새로운 lease를 삭제할 수 없습니다. 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을 함께 사용해야 합니다.
|
|
122
161
|
|
|
123
162
|
```typescript
|
|
124
163
|
@Module({
|
|
@@ -168,13 +207,13 @@ class TaskManager {
|
|
|
168
207
|
|
|
169
208
|
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에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
|
|
170
209
|
|
|
171
|
-
Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. 이전 scheduled handle의 stop이
|
|
210
|
+
Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression 또는 interval cadence update도 rollback-safe합니다. Provisional replacement는 이전 scheduled handle의 stop이 성공하고 registry가 새 handle token을 commit할 때까지 tick을 실행할 수 없으며, retired handle이 이미 queue한 callback도 무시됩니다. Replacement scheduling이 실패하거나 이전 handle을 stop할 수 없으면 fluo는 provisional replacement를 stop하고 이전 expression 또는 interval milliseconds와 handle을 복원한 뒤 failure를 다시 throw하므로 duplicate schedule을 조용히 남기지 않습니다. Active task를 disable 또는 remove할 때도 `stop()`이 성공한 뒤에만 scheduler handle을 지웁니다. Stop failure는 log로 드러나고 handle은 안전한 retry를 위해 registry에 남으며 operation은 `false`를 반환합니다. 실패한 `disable()`은 task descriptor를 disabled 상태로 두어 다음 tick을 계속 차단하고, 이후 disable 또는 shutdown이 cleanup을 재시도합니다. 실패한 `remove()`는 이후 removal이 성공할 때까지 task를 유지합니다. 첫 shutdown cleanup 시도도 실패하면 다음 shutdown lifecycle hook이 같은 retained handle을 다시 시도하고, `stop()`이 성공한 뒤에만 handle을 지웁니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
|
|
172
211
|
|
|
173
212
|
### 제한된 종료
|
|
174
213
|
|
|
175
214
|
`CronModule`은 애플리케이션 종료 시 실행 중인 작업을 제한된 타임아웃 안에서 drain합니다. 따라서 하나의 hung task 때문에 프로세스 종료가 영원히 막히지 않습니다.
|
|
176
215
|
|
|
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을 지우지 않습니다. 이렇게 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
|
|
216
|
+
Shutdown은 scheduled handle을 stop하기 전에 tick admission을 닫습니다. 따라서 이미 queue된 callback도 drain이 시작된 뒤에는 task body를 시작하거나 distributed lease를 획득할 수 없습니다. 기본적으로 shutdown drain은 최대 `10_000ms` 동안 기다립니다. 이 시간이 지나면 스케줄러는 경고 로그를 남기고 hung task가 끝나기를 더 기다리지 않은 채 종료를 계속합니다. 같은 `shutdown.timeoutMs` 경계는 shutdown 중 Redis owned-lock release I/O에도 적용되므로, 멈춘 Redis release가 process termination을 무기한 막지 못합니다. Task의 post-run `finally` release와 즉시 이어지는 stopped-state retry는 shutdown 시작 시 설정되어 `shutdown.timeoutMs` 뒤에 만료되는 deadline의 남은 시간만 사용합니다. 따라서 deadline 뒤에 task가 settle되어도 새 release window를 열지 않습니다. 분산 락을 사용하는 경우 아직 실행 중인 작업이 보유한 락은 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을 지우지 않습니다. 이렇게 원래 작업이 아직 실행 중인데 다른 노드가 같은 작업을 시작하지 않도록 합니다.
|
|
178
217
|
|
|
179
218
|
```typescript
|
|
180
219
|
@Module({
|
package/README.md
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
# @fluojs/cron
|
|
2
2
|
|
|
3
3
|
<p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
|
|
4
|
+
<!-- fluo:cron-nestjs-migration: timezone-mapping -->
|
|
5
|
+
<!-- fluo:cron-nestjs-migration: wait-for-completion -->
|
|
6
|
+
<!-- fluo:cron-nestjs-migration: unsupported-options -->
|
|
7
|
+
<!-- fluo:cron-nestjs-migration: absolute-time -->
|
|
8
|
+
<!-- fluo:cron-nestjs-migration: named-interval-timeout -->
|
|
9
|
+
<!-- fluo:cron-nestjs-migration: async-configuration -->
|
|
10
|
+
<!-- fluo:cron-nestjs-migration: global-visibility -->
|
|
11
|
+
<!-- fluo:cron-nestjs-migration: category-switches -->
|
|
12
|
+
| Migration proposition | Governed fluo rule |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `timezone-mapping` | `timeZone` maps to `timezone`; `CronTaskOptions.timezone` is a string. |
|
|
15
|
+
| `wait-for-completion` | `protect: true` prevents overlapping Croner invocations, and `CronLifecycleService` rejects a tick while its task is running. |
|
|
16
|
+
| `unsupported-options` | NestJS scheduler options other than the documented fluo options are unsupported. |
|
|
17
|
+
| `absolute-time` | `@Cron` accepts a cron-expression string only; `Date` and `DateTime` overloads are unsupported. |
|
|
18
|
+
| `named-interval-timeout` | `@Interval(ms, options)` and `@Timeout(ms, options)` accept milliseconds and optional named task options. |
|
|
19
|
+
| `async-configuration` | `CronModule.forRoot(...)` is synchronous; resolve async configuration before calling it. |
|
|
20
|
+
| `global-visibility` | `CronModule.forRoot(...)` is local by default; pass `global: true` explicitly when needed. |
|
|
21
|
+
| `category-switches` | `cronJobs`, `intervals`, and `timeouts` category switches are unsupported. |
|
|
4
22
|
|
|
5
23
|
Decorator-based scheduling for fluo applications with lifecycle-managed startup/shutdown and optional Redis distributed locking.
|
|
6
24
|
|
|
7
25
|
## Table of Contents
|
|
8
26
|
|
|
9
27
|
- [Installation](#installation)
|
|
28
|
+
- [Node.js Compatibility](#nodejs-compatibility)
|
|
10
29
|
- [When to Use](#when-to-use)
|
|
11
30
|
- [Quick Start](#quick-start)
|
|
12
31
|
- [Common Patterns](#common-patterns)
|
|
@@ -28,6 +47,14 @@ npm install @fluojs/cron
|
|
|
28
47
|
|
|
29
48
|
`@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.
|
|
30
49
|
|
|
50
|
+
## Node.js Compatibility
|
|
51
|
+
|
|
52
|
+
`@fluojs/cron` requires Node.js `>=24.0.0 <27` as its package-owned support policy. Node.js versions below 24 and Node.js 27+ are not supported. The portable `@fluojs/runtime` engine omission is a separate contract.
|
|
53
|
+
|
|
54
|
+
### Migration to 3
|
|
55
|
+
|
|
56
|
+
Before upgrading from `@fluojs/cron` 2, move deployment hosts to Node.js `>=24.0.0 <27`. Update any runtime image, CI matrix, or local development environment that still uses a removed Node.js version before installing `@fluojs/cron` 3.
|
|
57
|
+
|
|
31
58
|
## When to Use
|
|
32
59
|
|
|
33
60
|
- When you need to run periodic background tasks (e.g., database cleanup, report generation).
|
|
@@ -90,6 +117,18 @@ Do not copy `waitForCompletion` or invent an overlap flag. fluo does not expose
|
|
|
90
117
|
|
|
91
118
|
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
119
|
|
|
120
|
+
The remaining NestJS scheduling options have no `CronTaskOptions` equivalent. Do not copy `utcOffset`, `unrefTimeout`, `disabled`, `threshold`, or `initialDelay`: fluo does not accept or emulate them. Make disabled or category-specific schedules part of application composition by omitting the provider/module that declares them, or register a dynamic task only after the application-owned condition is satisfied. Treat threshold/recovery policy as application-owned work. Use `@Timeout(ms, ...)` only when a delay relative to application startup is the intended behavior; it is not an absolute-time replacement for NestJS `@Cron(Date)` or `@Cron(DateTime)`.
|
|
121
|
+
|
|
122
|
+
| NestJS scheduling form | fluo migration |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `@Cron(expression, { timeZone })` | `@Cron(expression, { timezone })` |
|
|
125
|
+
| `@Cron(Date)` or `@Cron(DateTime)` | No direct equivalent. Resolve the absolute-time plan in application code; do not substitute a startup-relative `@Timeout`. |
|
|
126
|
+
| `@Interval('name', ms)` | `@Interval(ms, { name: 'name' })` |
|
|
127
|
+
| `@Timeout('name', ms)` | `@Timeout(ms, { name: 'name' })` |
|
|
128
|
+
| `ScheduleModule.forRootAsync(...)` | Resolve async configuration at the application bootstrap/composition boundary, then call synchronous `CronModule.forRoot(...)`. |
|
|
129
|
+
| NestJS global-by-default schedule registration | `CronModule.forRoot({ global: true })` when global visibility is required; fluo defaults to `false`. |
|
|
130
|
+
| `cronJobs`, `intervals`, or `timeouts` category switches | No direct equivalent. Own the conditional provider/module composition or explicit dynamic registration in the application. |
|
|
131
|
+
|
|
93
132
|
### Distributed Locking
|
|
94
133
|
|
|
95
134
|
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`.
|
|
@@ -118,7 +157,7 @@ Leave `distributed.clientName` unset to keep using the default Redis registratio
|
|
|
118
157
|
|
|
119
158
|
`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.
|
|
120
159
|
|
|
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.
|
|
160
|
+
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. Every successful acquisition derives a unique lease token from that owner identity, and renewal and release compare the exact token. A delayed release from an expired acquisition therefore cannot delete a newer lease even when both acquisitions reuse the same configured `ownerId`. 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.
|
|
122
161
|
|
|
123
162
|
```typescript
|
|
124
163
|
@Module({
|
|
@@ -168,13 +207,13 @@ class TaskManager {
|
|
|
168
207
|
|
|
169
208
|
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.
|
|
170
209
|
|
|
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
|
|
210
|
+
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 provisional replacement remains tick-ineligible until the previous scheduled handle stops successfully and the registry commits the new handle token; callbacks already queued by a retired handle are ignored. 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. Disabling or removing an active task clears its scheduler handle only after `stop()` succeeds. A stop failure is logged, the handle remains registered for a safe retry, and the operation returns `false`. A failed `disable()` leaves the task descriptor disabled so later ticks stay gated while another disable or shutdown retries cleanup; a failed `remove()` keeps the task registered until a later removal succeeds. If the first shutdown cleanup attempt also fails, the next shutdown lifecycle hook retries the same retained handle and clears it only after `stop()` succeeds. 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.
|
|
172
211
|
|
|
173
212
|
### Bounded Shutdown
|
|
174
213
|
|
|
175
214
|
`CronModule` drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.
|
|
176
215
|
|
|
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.
|
|
216
|
+
Shutdown closes tick admission before scheduled handles are stopped, so callbacks that were already queued cannot start task bodies or acquire distributed leases after draining begins. 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. A task's post-run `finally` release and any immediate stopped-state retry use only the time remaining on the deadline established when shutdown starts, which expires after `shutdown.timeoutMs`; a task that settles after that deadline does not open a new release window. 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.
|
|
178
217
|
|
|
179
218
|
```typescript
|
|
180
219
|
@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;AA4CrD;;;;;;;;;;;;;;;;;;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
|
@@ -8,7 +8,7 @@ function getStandardMetadataBag(metadata) {
|
|
|
8
8
|
function defineStandardSchedulingMetadata(metadata, propertyKey, taskMetadata) {
|
|
9
9
|
const bag = getStandardMetadataBag(metadata);
|
|
10
10
|
const current = bag[schedulingMetadataSymbol];
|
|
11
|
-
const map = current
|
|
11
|
+
const map = Object.hasOwn(bag, schedulingMetadataSymbol) && current !== undefined ? current : new Map(current);
|
|
12
12
|
map.set(propertyKey, {
|
|
13
13
|
...taskMetadata,
|
|
14
14
|
options: {
|
|
@@ -11,12 +11,17 @@ export interface LockRenewalMonitor {
|
|
|
11
11
|
getPostRunError(): Promise<Error | undefined>;
|
|
12
12
|
stop(): void;
|
|
13
13
|
}
|
|
14
|
+
interface DistributedLockLease {
|
|
15
|
+
readonly lockKey: string;
|
|
16
|
+
readonly taskName: string;
|
|
17
|
+
readonly token: string;
|
|
18
|
+
}
|
|
14
19
|
/** Coordinates Redis lock acquisition, renewal, and release for scheduled cron tasks. */
|
|
15
20
|
export declare class CronDistributedLockManager {
|
|
16
21
|
private readonly options;
|
|
17
22
|
private readonly runtimeContainer;
|
|
18
23
|
private readonly logger;
|
|
19
|
-
private readonly
|
|
24
|
+
private readonly ownedLeases;
|
|
20
25
|
private lockIoError;
|
|
21
26
|
private redisClient;
|
|
22
27
|
private lockOwnershipLosses;
|
|
@@ -29,18 +34,20 @@ export declare class CronDistributedLockManager {
|
|
|
29
34
|
get renewalFailures(): number;
|
|
30
35
|
resolveClient(): Promise<void>;
|
|
31
36
|
reset(): void;
|
|
32
|
-
tryAcquireLock(descriptor: CronTaskDescriptor): Promise<
|
|
33
|
-
startLockRenewalMonitor(descriptor: CronTaskDescriptor): LockRenewalMonitor;
|
|
34
|
-
releaseLock(
|
|
37
|
+
tryAcquireLock(descriptor: CronTaskDescriptor): Promise<DistributedLockLease | undefined>;
|
|
38
|
+
startLockRenewalMonitor(descriptor: CronTaskDescriptor, lease: DistributedLockLease): LockRenewalMonitor;
|
|
39
|
+
releaseLock(lease: DistributedLockLease, timeoutMs?: number): Promise<boolean>;
|
|
35
40
|
releaseOwnedLocks(excludedLockKeys?: ReadonlySet<string>, timeoutMs?: number): Promise<void>;
|
|
36
41
|
private createLockRenewalState;
|
|
37
42
|
private queueDueLockRenewalAttempts;
|
|
38
43
|
private runLockRenewalAttempt;
|
|
39
44
|
private toLockPostRunError;
|
|
40
45
|
private renewLock;
|
|
41
|
-
private
|
|
46
|
+
private releaseLease;
|
|
47
|
+
private clearOwnedLease;
|
|
42
48
|
private verifyLockIoAvailability;
|
|
43
49
|
private markLockIoAvailable;
|
|
44
50
|
private markLockIoUnavailable;
|
|
45
51
|
}
|
|
52
|
+
export {};
|
|
46
53
|
//# 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;
|
|
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;AAGzD,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;AAWD,UAAU,oBAAoB;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAuED,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,WAAW,CAA2C;IACvE,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,oBAAoB,GAAG,SAAS,CAAC;IA6C/F,uBAAuB,CAAC,UAAU,EAAE,kBAAkB,EAAE,KAAK,EAAE,oBAAoB,GAAG,kBAAkB;IA8BlG,WAAW,CAAC,KAAK,EAAE,oBAAoB,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAI9E,iBAAiB,CAAC,gBAAgB,GAAE,WAAW,CAAC,MAAM,CAAa,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkB7G,OAAO,CAAC,sBAAsB;IAa9B,OAAO,CAAC,2BAA2B;YAcrB,qBAAqB;IAqBnC,OAAO,CAAC,kBAAkB;YAYZ,SAAS;YA2CT,YAAY;IAyC1B,OAAO,CAAC,eAAe;YAMT,wBAAwB;IAoBtC,OAAO,CAAC,mBAAmB;IAI3B,OAAO,CAAC,qBAAqB;CAG9B"}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createCronRandomId } from './random-id.js';
|
|
2
|
+
|
|
1
3
|
/** Minimal Redis command surface required for distributed cron locks. */
|
|
2
4
|
|
|
3
5
|
/** Tracks renewal state for one acquired distributed cron lock. */
|
|
@@ -49,7 +51,7 @@ async function resolveRedisPeerModule() {
|
|
|
49
51
|
|
|
50
52
|
/** Coordinates Redis lock acquisition, renewal, and release for scheduled cron tasks. */
|
|
51
53
|
export class CronDistributedLockManager {
|
|
52
|
-
|
|
54
|
+
ownedLeases = new Map();
|
|
53
55
|
lockIoError;
|
|
54
56
|
redisClient;
|
|
55
57
|
lockOwnershipLosses = 0;
|
|
@@ -63,7 +65,7 @@ export class CronDistributedLockManager {
|
|
|
63
65
|
return this.redisClient;
|
|
64
66
|
}
|
|
65
67
|
get ownedLocks() {
|
|
66
|
-
return this.
|
|
68
|
+
return this.ownedLeases.size;
|
|
67
69
|
}
|
|
68
70
|
get lockIoAvailable() {
|
|
69
71
|
if (!this.options.distributed.enabled) {
|
|
@@ -102,23 +104,33 @@ export class CronDistributedLockManager {
|
|
|
102
104
|
async tryAcquireLock(descriptor) {
|
|
103
105
|
const redis = this.redisClient;
|
|
104
106
|
if (!redis) {
|
|
105
|
-
return
|
|
107
|
+
return {
|
|
108
|
+
lockKey: descriptor.lockKey,
|
|
109
|
+
taskName: descriptor.taskName,
|
|
110
|
+
token: `${this.options.distributed.ownerId}:${createCronRandomId()}`
|
|
111
|
+
};
|
|
106
112
|
}
|
|
113
|
+
const lease = {
|
|
114
|
+
lockKey: descriptor.lockKey,
|
|
115
|
+
taskName: descriptor.taskName,
|
|
116
|
+
token: `${this.options.distributed.ownerId}:${createCronRandomId()}`
|
|
117
|
+
};
|
|
107
118
|
try {
|
|
108
|
-
const result = await redis.set(descriptor.lockKey,
|
|
119
|
+
const result = await redis.set(descriptor.lockKey, lease.token, 'PX', descriptor.lockTtlMs, 'NX');
|
|
109
120
|
this.markLockIoAvailable();
|
|
110
121
|
if (result === 'OK') {
|
|
111
|
-
this.
|
|
122
|
+
this.ownedLeases.set(descriptor.lockKey, lease);
|
|
123
|
+
return lease;
|
|
112
124
|
}
|
|
113
|
-
return
|
|
125
|
+
return undefined;
|
|
114
126
|
} catch (error) {
|
|
115
127
|
this.markLockIoUnavailable(error);
|
|
116
128
|
this.logger.error(`Failed to acquire distributed cron lock for ${descriptor.taskName}.`, error, 'CronLifecycleService');
|
|
117
|
-
return
|
|
129
|
+
return undefined;
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
|
-
startLockRenewalMonitor(descriptor) {
|
|
121
|
-
const renewalState = this.createLockRenewalState(descriptor.lockTtlMs);
|
|
132
|
+
startLockRenewalMonitor(descriptor, lease) {
|
|
133
|
+
const renewalState = this.createLockRenewalState(descriptor.lockTtlMs, lease);
|
|
122
134
|
const renewalTimer = setInterval(() => {
|
|
123
135
|
if (renewalState.stopped) {
|
|
124
136
|
return;
|
|
@@ -143,24 +155,28 @@ export class CronDistributedLockManager {
|
|
|
143
155
|
}
|
|
144
156
|
};
|
|
145
157
|
}
|
|
146
|
-
async releaseLock(
|
|
147
|
-
return await this.
|
|
158
|
+
async releaseLock(lease, timeoutMs) {
|
|
159
|
+
return await this.releaseLease(lease, timeoutMs);
|
|
148
160
|
}
|
|
149
161
|
async releaseOwnedLocks(excludedLockKeys = new Set(), timeoutMs) {
|
|
150
|
-
if (!this.redisClient || this.
|
|
162
|
+
if (!this.redisClient || this.ownedLeases.size === 0) {
|
|
151
163
|
return;
|
|
152
164
|
}
|
|
153
|
-
const
|
|
154
|
-
if (
|
|
165
|
+
const leases = Array.from(this.ownedLeases.values()).filter(lease => !excludedLockKeys.has(lease.lockKey));
|
|
166
|
+
if (leases.length === 0) {
|
|
155
167
|
return;
|
|
156
168
|
}
|
|
157
|
-
await Promise.all(
|
|
158
|
-
await this.
|
|
169
|
+
await Promise.all(leases.map(async lease => {
|
|
170
|
+
await this.releaseLease({
|
|
171
|
+
...lease,
|
|
172
|
+
taskName: lease.lockKey
|
|
173
|
+
}, timeoutMs);
|
|
159
174
|
}));
|
|
160
175
|
}
|
|
161
|
-
createLockRenewalState(lockTtlMs) {
|
|
176
|
+
createLockRenewalState(lockTtlMs, lease) {
|
|
162
177
|
const renewalIntervalMs = Math.max(250, Math.floor(lockTtlMs / 2));
|
|
163
178
|
return {
|
|
179
|
+
lease,
|
|
164
180
|
lockPostRunError: undefined,
|
|
165
181
|
nextRenewalDueAt: Date.now() + renewalIntervalMs,
|
|
166
182
|
renewalChain: Promise.resolve(),
|
|
@@ -178,7 +194,7 @@ export class CronDistributedLockManager {
|
|
|
178
194
|
}
|
|
179
195
|
}
|
|
180
196
|
async runLockRenewalAttempt(descriptor, renewalState) {
|
|
181
|
-
const outcome = await this.renewLock(descriptor);
|
|
197
|
+
const outcome = await this.renewLock(descriptor, renewalState.lease);
|
|
182
198
|
if (outcome === 'ownership-lost') {
|
|
183
199
|
this.lockOwnershipLosses += 1;
|
|
184
200
|
}
|
|
@@ -199,13 +215,13 @@ export class CronDistributedLockManager {
|
|
|
199
215
|
}
|
|
200
216
|
return undefined;
|
|
201
217
|
}
|
|
202
|
-
async renewLock(descriptor) {
|
|
218
|
+
async renewLock(descriptor, lease) {
|
|
203
219
|
const redis = this.redisClient;
|
|
204
220
|
if (!redis) {
|
|
205
221
|
return 'renewed';
|
|
206
222
|
}
|
|
207
223
|
try {
|
|
208
|
-
const result = await redis.eval(RENEW_LOCK_SCRIPT, 1, descriptor.lockKey,
|
|
224
|
+
const result = await redis.eval(RENEW_LOCK_SCRIPT, 1, descriptor.lockKey, lease.token, String(descriptor.lockTtlMs));
|
|
209
225
|
if (typeof result === 'number' && result <= 0) {
|
|
210
226
|
this.markLockIoAvailable();
|
|
211
227
|
this.logger.warn(`Distributed cron lock ownership was lost for ${descriptor.taskName}.`, 'CronLifecycleService');
|
|
@@ -220,38 +236,44 @@ export class CronDistributedLockManager {
|
|
|
220
236
|
return 'renewal-failed';
|
|
221
237
|
}
|
|
222
238
|
}
|
|
223
|
-
async
|
|
239
|
+
async releaseLease(lease, timeoutMs) {
|
|
224
240
|
const redis = this.redisClient;
|
|
225
|
-
if (!redis) {
|
|
226
|
-
return
|
|
241
|
+
if (!redis || timeoutMs !== undefined && timeoutMs <= 0) {
|
|
242
|
+
return redis === undefined;
|
|
227
243
|
}
|
|
228
244
|
try {
|
|
229
|
-
const result = await withTimeout(redis.eval(RELEASE_LOCK_SCRIPT, 1, lockKey,
|
|
245
|
+
const result = await withTimeout(redis.eval(RELEASE_LOCK_SCRIPT, 1, lease.lockKey, lease.token), timeoutMs);
|
|
230
246
|
if (typeof result === 'number' && result <= 0) {
|
|
231
247
|
this.markLockIoAvailable();
|
|
232
|
-
this.logger.warn(`Distributed cron lock for ${taskName} was already released or owned by another node.`, 'CronLifecycleService');
|
|
233
|
-
this.
|
|
248
|
+
this.logger.warn(`Distributed cron lock for ${lease.taskName} was already released or owned by another node.`, 'CronLifecycleService');
|
|
249
|
+
this.clearOwnedLease(lease);
|
|
234
250
|
return true;
|
|
235
251
|
}
|
|
236
252
|
this.markLockIoAvailable();
|
|
237
|
-
this.logger.log(`Released distributed cron lock for ${taskName}.`, 'CronLifecycleService');
|
|
238
|
-
this.
|
|
253
|
+
this.logger.log(`Released distributed cron lock for ${lease.taskName}.`, 'CronLifecycleService');
|
|
254
|
+
this.clearOwnedLease(lease);
|
|
239
255
|
return true;
|
|
240
256
|
} catch (error) {
|
|
241
257
|
this.markLockIoUnavailable(error);
|
|
242
|
-
this.logger.error(`Failed to release distributed cron lock for ${taskName}.`, error, 'CronLifecycleService');
|
|
258
|
+
this.logger.error(`Failed to release distributed cron lock for ${lease.taskName}.`, error, 'CronLifecycleService');
|
|
243
259
|
return false;
|
|
244
260
|
}
|
|
245
261
|
}
|
|
262
|
+
clearOwnedLease(lease) {
|
|
263
|
+
if (this.ownedLeases.get(lease.lockKey)?.token === lease.token) {
|
|
264
|
+
this.ownedLeases.delete(lease.lockKey);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
246
267
|
async verifyLockIoAvailability() {
|
|
247
268
|
const redis = this.redisClient;
|
|
248
269
|
if (!redis) {
|
|
249
270
|
return;
|
|
250
271
|
}
|
|
251
|
-
const
|
|
272
|
+
const probeToken = `${this.options.distributed.ownerId}:${createCronRandomId()}`;
|
|
273
|
+
const probeKey = `${this.options.distributed.keyPrefix}:__probe:${probeToken}`;
|
|
252
274
|
try {
|
|
253
|
-
await redis.set(probeKey,
|
|
254
|
-
await redis.eval(RELEASE_LOCK_SCRIPT, 1, probeKey,
|
|
275
|
+
await redis.set(probeKey, probeToken, 'PX', 1_000, 'NX');
|
|
276
|
+
await redis.eval(RELEASE_LOCK_SCRIPT, 1, probeKey, probeToken);
|
|
255
277
|
this.markLockIoAvailable();
|
|
256
278
|
} catch (error) {
|
|
257
279
|
this.markLockIoUnavailable(error);
|
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;
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAMhE,OAAO,KAAK,EAAE,iBAAiB,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AA0FjF;;;;;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,15 +1,9 @@
|
|
|
1
1
|
import { defineModule } from '@fluojs/runtime';
|
|
2
|
+
import { createCronRandomId } from './random-id.js';
|
|
2
3
|
import { defaultCronScheduler } from './scheduler.js';
|
|
3
4
|
import { CronLifecycleService } from './service.js';
|
|
4
5
|
import { CRON_OPTIONS, SCHEDULING_REGISTRY } from './tokens.js';
|
|
5
6
|
const DEFAULT_CRON_SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
6
|
-
function randomId() {
|
|
7
|
-
const randomUUID = globalThis.crypto?.randomUUID;
|
|
8
|
-
if (randomUUID) {
|
|
9
|
-
return randomUUID.call(globalThis.crypto);
|
|
10
|
-
}
|
|
11
|
-
return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
12
|
-
}
|
|
13
7
|
function normalizeRedisClientName(clientName) {
|
|
14
8
|
if (clientName === undefined) {
|
|
15
9
|
return undefined;
|
|
@@ -27,7 +21,7 @@ function assertValidDistributedLockTtlMs(lockTtlMs) {
|
|
|
27
21
|
}
|
|
28
22
|
function normalizeDistributedOwnerId(ownerId) {
|
|
29
23
|
if (ownerId === undefined) {
|
|
30
|
-
return
|
|
24
|
+
return createCronRandomId();
|
|
31
25
|
}
|
|
32
26
|
if (typeof ownerId !== 'string') {
|
|
33
27
|
throw new Error('Cron distributed ownerId must be a string when provided.');
|
|
@@ -45,7 +39,7 @@ function normalizeDistributedOptions(distributed) {
|
|
|
45
39
|
enabled: false,
|
|
46
40
|
keyPrefix: 'fluo:cron:lock',
|
|
47
41
|
lockTtlMs: 30_000,
|
|
48
|
-
ownerId:
|
|
42
|
+
ownerId: createCronRandomId()
|
|
49
43
|
};
|
|
50
44
|
}
|
|
51
45
|
if (distributed === true) {
|
|
@@ -54,7 +48,7 @@ function normalizeDistributedOptions(distributed) {
|
|
|
54
48
|
enabled: true,
|
|
55
49
|
keyPrefix: 'fluo:cron:lock',
|
|
56
50
|
lockTtlMs: 30_000,
|
|
57
|
-
ownerId:
|
|
51
|
+
ownerId: createCronRandomId()
|
|
58
52
|
};
|
|
59
53
|
}
|
|
60
54
|
const normalizedDistributed = {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random-id.d.ts","sourceRoot":"","sources":["../src/random-id.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,wBAAgB,kBAAkB,IAAI,MAAM,CAQ3C"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Creates a platform-neutral random identifier for Cron ownership boundaries. */
|
|
2
|
+
export function createCronRandomId() {
|
|
3
|
+
const randomUUID = globalThis.crypto?.randomUUID;
|
|
4
|
+
if (randomUUID) {
|
|
5
|
+
return randomUUID.call(globalThis.crypto);
|
|
6
|
+
}
|
|
7
|
+
return `cron-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
8
|
+
}
|
package/dist/service.d.ts
CHANGED
|
@@ -15,10 +15,11 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
15
15
|
private readonly logger;
|
|
16
16
|
private readonly tasks;
|
|
17
17
|
private readonly activeTasks;
|
|
18
|
-
private readonly
|
|
18
|
+
private readonly runningDistributedLeaseCounts;
|
|
19
19
|
private readonly distributedLocks;
|
|
20
20
|
private readonly taskRunner;
|
|
21
21
|
private lifecycleState;
|
|
22
|
+
private shutdownDeadlineMs;
|
|
22
23
|
private started;
|
|
23
24
|
private shutdownPromise;
|
|
24
25
|
constructor(options: NormalizedCronModuleOptions, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger);
|
|
@@ -53,7 +54,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
53
54
|
* Removes a registered task by name.
|
|
54
55
|
*
|
|
55
56
|
* @param name Task name to remove.
|
|
56
|
-
* @returns `true` when a task existed and was removed.
|
|
57
|
+
* @returns `true` when a task existed and was removed; `false` when it was absent or its handle could not stop.
|
|
57
58
|
*/
|
|
58
59
|
remove(name: string): boolean;
|
|
59
60
|
/**
|
|
@@ -67,7 +68,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
67
68
|
* Disables a task without removing its descriptor.
|
|
68
69
|
*
|
|
69
70
|
* @param name Task name to disable.
|
|
70
|
-
* @returns `true` when the task exists
|
|
71
|
+
* @returns `true` when the task exists and its handle stopped; `false` when it was absent or its handle could not stop.
|
|
71
72
|
*/
|
|
72
73
|
disable(name: string): boolean;
|
|
73
74
|
/**
|
|
@@ -112,6 +113,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
112
113
|
private completeStartupFailureCleanupAfterActiveTasks;
|
|
113
114
|
private resetDistributedLocksAfterStartupFailure;
|
|
114
115
|
private runShutdownLifecycle;
|
|
116
|
+
private getRemainingShutdownTimeoutMs;
|
|
115
117
|
private getRunningDistributedLockKeys;
|
|
116
118
|
private registerDecoratorTasks;
|
|
117
119
|
private registerTask;
|
|
@@ -123,6 +125,7 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
123
125
|
private unscheduleTask;
|
|
124
126
|
private stopScheduledHandle;
|
|
125
127
|
private handleTaskTick;
|
|
128
|
+
private isTaskTickCurrent;
|
|
126
129
|
private runTaskTick;
|
|
127
130
|
private shouldUseDistributedExecution;
|
|
128
131
|
private runDistributedTaskTick;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AAczB,OAAO,KAAK,EAEV,eAAe,EACf,mBAAmB,EACnB,2BAA2B,EAC3B,kBAAkB,EAClB,sBAAsB,EACtB,wBAAwB,EACxB,kBAAkB,EACnB,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
|
@@ -44,6 +44,9 @@ function createRedisDependencyId(name) {
|
|
|
44
44
|
}
|
|
45
45
|
return `redis.${normalizedName}`;
|
|
46
46
|
}
|
|
47
|
+
function getRemainingTimeoutMs(deadlineMs) {
|
|
48
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
49
|
+
}
|
|
47
50
|
|
|
48
51
|
/**
|
|
49
52
|
* Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
|
|
@@ -59,10 +62,11 @@ class CronLifecycleService {
|
|
|
59
62
|
}
|
|
60
63
|
tasks = new Map();
|
|
61
64
|
activeTasks = new Set();
|
|
62
|
-
|
|
65
|
+
runningDistributedLeaseCounts = new Map();
|
|
63
66
|
distributedLocks;
|
|
64
67
|
taskRunner;
|
|
65
68
|
lifecycleState = 'created';
|
|
69
|
+
shutdownDeadlineMs;
|
|
66
70
|
started = false;
|
|
67
71
|
shutdownPromise;
|
|
68
72
|
constructor(options, runtimeContainer, compiledModules, logger) {
|
|
@@ -157,14 +161,16 @@ class CronLifecycleService {
|
|
|
157
161
|
* Removes a registered task by name.
|
|
158
162
|
*
|
|
159
163
|
* @param name Task name to remove.
|
|
160
|
-
* @returns `true` when a task existed and was removed.
|
|
164
|
+
* @returns `true` when a task existed and was removed; `false` when it was absent or its handle could not stop.
|
|
161
165
|
*/
|
|
162
166
|
remove(name) {
|
|
163
167
|
const task = this.tasks.get(name);
|
|
164
168
|
if (!task) {
|
|
165
169
|
return false;
|
|
166
170
|
}
|
|
167
|
-
this.unscheduleTask(task)
|
|
171
|
+
if (!this.unscheduleTask(task)) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
168
174
|
this.tasks.delete(name);
|
|
169
175
|
return true;
|
|
170
176
|
}
|
|
@@ -201,7 +207,7 @@ class CronLifecycleService {
|
|
|
201
207
|
* Disables a task without removing its descriptor.
|
|
202
208
|
*
|
|
203
209
|
* @param name Task name to disable.
|
|
204
|
-
* @returns `true` when the task exists
|
|
210
|
+
* @returns `true` when the task exists and its handle stopped; `false` when it was absent or its handle could not stop.
|
|
205
211
|
*/
|
|
206
212
|
disable(name) {
|
|
207
213
|
const task = this.tasks.get(name);
|
|
@@ -212,8 +218,7 @@ class CronLifecycleService {
|
|
|
212
218
|
return true;
|
|
213
219
|
}
|
|
214
220
|
task.enabled = false;
|
|
215
|
-
this.unscheduleTask(task);
|
|
216
|
-
return true;
|
|
221
|
+
return this.unscheduleTask(task);
|
|
217
222
|
}
|
|
218
223
|
|
|
219
224
|
/**
|
|
@@ -258,6 +263,7 @@ class CronLifecycleService {
|
|
|
258
263
|
}
|
|
259
264
|
const previousExpression = task.descriptor.expression;
|
|
260
265
|
const previousHandle = task.scheduledHandle;
|
|
266
|
+
const previousToken = task.activeScheduleToken;
|
|
261
267
|
let nextHandle;
|
|
262
268
|
task.descriptor.expression = expression;
|
|
263
269
|
try {
|
|
@@ -266,12 +272,14 @@ class CronLifecycleService {
|
|
|
266
272
|
previousHandle.stop();
|
|
267
273
|
}
|
|
268
274
|
task.scheduledHandle = nextHandle;
|
|
275
|
+
task.activeScheduleToken = nextHandle.token;
|
|
269
276
|
} catch (error) {
|
|
270
277
|
if (nextHandle) {
|
|
271
278
|
this.stopScheduledHandle(nextHandle);
|
|
272
279
|
}
|
|
273
280
|
task.descriptor.expression = previousExpression;
|
|
274
281
|
task.scheduledHandle = previousHandle;
|
|
282
|
+
task.activeScheduleToken = previousToken;
|
|
275
283
|
throw error;
|
|
276
284
|
}
|
|
277
285
|
}
|
|
@@ -298,6 +306,7 @@ class CronLifecycleService {
|
|
|
298
306
|
}
|
|
299
307
|
const previousMs = task.descriptor.ms;
|
|
300
308
|
const previousHandle = task.scheduledHandle;
|
|
309
|
+
const previousToken = task.activeScheduleToken;
|
|
301
310
|
let nextHandle;
|
|
302
311
|
task.descriptor.ms = ms;
|
|
303
312
|
try {
|
|
@@ -306,12 +315,14 @@ class CronLifecycleService {
|
|
|
306
315
|
previousHandle.stop();
|
|
307
316
|
}
|
|
308
317
|
task.scheduledHandle = nextHandle;
|
|
318
|
+
task.activeScheduleToken = nextHandle.token;
|
|
309
319
|
} catch (error) {
|
|
310
320
|
if (nextHandle) {
|
|
311
321
|
this.stopScheduledHandle(nextHandle);
|
|
312
322
|
}
|
|
313
323
|
task.descriptor.ms = previousMs;
|
|
314
324
|
task.scheduledHandle = previousHandle;
|
|
325
|
+
task.activeScheduleToken = previousToken;
|
|
315
326
|
throw error;
|
|
316
327
|
}
|
|
317
328
|
}
|
|
@@ -381,9 +392,11 @@ class CronLifecycleService {
|
|
|
381
392
|
async shutdown() {
|
|
382
393
|
if (this.shutdownPromise) {
|
|
383
394
|
await this.shutdownPromise;
|
|
395
|
+
this.stopAllScheduledTasks();
|
|
384
396
|
await this.retryReleasedDistributedLocksAfterShutdown();
|
|
385
397
|
return;
|
|
386
398
|
}
|
|
399
|
+
this.shutdownDeadlineMs = Date.now() + this.options.shutdown.timeoutMs;
|
|
387
400
|
this.shutdownPromise = this.runShutdownLifecycle();
|
|
388
401
|
await this.shutdownPromise;
|
|
389
402
|
}
|
|
@@ -391,7 +404,7 @@ class CronLifecycleService {
|
|
|
391
404
|
if (this.lifecycleState !== 'stopped' || this.activeTasks.size > 0) {
|
|
392
405
|
return;
|
|
393
406
|
}
|
|
394
|
-
await this.distributedLocks.releaseOwnedLocks(new Set(), this.
|
|
407
|
+
await this.distributedLocks.releaseOwnedLocks(new Set(), this.getRemainingShutdownTimeoutMs());
|
|
395
408
|
}
|
|
396
409
|
async startLifecycle() {
|
|
397
410
|
this.validateDistributedLockConfiguration();
|
|
@@ -439,11 +452,14 @@ class CronLifecycleService {
|
|
|
439
452
|
if (shutdownTimedOut) {
|
|
440
453
|
this.logger.warn(`Cron shutdown timed out after ${String(this.options.shutdown.timeoutMs)}ms with ${String(this.activeTasks.size)} active task(s) still pending.`, 'CronLifecycleService');
|
|
441
454
|
}
|
|
442
|
-
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.
|
|
455
|
+
await this.distributedLocks.releaseOwnedLocks(shutdownTimedOut ? this.getRunningDistributedLockKeys() : new Set(), this.getRemainingShutdownTimeoutMs());
|
|
443
456
|
this.lifecycleState = 'stopped';
|
|
444
457
|
}
|
|
458
|
+
getRemainingShutdownTimeoutMs() {
|
|
459
|
+
return this.shutdownDeadlineMs === undefined ? this.options.shutdown.timeoutMs : getRemainingTimeoutMs(this.shutdownDeadlineMs);
|
|
460
|
+
}
|
|
445
461
|
getRunningDistributedLockKeys() {
|
|
446
|
-
return new Set(this.
|
|
462
|
+
return new Set(this.runningDistributedLeaseCounts.keys());
|
|
447
463
|
}
|
|
448
464
|
registerDecoratorTasks() {
|
|
449
465
|
const descriptors = discoverCronTaskDescriptors(this.compiledModules, this.options, this.logger);
|
|
@@ -458,6 +474,7 @@ class CronLifecycleService {
|
|
|
458
474
|
assertValidLockTtlMs(descriptor.lockTtlMs);
|
|
459
475
|
}
|
|
460
476
|
const task = {
|
|
477
|
+
activeScheduleToken: undefined,
|
|
461
478
|
descriptor,
|
|
462
479
|
enabled: true,
|
|
463
480
|
running: false,
|
|
@@ -465,7 +482,7 @@ class CronLifecycleService {
|
|
|
465
482
|
source
|
|
466
483
|
};
|
|
467
484
|
if (this.started) {
|
|
468
|
-
|
|
485
|
+
this.scheduleTask(task);
|
|
469
486
|
}
|
|
470
487
|
this.tasks.set(descriptor.taskName, task);
|
|
471
488
|
}
|
|
@@ -485,22 +502,36 @@ class CronLifecycleService {
|
|
|
485
502
|
if (!task.enabled || task.scheduledHandle) {
|
|
486
503
|
return;
|
|
487
504
|
}
|
|
488
|
-
|
|
505
|
+
const previousToken = task.activeScheduleToken;
|
|
506
|
+
const nextToken = {};
|
|
507
|
+
task.activeScheduleToken = nextToken;
|
|
508
|
+
try {
|
|
509
|
+
task.scheduledHandle = this.createScheduledHandle(task, nextToken);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
task.activeScheduleToken = previousToken;
|
|
512
|
+
throw error;
|
|
513
|
+
}
|
|
489
514
|
}
|
|
490
|
-
createScheduledHandle(task) {
|
|
515
|
+
createScheduledHandle(task, token = {}) {
|
|
491
516
|
const taskName = task.descriptor.taskName;
|
|
492
517
|
if (task.descriptor.kind === 'cron') {
|
|
493
518
|
const expression = task.descriptor.expression;
|
|
494
519
|
if (!expression) {
|
|
495
520
|
throw new Error(`Cron task "${taskName}" is missing a cron expression.`);
|
|
496
521
|
}
|
|
497
|
-
|
|
522
|
+
const scheduledHandle = this.options.scheduler(expression, {
|
|
498
523
|
name: taskName,
|
|
499
524
|
protect: true,
|
|
500
525
|
timezone: task.descriptor.timezone
|
|
501
526
|
}, async () => {
|
|
502
|
-
await this.handleTaskTick(taskName);
|
|
527
|
+
await this.handleTaskTick(taskName, token);
|
|
503
528
|
});
|
|
529
|
+
return {
|
|
530
|
+
stop: () => {
|
|
531
|
+
scheduledHandle.stop();
|
|
532
|
+
},
|
|
533
|
+
token
|
|
534
|
+
};
|
|
504
535
|
}
|
|
505
536
|
const ms = task.descriptor.ms;
|
|
506
537
|
if (!ms) {
|
|
@@ -508,54 +539,63 @@ class CronLifecycleService {
|
|
|
508
539
|
}
|
|
509
540
|
if (task.descriptor.kind === 'interval') {
|
|
510
541
|
const timer = setInterval(() => {
|
|
511
|
-
void this.handleTaskTick(taskName);
|
|
542
|
+
void this.handleTaskTick(taskName, token);
|
|
512
543
|
}, ms);
|
|
513
544
|
return {
|
|
514
545
|
stop: () => {
|
|
515
546
|
clearInterval(timer);
|
|
516
|
-
}
|
|
547
|
+
},
|
|
548
|
+
token
|
|
517
549
|
};
|
|
518
550
|
}
|
|
519
551
|
const timer = setTimeout(() => {
|
|
520
|
-
void this.handleTaskTick(taskName).finally(() => {
|
|
521
|
-
this.completeTimeoutTask(taskName);
|
|
552
|
+
void this.handleTaskTick(taskName, token).finally(() => {
|
|
553
|
+
this.completeTimeoutTask(taskName, token);
|
|
522
554
|
});
|
|
523
555
|
}, ms);
|
|
524
556
|
return {
|
|
525
557
|
stop: () => {
|
|
526
558
|
clearTimeout(timer);
|
|
527
|
-
}
|
|
559
|
+
},
|
|
560
|
+
token
|
|
528
561
|
};
|
|
529
562
|
}
|
|
530
|
-
completeTimeoutTask(taskName) {
|
|
563
|
+
completeTimeoutTask(taskName, token) {
|
|
531
564
|
const task = this.tasks.get(taskName);
|
|
532
|
-
if (!task || task.descriptor.kind !== 'timeout') {
|
|
565
|
+
if (!task || task.descriptor.kind !== 'timeout' || task.activeScheduleToken !== token) {
|
|
533
566
|
return;
|
|
534
567
|
}
|
|
535
568
|
task.scheduledHandle = undefined;
|
|
569
|
+
task.activeScheduleToken = undefined;
|
|
536
570
|
task.enabled = false;
|
|
537
571
|
}
|
|
538
572
|
unscheduleTask(task) {
|
|
539
573
|
if (!task.scheduledHandle) {
|
|
540
|
-
return;
|
|
574
|
+
return true;
|
|
541
575
|
}
|
|
542
576
|
const scheduledHandle = task.scheduledHandle;
|
|
577
|
+
if (!this.stopScheduledHandle(scheduledHandle)) {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
543
580
|
task.scheduledHandle = undefined;
|
|
544
|
-
|
|
581
|
+
task.activeScheduleToken = undefined;
|
|
582
|
+
return true;
|
|
545
583
|
}
|
|
546
584
|
stopScheduledHandle(scheduledHandle) {
|
|
547
585
|
try {
|
|
548
586
|
scheduledHandle.stop();
|
|
587
|
+
return true;
|
|
549
588
|
} catch (error) {
|
|
550
589
|
this.logger.error('Failed to stop scheduled task.', error, 'CronLifecycleService');
|
|
590
|
+
return false;
|
|
551
591
|
}
|
|
552
592
|
}
|
|
553
|
-
async handleTaskTick(taskName) {
|
|
593
|
+
async handleTaskTick(taskName, token) {
|
|
554
594
|
const taskState = this.tasks.get(taskName);
|
|
555
|
-
if (!taskState
|
|
595
|
+
if (!this.started || !this.isTaskTickCurrent(taskName, token, taskState) || taskState.running) {
|
|
556
596
|
return;
|
|
557
597
|
}
|
|
558
|
-
const task = this.runTaskTick(taskState.descriptor, taskState);
|
|
598
|
+
const task = this.runTaskTick(taskState.descriptor, taskState, token);
|
|
559
599
|
taskState.running = true;
|
|
560
600
|
this.activeTasks.add(task);
|
|
561
601
|
try {
|
|
@@ -565,23 +605,30 @@ class CronLifecycleService {
|
|
|
565
605
|
this.activeTasks.delete(task);
|
|
566
606
|
}
|
|
567
607
|
}
|
|
568
|
-
|
|
608
|
+
isTaskTickCurrent(taskName, token, taskState) {
|
|
609
|
+
return this.lifecycleState !== 'stopping' && this.lifecycleState !== 'stopped' && taskState?.enabled === true && this.tasks.get(taskName) === taskState && taskState.activeScheduleToken === token;
|
|
610
|
+
}
|
|
611
|
+
async runTaskTick(descriptor, taskState, token) {
|
|
569
612
|
if (!this.shouldUseDistributedExecution(descriptor)) {
|
|
570
613
|
await this.executeTask(descriptor, taskState);
|
|
571
614
|
return;
|
|
572
615
|
}
|
|
573
|
-
await this.runDistributedTaskTick(descriptor, taskState);
|
|
616
|
+
await this.runDistributedTaskTick(descriptor, taskState, token);
|
|
574
617
|
}
|
|
575
618
|
shouldUseDistributedExecution(descriptor) {
|
|
576
619
|
return this.options.distributed.enabled && descriptor.distributed && this.distributedLocks.resolvedClient !== undefined;
|
|
577
620
|
}
|
|
578
|
-
async runDistributedTaskTick(descriptor, taskState) {
|
|
579
|
-
const
|
|
580
|
-
if (!
|
|
621
|
+
async runDistributedTaskTick(descriptor, taskState, token) {
|
|
622
|
+
const lease = await this.distributedLocks.tryAcquireLock(descriptor);
|
|
623
|
+
if (!lease) {
|
|
581
624
|
return;
|
|
582
625
|
}
|
|
583
|
-
|
|
584
|
-
|
|
626
|
+
if (this.lifecycleState !== 'failed' && !this.isTaskTickCurrent(descriptor.taskName, token, taskState)) {
|
|
627
|
+
await this.distributedLocks.releaseLock(lease, this.shutdownPromise ? this.getRemainingShutdownTimeoutMs() : undefined);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const lockRenewalMonitor = this.distributedLocks.startLockRenewalMonitor(descriptor, lease);
|
|
631
|
+
this.runningDistributedLeaseCounts.set(descriptor.lockKey, (this.runningDistributedLeaseCounts.get(descriptor.lockKey) ?? 0) + 1);
|
|
585
632
|
try {
|
|
586
633
|
await this.executeTask(descriptor, taskState, async () => {
|
|
587
634
|
lockRenewalMonitor.stop();
|
|
@@ -589,10 +636,15 @@ class CronLifecycleService {
|
|
|
589
636
|
});
|
|
590
637
|
} finally {
|
|
591
638
|
lockRenewalMonitor.stop();
|
|
592
|
-
this.
|
|
593
|
-
|
|
639
|
+
const runningLeaseCount = this.runningDistributedLeaseCounts.get(descriptor.lockKey);
|
|
640
|
+
if (runningLeaseCount === 1) {
|
|
641
|
+
this.runningDistributedLeaseCounts.delete(descriptor.lockKey);
|
|
642
|
+
} else if (runningLeaseCount !== undefined) {
|
|
643
|
+
this.runningDistributedLeaseCounts.set(descriptor.lockKey, runningLeaseCount - 1);
|
|
644
|
+
}
|
|
645
|
+
const released = await this.distributedLocks.releaseLock(lease, this.shutdownPromise ? this.getRemainingShutdownTimeoutMs() : undefined);
|
|
594
646
|
if (!released && this.lifecycleState === 'stopped') {
|
|
595
|
-
await this.distributedLocks.releaseOwnedLocks(
|
|
647
|
+
await this.distributedLocks.releaseOwnedLocks(this.getRunningDistributedLockKeys(), this.getRemainingShutdownTimeoutMs());
|
|
596
648
|
}
|
|
597
649
|
}
|
|
598
650
|
}
|
|
@@ -600,7 +652,8 @@ class CronLifecycleService {
|
|
|
600
652
|
if (this.activeTasks.size === 0) {
|
|
601
653
|
return false;
|
|
602
654
|
}
|
|
603
|
-
|
|
655
|
+
const timeoutMs = this.getRemainingShutdownTimeoutMs();
|
|
656
|
+
if (timeoutMs === 0) {
|
|
604
657
|
return true;
|
|
605
658
|
}
|
|
606
659
|
let timeoutHandle;
|
|
@@ -608,7 +661,7 @@ class CronLifecycleService {
|
|
|
608
661
|
return await Promise.race([this.drainActiveTasks().then(() => false), new Promise(resolve => {
|
|
609
662
|
timeoutHandle = setTimeout(() => {
|
|
610
663
|
resolve(true);
|
|
611
|
-
},
|
|
664
|
+
}, timeoutMs);
|
|
612
665
|
})]);
|
|
613
666
|
} finally {
|
|
614
667
|
if (timeoutHandle) {
|
|
@@ -626,6 +679,7 @@ class CronLifecycleService {
|
|
|
626
679
|
if (descriptor.kind === 'timeout') {
|
|
627
680
|
taskState.enabled = false;
|
|
628
681
|
taskState.scheduledHandle = undefined;
|
|
682
|
+
taskState.activeScheduleToken = undefined;
|
|
629
683
|
}
|
|
630
684
|
}
|
|
631
685
|
stopAllScheduledTasks() {
|
package/dist/types.d.ts
CHANGED
|
@@ -167,7 +167,8 @@ export interface SchedulingRegistry {
|
|
|
167
167
|
* Removes one registered task from the runtime registry.
|
|
168
168
|
*
|
|
169
169
|
* @param name Task name to remove.
|
|
170
|
-
* @returns `true` when a task existed and was removed.
|
|
170
|
+
* @returns `true` when a task existed and was removed; `false` when it was absent or its handle could not stop.
|
|
171
|
+
* @remarks A failed stop keeps the task and scheduler handle registered so removal can be retried safely.
|
|
171
172
|
*/
|
|
172
173
|
remove(name: string): boolean;
|
|
173
174
|
/**
|
|
@@ -181,7 +182,8 @@ export interface SchedulingRegistry {
|
|
|
181
182
|
* Disables a task without deleting its descriptor.
|
|
182
183
|
*
|
|
183
184
|
* @param name Task name to disable.
|
|
184
|
-
* @returns `true` when the task exists
|
|
185
|
+
* @returns `true` when the task exists and its handle stopped; `false` when it was absent or its handle could not stop.
|
|
186
|
+
* @remarks A failed stop leaves the task disabled and keeps its scheduler handle registered for cleanup retry.
|
|
185
187
|
*/
|
|
186
188
|
disable(name: string): boolean;
|
|
187
189
|
/**
|
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": "3.0.0",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"directory": "packages/cron"
|
|
19
19
|
},
|
|
20
20
|
"engines": {
|
|
21
|
-
"node": ">=
|
|
21
|
+
"node": ">=24.0.0 <27"
|
|
22
22
|
},
|
|
23
23
|
"publishConfig": {
|
|
24
24
|
"access": "public"
|
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"croner": "^8.1.2",
|
|
40
|
-
"@fluojs/core": "^
|
|
41
|
-
"@fluojs/
|
|
42
|
-
"@fluojs/
|
|
40
|
+
"@fluojs/core": "^2.0.0",
|
|
41
|
+
"@fluojs/runtime": "^3.0.0",
|
|
42
|
+
"@fluojs/di": "^3.0.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
|
-
"@fluojs/redis": "^
|
|
45
|
+
"@fluojs/redis": "^2.0.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependenciesMeta": {
|
|
48
48
|
"@fluojs/redis": {
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
-
"vitest": "^
|
|
54
|
-
"@fluojs/redis": "^
|
|
53
|
+
"vitest": "^4.1.11",
|
|
54
|
+
"@fluojs/redis": "^2.0.0"
|
|
55
55
|
},
|
|
56
56
|
"scripts": {
|
|
57
57
|
"prebuild": "node ../../tooling/scripts/clean-dist.mjs",
|