@fluojs/cron 1.0.0 → 1.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 +5 -0
- package/README.md +5 -0
- package/dist/service.d.ts +3 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +48 -25
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -23,6 +23,8 @@ fluo 애플리케이션을 위한 데코레이터 기반 스케줄링 패키지
|
|
|
23
23
|
npm install @fluojs/cron croner
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
`croner`는 `@fluojs/cron`이 사용하는 scheduler engine입니다. 애플리케이션과 배포 감사에서 runtime scheduler dependency ownership이 명확히 보이도록 패키지와 함께 설치하세요.
|
|
27
|
+
|
|
26
28
|
## 사용 시점
|
|
27
29
|
|
|
28
30
|
- 정기적인 백그라운드 작업(예: 데이터베이스 정리, 리포트 생성)이 필요할 때 사용합니다.
|
|
@@ -141,6 +143,8 @@ class TaskManager {
|
|
|
141
143
|
|
|
142
144
|
Registry는 `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, `updateCronExpression`을 제공합니다. Timeout task는 한 번 실행된 뒤 비활성화되지만 registry에는 남아 있어 의도적으로 다시 활성화할 수 있습니다.
|
|
143
145
|
|
|
146
|
+
Dynamic cron 등록은 scheduler startup과 원자적으로 처리됩니다. Scheduler가 새 cron job을 거부하면 registry는 half-registered task를 남기지 않습니다. 실행 중인 cron expression update도 rollback-safe합니다. Rescheduling이 실패하면 이전 expression과 scheduled job이 그대로 유지됩니다. Cron task는 scheduler-level no-overlap protection과 fluo의 in-process running guard를 함께 사용하므로 같은 task instance가 overlapping tick으로 실행되지 않습니다.
|
|
147
|
+
|
|
144
148
|
### 제한된 종료
|
|
145
149
|
|
|
146
150
|
`CronModule`은 애플리케이션 종료 시 실행 중인 작업을 제한된 타임아웃 안에서 drain합니다. 따라서 하나의 hung task 때문에 프로세스 종료가 영원히 막히지 않습니다.
|
|
@@ -177,6 +181,7 @@ singleton provider/controller만 스케줄링됩니다. Request-scoped 및 trans
|
|
|
177
181
|
- `SCHEDULING_REGISTRY`: `SchedulingRegistry` 서비스를 위한 주입 토큰입니다.
|
|
178
182
|
- `normalizeCronModuleOptions(...)`: module option과 기본값을 정규화합니다.
|
|
179
183
|
- `createCronPlatformStatusSnapshot(...)`: health/readiness 통합을 위한 status snapshot을 생성합니다.
|
|
184
|
+
- 공개 타입: `CronModuleOptions`, `CronDistributedOptions`, `CronShutdownOptions`, `CronScheduleOptions`, `CronScheduler`, `CronScheduledJob`, `SchedulingRegistry`, `SchedulingTaskDescriptor`, `SchedulingTaskCallback`, `SchedulingTaskOptions`, `CronTaskOptions`, `IntervalTaskOptions`, `TimeoutTaskOptions`.
|
|
180
185
|
- 메타데이터 헬퍼와 심볼: `defineSchedulingTaskMetadata`, `defineCronTaskMetadata`, `getSchedulingTaskMetadata`, `getCronTaskMetadata`, `getSchedulingTaskMetadataEntries`, `getCronTaskMetadataEntries`, `schedulingMetadataSymbol`, `cronMetadataSymbol`.
|
|
181
186
|
|
|
182
187
|
|
package/README.md
CHANGED
|
@@ -23,6 +23,8 @@ Decorator-based scheduling for fluo applications with lifecycle-managed startup/
|
|
|
23
23
|
npm install @fluojs/cron croner
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
`croner` is the scheduler engine used by `@fluojs/cron`. Install it alongside the package so lockfiles make the runtime scheduler dependency explicit for applications and deployment audits.
|
|
27
|
+
|
|
26
28
|
## When to Use
|
|
27
29
|
|
|
28
30
|
- When you need to run periodic background tasks (e.g., database cleanup, report generation).
|
|
@@ -141,6 +143,8 @@ class TaskManager {
|
|
|
141
143
|
|
|
142
144
|
The registry exposes `addCron`, `addInterval`, `addTimeout`, `remove`, `enable`, `disable`, `get`, `getAll`, and `updateCronExpression`. Timeout tasks run once, then disable themselves while remaining in the registry so they can be re-enabled deliberately.
|
|
143
145
|
|
|
146
|
+
Dynamic cron registration is atomic with scheduler startup: if the scheduler rejects a new cron job, the registry does not retain a half-registered task. Updating a running cron expression is also rollback-safe. If rescheduling fails, the previous expression and scheduled job remain active. Cron tasks use both scheduler-level no-overlap protection and fluo's in-process running guard, so the same task instance will not run overlapping ticks.
|
|
147
|
+
|
|
144
148
|
### Bounded Shutdown
|
|
145
149
|
|
|
146
150
|
`CronModule` drains active task executions during application shutdown with a bounded timeout so one hung task cannot block process termination forever.
|
|
@@ -177,6 +181,7 @@ Only singleton providers/controllers are scheduled. Request-scoped and transient
|
|
|
177
181
|
- `SCHEDULING_REGISTRY`: Injection token for the `SchedulingRegistry` service.
|
|
178
182
|
- `normalizeCronModuleOptions(...)`: Normalizes module options and defaults.
|
|
179
183
|
- `createCronPlatformStatusSnapshot(...)`: Creates a status snapshot for health/readiness integrations.
|
|
184
|
+
- Public types: `CronModuleOptions`, `CronDistributedOptions`, `CronShutdownOptions`, `CronScheduleOptions`, `CronScheduler`, `CronScheduledJob`, `SchedulingRegistry`, `SchedulingTaskDescriptor`, `SchedulingTaskCallback`, `SchedulingTaskOptions`, `CronTaskOptions`, `IntervalTaskOptions`, and `TimeoutTaskOptions`.
|
|
180
185
|
- Metadata helpers and symbols: `defineSchedulingTaskMetadata`, `defineCronTaskMetadata`, `getSchedulingTaskMetadata`, `getCronTaskMetadata`, `getSchedulingTaskMetadataEntries`, `getCronTaskMetadataEntries`, `schedulingMetadataSymbol`, `cronMetadataSymbol`.
|
|
181
186
|
|
|
182
187
|
|
package/dist/service.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Container } from '@fluojs/di';
|
|
2
|
-
import {
|
|
2
|
+
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy } from '@fluojs/runtime';
|
|
3
3
|
import type { CronTaskOptions, IntervalTaskOptions, NormalizedCronModuleOptions, SchedulingRegistry, SchedulingTaskCallback, SchedulingTaskDescriptor, TimeoutTaskOptions } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Lifecycle-managed scheduler runtime for decorator-discovered and dynamic tasks.
|
|
@@ -106,8 +106,10 @@ export declare class CronLifecycleService implements SchedulingRegistry, OnAppli
|
|
|
106
106
|
private assertTaskNameAvailable;
|
|
107
107
|
private scheduleEnabledTasks;
|
|
108
108
|
private scheduleTask;
|
|
109
|
+
private createScheduledHandle;
|
|
109
110
|
private completeTimeoutTask;
|
|
110
111
|
private unscheduleTask;
|
|
112
|
+
private stopScheduledHandle;
|
|
111
113
|
private handleTaskTick;
|
|
112
114
|
private runTaskTick;
|
|
113
115
|
private shouldUseDistributedExecution;
|
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;AAG5C,OAAO,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
|
package/dist/service.js
CHANGED
|
@@ -176,10 +176,17 @@ class CronLifecycleService {
|
|
|
176
176
|
if (task.enabled) {
|
|
177
177
|
return true;
|
|
178
178
|
}
|
|
179
|
-
task.enabled = true;
|
|
180
179
|
if (this.started) {
|
|
181
|
-
|
|
180
|
+
task.enabled = true;
|
|
181
|
+
try {
|
|
182
|
+
this.scheduleTask(task);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
task.enabled = false;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
182
188
|
}
|
|
189
|
+
task.enabled = true;
|
|
183
190
|
return true;
|
|
184
191
|
}
|
|
185
192
|
|
|
@@ -237,12 +244,24 @@ class CronLifecycleService {
|
|
|
237
244
|
if (task.descriptor.kind !== 'cron') {
|
|
238
245
|
throw new Error(`updateCronExpression() supports only cron tasks. Received ${task.descriptor.kind} task "${name}".`);
|
|
239
246
|
}
|
|
240
|
-
task.descriptor.expression = expression;
|
|
241
247
|
if (!task.enabled || !this.started) {
|
|
248
|
+
task.descriptor.expression = expression;
|
|
242
249
|
return;
|
|
243
250
|
}
|
|
244
|
-
|
|
245
|
-
|
|
251
|
+
const previousExpression = task.descriptor.expression;
|
|
252
|
+
const previousHandle = task.scheduledHandle;
|
|
253
|
+
task.descriptor.expression = expression;
|
|
254
|
+
try {
|
|
255
|
+
const nextHandle = this.createScheduledHandle(task);
|
|
256
|
+
task.scheduledHandle = nextHandle;
|
|
257
|
+
if (previousHandle) {
|
|
258
|
+
this.stopScheduledHandle(previousHandle);
|
|
259
|
+
}
|
|
260
|
+
} catch (error) {
|
|
261
|
+
task.descriptor.expression = previousExpression;
|
|
262
|
+
task.scheduledHandle = previousHandle;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
246
265
|
}
|
|
247
266
|
async onApplicationBootstrap() {
|
|
248
267
|
if (this.started) {
|
|
@@ -365,10 +384,10 @@ class CronLifecycleService {
|
|
|
365
384
|
scheduledHandle: undefined,
|
|
366
385
|
source
|
|
367
386
|
};
|
|
368
|
-
this.tasks.set(descriptor.taskName, task);
|
|
369
387
|
if (this.started) {
|
|
370
|
-
this.
|
|
388
|
+
task.scheduledHandle = this.createScheduledHandle(task);
|
|
371
389
|
}
|
|
390
|
+
this.tasks.set(descriptor.taskName, task);
|
|
372
391
|
}
|
|
373
392
|
assertTaskNameAvailable(taskName) {
|
|
374
393
|
if (this.tasks.has(taskName)) {
|
|
@@ -386,42 +405,43 @@ class CronLifecycleService {
|
|
|
386
405
|
if (!task.enabled || task.scheduledHandle) {
|
|
387
406
|
return;
|
|
388
407
|
}
|
|
408
|
+
task.scheduledHandle = this.createScheduledHandle(task);
|
|
409
|
+
}
|
|
410
|
+
createScheduledHandle(task) {
|
|
411
|
+
const taskName = task.descriptor.taskName;
|
|
389
412
|
if (task.descriptor.kind === 'cron') {
|
|
390
413
|
const expression = task.descriptor.expression;
|
|
391
414
|
if (!expression) {
|
|
392
|
-
throw new Error(`Cron task "${
|
|
415
|
+
throw new Error(`Cron task "${taskName}" is missing a cron expression.`);
|
|
393
416
|
}
|
|
394
|
-
|
|
395
|
-
name:
|
|
417
|
+
return this.options.scheduler(expression, {
|
|
418
|
+
name: taskName,
|
|
396
419
|
protect: true,
|
|
397
420
|
timezone: task.descriptor.timezone
|
|
398
421
|
}, async () => {
|
|
399
|
-
await this.handleTaskTick(
|
|
422
|
+
await this.handleTaskTick(taskName);
|
|
400
423
|
});
|
|
401
|
-
task.scheduledHandle = scheduled;
|
|
402
|
-
return;
|
|
403
424
|
}
|
|
404
425
|
const ms = task.descriptor.ms;
|
|
405
426
|
if (!ms) {
|
|
406
|
-
throw new Error(`${task.descriptor.kind} task "${
|
|
427
|
+
throw new Error(`${task.descriptor.kind} task "${taskName}" is missing interval duration.`);
|
|
407
428
|
}
|
|
408
429
|
if (task.descriptor.kind === 'interval') {
|
|
409
430
|
const timer = setInterval(() => {
|
|
410
|
-
void this.handleTaskTick(
|
|
431
|
+
void this.handleTaskTick(taskName);
|
|
411
432
|
}, ms);
|
|
412
|
-
|
|
433
|
+
return {
|
|
413
434
|
stop: () => {
|
|
414
435
|
clearInterval(timer);
|
|
415
436
|
}
|
|
416
437
|
};
|
|
417
|
-
return;
|
|
418
438
|
}
|
|
419
439
|
const timer = setTimeout(() => {
|
|
420
|
-
void this.handleTaskTick(
|
|
421
|
-
this.completeTimeoutTask(
|
|
440
|
+
void this.handleTaskTick(taskName).finally(() => {
|
|
441
|
+
this.completeTimeoutTask(taskName);
|
|
422
442
|
});
|
|
423
443
|
}, ms);
|
|
424
|
-
|
|
444
|
+
return {
|
|
425
445
|
stop: () => {
|
|
426
446
|
clearTimeout(timer);
|
|
427
447
|
}
|
|
@@ -439,17 +459,20 @@ class CronLifecycleService {
|
|
|
439
459
|
if (!task.scheduledHandle) {
|
|
440
460
|
return;
|
|
441
461
|
}
|
|
462
|
+
const scheduledHandle = task.scheduledHandle;
|
|
463
|
+
task.scheduledHandle = undefined;
|
|
464
|
+
this.stopScheduledHandle(scheduledHandle);
|
|
465
|
+
}
|
|
466
|
+
stopScheduledHandle(scheduledHandle) {
|
|
442
467
|
try {
|
|
443
|
-
|
|
468
|
+
scheduledHandle.stop();
|
|
444
469
|
} catch (error) {
|
|
445
|
-
this.logger.error('Failed to stop scheduled task
|
|
446
|
-
} finally {
|
|
447
|
-
task.scheduledHandle = undefined;
|
|
470
|
+
this.logger.error('Failed to stop scheduled task.', error, 'CronLifecycleService');
|
|
448
471
|
}
|
|
449
472
|
}
|
|
450
473
|
async handleTaskTick(taskName) {
|
|
451
474
|
const taskState = this.tasks.get(taskName);
|
|
452
|
-
if (!taskState
|
|
475
|
+
if (!taskState?.enabled || taskState.running) {
|
|
453
476
|
return;
|
|
454
477
|
}
|
|
455
478
|
const task = this.runTaskTick(taskState.descriptor, taskState);
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"timeout",
|
|
10
10
|
"distributed-lock"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.1",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
],
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"croner": "^8.1.2",
|
|
40
|
-
"@fluojs/core": "^1.0.
|
|
41
|
-
"@fluojs/di": "^1.0.
|
|
42
|
-
"@fluojs/
|
|
43
|
-
"@fluojs/
|
|
40
|
+
"@fluojs/core": "^1.0.1",
|
|
41
|
+
"@fluojs/di": "^1.0.1",
|
|
42
|
+
"@fluojs/redis": "^1.0.0",
|
|
43
|
+
"@fluojs/runtime": "^1.0.1"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"vitest": "^3.2.4"
|