@fluojs/queue 1.0.0-beta.4 → 1.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 +24 -11
- package/README.md +24 -11
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +2 -1
- package/dist/service.d.ts +8 -2
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +76 -8
- package/dist/status.d.ts +1 -0
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +1 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
package/README.ko.md
CHANGED
|
@@ -59,23 +59,23 @@ import { Module, Inject } from '@fluojs/core';
|
|
|
59
59
|
import { QueueModule, QueueLifecycleService } from '@fluojs/queue';
|
|
60
60
|
import { RedisModule } from '@fluojs/redis';
|
|
61
61
|
|
|
62
|
+
@Inject(QueueLifecycleService)
|
|
63
|
+
export class OrderService {
|
|
64
|
+
constructor(private readonly queue: QueueLifecycleService) {}
|
|
65
|
+
|
|
66
|
+
async placeOrder(id: string) {
|
|
67
|
+
await this.queue.enqueue(new ProcessOrderJob(id));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
@Module({
|
|
63
72
|
imports: [
|
|
64
73
|
RedisModule.forRoot({ host: 'localhost', port: 6379 }),
|
|
65
74
|
QueueModule.forRoot(),
|
|
66
75
|
],
|
|
67
|
-
providers: [OrderWorker],
|
|
76
|
+
providers: [OrderService, OrderWorker],
|
|
68
77
|
})
|
|
69
78
|
export class AppModule {}
|
|
70
|
-
|
|
71
|
-
export class OrderService {
|
|
72
|
-
@Inject(QueueLifecycleService)
|
|
73
|
-
private readonly queue: QueueLifecycleService;
|
|
74
|
-
|
|
75
|
-
async placeOrder(id: string) {
|
|
76
|
-
await this.queue.enqueue(new ProcessOrderJob(id));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
## 일반적인 패턴
|
|
@@ -90,6 +90,12 @@ QueueModule.forRoot({ clientName: 'jobs' })
|
|
|
90
90
|
|
|
91
91
|
`@fluojs/queue`는 애플리케이션 부트스트랩 중 해당 Redis 클라이언트를 조회한 뒤 BullMQ용으로 큐가 소유하는 duplicate 연결을 만듭니다. 공유 `@fluojs/redis` 클라이언트의 소유권은 `RedisModule`에 남아 있으며, Queue는 자신이 만든 BullMQ duplicate 연결만 닫습니다. 이 duplicate 연결은 BullMQ Worker가 요구하는 `maxRetriesPerRequest: null` 설정으로 구성되어 시작 동작이 BullMQ의 실제 런타임 제약과 일치합니다.
|
|
92
92
|
|
|
93
|
+
### 부트스트랩 및 종료 수명 주기
|
|
94
|
+
|
|
95
|
+
Queue는 애플리케이션 부트스트랩 중 worker를 탐색하고 Queue가 소유하는 BullMQ 리소스를 만들지만, BullMQ worker processor는 runtime이 전체 애플리케이션 bootstrap/readiness sequence 완료를 표시한 뒤에만 시작합니다. 다른 `onApplicationBootstrap()` hook에서 enqueue한 job은 Queue 서비스가 초기화된 뒤에는 받을 수 있으며, processor는 뒤에 실행되는 async bootstrap hook이나 애플리케이션 readiness보다 앞서 실행되지 않고 bootstrap-ready handoff 이후 실행됩니다.
|
|
96
|
+
|
|
97
|
+
애플리케이션 종료가 시작되면 Queue는 상태를 `stopping`으로 바꾸고 새 enqueue를 거부한 다음 Queue 소유 worker/queue/connection을 닫고 pending dead-letter write를 drain합니다. Worker 종료는 `workerShutdownTimeoutMs`로 bounded wait를 적용하므로 끝나지 않는 active processor가 애플리케이션 종료를 무기한 막을 수 없습니다. Timeout이 지나면 Queue는 로그를 남기고 BullMQ worker에 force-close를 요청한 뒤 나머지 리소스 정리를 계속합니다.
|
|
98
|
+
|
|
93
99
|
### 분산 재시도 (Distributed Retries)
|
|
94
100
|
|
|
95
101
|
워커 설정에서 최대 시도 횟수와 백오프 전략을 지정하여 일시적인 실패를 자동으로 처리할 수 있습니다.
|
|
@@ -127,7 +133,14 @@ Job은 JSON으로 직렬화 가능한 plain object여야 합니다. Queue는 enq
|
|
|
127
133
|
- `QueueWorkerOptions`: 개별 작업 설정(시도 횟수, 백오프, 동시성, jobName, 전송률 제한 등)을 위한 타입입니다.
|
|
128
134
|
- `QueueBackoffOptions`: 재시도 백오프 설정(`type`, `delayMs`)을 위한 타입입니다.
|
|
129
135
|
|
|
130
|
-
`QueueModuleOptions`에는 `defaultDeadLetterMaxEntries` 같은 dead-letter retention 설정도 포함됩니다.
|
|
136
|
+
`QueueModuleOptions`에는 `workerShutdownTimeoutMs`, `defaultDeadLetterMaxEntries` 같은 lifecycle 및 dead-letter retention 설정도 포함됩니다.
|
|
137
|
+
|
|
138
|
+
`QueueModuleOptions` 수명 주기/status 설정:
|
|
139
|
+
|
|
140
|
+
- `workerShutdownTimeoutMs`: 종료 중 active worker processor를 기다리는 최대 시간입니다. 시간이 지나면 BullMQ worker를 force-close합니다. 기본값은 `30_000`입니다.
|
|
141
|
+
- `defaultDeadLetterMaxEntries`: job별로 유지할 dead-letter record의 최대 개수이며, trimming을 끄려면 `false`를 지정합니다. 기본값은 `1_000`입니다.
|
|
142
|
+
|
|
143
|
+
`createQueuePlatformStatusSnapshot(...)`은 Queue가 `started`에 도달한 뒤에만 readiness를 `ready`로 보고합니다. `starting`은 degraded readiness, `stopping`/`stopped`는 not-ready로 보고합니다. Snapshot details에는 Redis dependency id, lifecycle state, ready/discovered worker 수, pending dead-letter write 수, dead-letter drain timeout, `workerShutdownTimeoutMs`가 포함됩니다.
|
|
131
144
|
|
|
132
145
|
singleton `@QueueWorker()` provider/controller만 등록됩니다. request/transient worker는 discovery 중 건너뜁니다.
|
|
133
146
|
|
package/README.md
CHANGED
|
@@ -59,23 +59,23 @@ import { Module, Inject } from '@fluojs/core';
|
|
|
59
59
|
import { QueueModule, QueueLifecycleService } from '@fluojs/queue';
|
|
60
60
|
import { RedisModule } from '@fluojs/redis';
|
|
61
61
|
|
|
62
|
+
@Inject(QueueLifecycleService)
|
|
63
|
+
export class OrderService {
|
|
64
|
+
constructor(private readonly queue: QueueLifecycleService) {}
|
|
65
|
+
|
|
66
|
+
async placeOrder(id: string) {
|
|
67
|
+
await this.queue.enqueue(new ProcessOrderJob(id));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
@Module({
|
|
63
72
|
imports: [
|
|
64
73
|
RedisModule.forRoot({ host: 'localhost', port: 6379 }),
|
|
65
74
|
QueueModule.forRoot(),
|
|
66
75
|
],
|
|
67
|
-
providers: [OrderWorker],
|
|
76
|
+
providers: [OrderService, OrderWorker],
|
|
68
77
|
})
|
|
69
78
|
export class AppModule {}
|
|
70
|
-
|
|
71
|
-
export class OrderService {
|
|
72
|
-
@Inject(QueueLifecycleService)
|
|
73
|
-
private readonly queue: QueueLifecycleService;
|
|
74
|
-
|
|
75
|
-
async placeOrder(id: string) {
|
|
76
|
-
await this.queue.enqueue(new ProcessOrderJob(id));
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
## Common Patterns
|
|
@@ -90,6 +90,12 @@ QueueModule.forRoot({ clientName: 'jobs' })
|
|
|
90
90
|
|
|
91
91
|
`@fluojs/queue` resolves that Redis client during application bootstrap, then creates queue-owned duplicate connections for BullMQ. The shared `@fluojs/redis` client remains owned by `RedisModule`; Queue closes only the duplicate BullMQ connections it creates. Those duplicate connections are configured with BullMQ's required `maxRetriesPerRequest: null` worker setting so startup behavior matches BullMQ's runtime constraints.
|
|
92
92
|
|
|
93
|
+
### Bootstrap and Shutdown Lifecycle
|
|
94
|
+
|
|
95
|
+
Queue discovers workers and creates queue-owned BullMQ resources during application bootstrap, but BullMQ worker processors are started only after the runtime marks the full application bootstrap/readiness sequence complete. Jobs enqueued by other `onApplicationBootstrap()` hooks can be accepted once the Queue service is initialized, and their processors run after the bootstrap-ready handoff instead of racing ahead of later async bootstrap hooks or application readiness.
|
|
96
|
+
|
|
97
|
+
Application shutdown marks Queue as `stopping`, rejects new enqueue attempts, closes queue-owned workers/queues/connections, and drains pending dead-letter writes. Worker shutdown is bounded by `workerShutdownTimeoutMs` so an active processor that never settles cannot block application shutdown indefinitely. When the timeout elapses, Queue logs the timeout and asks BullMQ to force-close the worker before continuing resource cleanup.
|
|
98
|
+
|
|
93
99
|
### Distributed Retries
|
|
94
100
|
|
|
95
101
|
Workers can be configured with a maximum number of attempts and backoff strategies to handle transient failures automatically.
|
|
@@ -127,7 +133,14 @@ Treat low-level provider assembly as an internal implementation detail: low-leve
|
|
|
127
133
|
- `QueueWorkerOptions`: Per-job settings (attempts, backoff, concurrency, jobName, rate limiting).
|
|
128
134
|
- `QueueBackoffOptions`: Retry backoff settings (`type`, `delayMs`).
|
|
129
135
|
|
|
130
|
-
`QueueModuleOptions` also includes dead-letter retention controls such as `defaultDeadLetterMaxEntries`.
|
|
136
|
+
`QueueModuleOptions` also includes lifecycle and dead-letter retention controls such as `workerShutdownTimeoutMs` and `defaultDeadLetterMaxEntries`.
|
|
137
|
+
|
|
138
|
+
`QueueModuleOptions` lifecycle/status controls:
|
|
139
|
+
|
|
140
|
+
- `workerShutdownTimeoutMs`: maximum time to wait for active worker processors during shutdown before force-closing the BullMQ worker. Defaults to `30_000`.
|
|
141
|
+
- `defaultDeadLetterMaxEntries`: maximum retained dead-letter records per job, or `false` to disable trimming. Defaults to `1_000`.
|
|
142
|
+
|
|
143
|
+
`createQueuePlatformStatusSnapshot(...)` reports readiness as `ready` only after Queue reaches `started`; `starting` reports degraded readiness, and `stopping`/`stopped` report not-ready. Snapshot details include the Redis dependency id, lifecycle state, ready/discovered worker counts, pending dead-letter writes, the dead-letter drain timeout, and `workerShutdownTimeoutMs`.
|
|
131
144
|
|
|
132
145
|
Only singleton `@QueueWorker()` providers/controllers are registered. Request/transient workers are skipped during discovery.
|
|
133
146
|
|
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,EAAgC,kBAAkB,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,EAAgC,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAsCnF;;GAEG;AACH,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU;CAS7D"}
|
package/dist/module.js
CHANGED
|
@@ -13,7 +13,8 @@ function normalizeQueueModuleOptions(options = {}) {
|
|
|
13
13
|
} : undefined,
|
|
14
14
|
defaultConcurrency: normalizePositiveInteger(options.defaultConcurrency, 1),
|
|
15
15
|
defaultDeadLetterMaxEntries: normalizePositiveIntegerOrFalse(options.defaultDeadLetterMaxEntries, 1_000),
|
|
16
|
-
defaultRateLimiter
|
|
16
|
+
defaultRateLimiter,
|
|
17
|
+
workerShutdownTimeoutMs: normalizePositiveInteger(options.workerShutdownTimeoutMs, 30_000)
|
|
17
18
|
};
|
|
18
19
|
}
|
|
19
20
|
function createQueueProviders(options = {}) {
|
package/dist/service.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Container } from '@fluojs/di';
|
|
2
|
-
import {
|
|
2
|
+
import type { ApplicationLogger, CompiledModule, OnApplicationBootstrap, OnApplicationShutdown, OnModuleDestroy } from '@fluojs/runtime';
|
|
3
|
+
import { type BootstrapReadySignal } from '@fluojs/runtime/internal';
|
|
3
4
|
import type { NormalizedQueueModuleOptions, Queue } from './types.js';
|
|
4
5
|
/**
|
|
5
6
|
* Lifecycle-managed queue runtime for worker discovery and job dispatch.
|
|
@@ -12,16 +13,18 @@ export declare class QueueLifecycleService implements Queue, OnApplicationBootst
|
|
|
12
13
|
private readonly runtimeContainer;
|
|
13
14
|
private readonly compiledModules;
|
|
14
15
|
private readonly logger;
|
|
16
|
+
private readonly bootstrapReadySignal;
|
|
15
17
|
private readonly descriptorsByJobType;
|
|
16
18
|
private readonly queuesByJobName;
|
|
17
19
|
private readonly workersByJobName;
|
|
18
20
|
private readonly ownedConnections;
|
|
19
21
|
private readonly deadLetterManager;
|
|
22
|
+
private readonly readyWorkers;
|
|
20
23
|
private lifecycleState;
|
|
21
24
|
private redisClient;
|
|
22
25
|
private startPromise;
|
|
23
26
|
private shutdownPromise;
|
|
24
|
-
constructor(options: NormalizedQueueModuleOptions, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger);
|
|
27
|
+
constructor(options: NormalizedQueueModuleOptions, runtimeContainer: Container, compiledModules: readonly CompiledModule[], logger: ApplicationLogger, bootstrapReadySignal?: BootstrapReadySignal);
|
|
25
28
|
onApplicationBootstrap(): Promise<void>;
|
|
26
29
|
onApplicationShutdown(): Promise<void>;
|
|
27
30
|
onModuleDestroy(): Promise<void>;
|
|
@@ -53,12 +56,15 @@ export declare class QueueLifecycleService implements Queue, OnApplicationBootst
|
|
|
53
56
|
private createWorkerLimiterOptions;
|
|
54
57
|
private attachWorkerFailureHandler;
|
|
55
58
|
private registerInitializedWorker;
|
|
59
|
+
private scheduleReadyWorkers;
|
|
60
|
+
private runWorker;
|
|
56
61
|
private cleanupWorkerInitializationFailure;
|
|
57
62
|
private createOwnedConnection;
|
|
58
63
|
private executeWorker;
|
|
59
64
|
private resolveWorkerHandler;
|
|
60
65
|
private rehydrateWorkerPayload;
|
|
61
66
|
private shutdown;
|
|
67
|
+
private waitForInFlightStartup;
|
|
62
68
|
private closeInitializedResources;
|
|
63
69
|
private tryCloseWorker;
|
|
64
70
|
private tryCloseQueue;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,OAAO,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,0BAA0B,CAAC;AAQlC,OAAO,KAAK,EACV,4BAA4B,EAC5B,KAAK,EAIN,MAAM,YAAY,CAAC;AAqHpB;;;;;GAKG;AACH,qBACa,qBAAsB,YAAW,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe;IAa/G,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,oBAAoB;IAhBvC,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAkD;IACvF,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAoC;IACpE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqC;IACtE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA8B;IAC/D,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAyB;IAC3D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,cAAc,CAA+B;IACrD,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,YAAY,CAA4B;IAChD,OAAO,CAAC,eAAe,CAA4B;gBAGhC,OAAO,EAAE,4BAA4B,EACrC,gBAAgB,EAAE,SAAS,EAC3B,eAAe,EAAE,SAAS,cAAc,EAAE,EAC1C,MAAM,EAAE,iBAAiB,EACzB,oBAAoB,GAAE,oBAAuD;IAK1F,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIvC,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAItC;;;;;;;OAOG;IACG,OAAO,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;IA2B9D;;;;OAIG;IACH,4BAA4B;YAYd,aAAa;YAwBb,cAAc;YAgBd,oBAAoB;YASpB,kBAAkB;IAgBhC,OAAO,CAAC,cAAc;YAQR,iBAAiB;YAOjB,yBAAyB;IAyBvC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,yBAAyB;IAUjC,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,SAAS;YAgBH,kCAAkC;YAkBlC,qBAAqB;YAiBrB,aAAa;YAOb,oBAAoB;IAyBlC,OAAO,CAAC,sBAAsB;YAQhB,QAAQ;YAyBR,sBAAsB;YAgBtB,yBAAyB;YAsBzB,cAAc;YAkBd,aAAa;YAQb,uBAAuB;CAOtC"}
|
package/dist/service.js
CHANGED
|
@@ -7,13 +7,16 @@ function _checkInRHS(e) { if (Object(e) !== e) throw TypeError("right-hand side
|
|
|
7
7
|
import { Inject } from '@fluojs/core';
|
|
8
8
|
import { cloneWithFallback } from '@fluojs/core/internal';
|
|
9
9
|
import { getRedisClientToken, getRedisComponentId } from '@fluojs/redis';
|
|
10
|
-
import { APPLICATION_LOGGER, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
10
|
+
import { APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL, COMPILED_MODULES, RUNTIME_CONTAINER } from '@fluojs/runtime/internal';
|
|
11
11
|
import { Queue as BullQueue, Worker as BullWorker } from 'bullmq';
|
|
12
12
|
import { QueueDeadLetterManager } from './dead-letter-manager.js';
|
|
13
|
-
import { normalizePositiveInteger } from './helpers.js';
|
|
13
|
+
import { normalizePositiveInteger, withTimeout } from './helpers.js';
|
|
14
14
|
import { createQueuePlatformStatusSnapshot } from './status.js';
|
|
15
15
|
import { QUEUE_OPTIONS } from './tokens.js';
|
|
16
16
|
import { discoverQueueWorkerDescriptors } from './worker-discovery.js';
|
|
17
|
+
const IMMEDIATE_BOOTSTRAP_READY_SIGNAL = {
|
|
18
|
+
wait: () => Promise.resolve()
|
|
19
|
+
};
|
|
17
20
|
function hasQueueRedisClient(value) {
|
|
18
21
|
if (typeof value !== 'object' || value === null) {
|
|
19
22
|
return false;
|
|
@@ -66,22 +69,24 @@ async function closeConnection(connection) {
|
|
|
66
69
|
let _QueueLifecycleServic;
|
|
67
70
|
class QueueLifecycleService {
|
|
68
71
|
static {
|
|
69
|
-
[_QueueLifecycleServic, _initClass] = _applyDecs(this, [Inject(QUEUE_OPTIONS, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER)], []).c;
|
|
72
|
+
[_QueueLifecycleServic, _initClass] = _applyDecs(this, [Inject(QUEUE_OPTIONS, RUNTIME_CONTAINER, COMPILED_MODULES, APPLICATION_LOGGER, BOOTSTRAP_READY_SIGNAL)], []).c;
|
|
70
73
|
}
|
|
71
74
|
descriptorsByJobType = new Map();
|
|
72
75
|
queuesByJobName = new Map();
|
|
73
76
|
workersByJobName = new Map();
|
|
74
77
|
ownedConnections = [];
|
|
75
78
|
deadLetterManager;
|
|
79
|
+
readyWorkers = [];
|
|
76
80
|
lifecycleState = 'idle';
|
|
77
81
|
redisClient;
|
|
78
82
|
startPromise;
|
|
79
83
|
shutdownPromise;
|
|
80
|
-
constructor(options, runtimeContainer, compiledModules, logger) {
|
|
84
|
+
constructor(options, runtimeContainer, compiledModules, logger, bootstrapReadySignal = IMMEDIATE_BOOTSTRAP_READY_SIGNAL) {
|
|
81
85
|
this.options = options;
|
|
82
86
|
this.runtimeContainer = runtimeContainer;
|
|
83
87
|
this.compiledModules = compiledModules;
|
|
84
88
|
this.logger = logger;
|
|
89
|
+
this.bootstrapReadySignal = bootstrapReadySignal;
|
|
85
90
|
this.deadLetterManager = new QueueDeadLetterManager(this.options, this.logger, () => this.getRedisClient());
|
|
86
91
|
}
|
|
87
92
|
async onApplicationBootstrap() {
|
|
@@ -104,6 +109,9 @@ class QueueLifecycleService {
|
|
|
104
109
|
*/
|
|
105
110
|
async enqueue(job) {
|
|
106
111
|
await this.ensureStarted();
|
|
112
|
+
if (this.lifecycleState !== 'started') {
|
|
113
|
+
throw new Error(`Queue lifecycle state is ${this.lifecycleState}.`);
|
|
114
|
+
}
|
|
107
115
|
const descriptor = this.descriptorsByJobType.get(job.constructor);
|
|
108
116
|
if (!descriptor) {
|
|
109
117
|
throw new Error(`No @QueueWorker() registered for job type ${job.constructor.name}.`);
|
|
@@ -130,6 +138,7 @@ class QueueLifecycleService {
|
|
|
130
138
|
lifecycleState: this.lifecycleState,
|
|
131
139
|
pendingDeadLetterWrites: this.deadLetterManager.pendingWriteCount,
|
|
132
140
|
queuesReady: this.queuesByJobName.size,
|
|
141
|
+
workerShutdownTimeoutMs: this.options.workerShutdownTimeoutMs,
|
|
133
142
|
workersDiscovered: this.descriptorsByJobType.size,
|
|
134
143
|
workersReady: this.workersByJobName.size
|
|
135
144
|
});
|
|
@@ -161,11 +170,16 @@ class QueueLifecycleService {
|
|
|
161
170
|
this.descriptorsByJobType.set(jobType, descriptor);
|
|
162
171
|
}
|
|
163
172
|
await this.initializeWorkers(redis);
|
|
164
|
-
this.lifecycleState
|
|
173
|
+
if (this.lifecycleState === 'starting') {
|
|
174
|
+
this.lifecycleState = 'started';
|
|
175
|
+
this.scheduleReadyWorkers();
|
|
176
|
+
}
|
|
165
177
|
}
|
|
166
178
|
async handleStartupFailure() {
|
|
167
179
|
await this.closeInitializedResources();
|
|
168
|
-
this.lifecycleState
|
|
180
|
+
if (this.lifecycleState === 'starting') {
|
|
181
|
+
this.lifecycleState = 'idle';
|
|
182
|
+
}
|
|
169
183
|
this.redisClient = undefined;
|
|
170
184
|
this.startPromise = undefined;
|
|
171
185
|
}
|
|
@@ -223,6 +237,7 @@ class QueueLifecycleService {
|
|
|
223
237
|
}
|
|
224
238
|
createWorkerOptions(descriptor, workerConnection) {
|
|
225
239
|
return {
|
|
240
|
+
autorun: false,
|
|
226
241
|
concurrency: descriptor.concurrency,
|
|
227
242
|
connection: workerConnection,
|
|
228
243
|
...this.createWorkerLimiterOptions(descriptor)
|
|
@@ -248,6 +263,38 @@ class QueueLifecycleService {
|
|
|
248
263
|
this.queuesByJobName.set(descriptor.jobName, resources.queue);
|
|
249
264
|
this.workersByJobName.set(descriptor.jobName, resources.worker);
|
|
250
265
|
this.ownedConnections.push(resources.queueConnection, resources.workerConnection);
|
|
266
|
+
this.readyWorkers.push({
|
|
267
|
+
descriptor,
|
|
268
|
+
worker: resources.worker
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
scheduleReadyWorkers() {
|
|
272
|
+
const workers = this.readyWorkers.splice(0);
|
|
273
|
+
void this.bootstrapReadySignal.wait().then(() => {
|
|
274
|
+
if (this.lifecycleState !== 'started') {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
for (const {
|
|
278
|
+
descriptor,
|
|
279
|
+
worker
|
|
280
|
+
} of workers) {
|
|
281
|
+
this.runWorker(descriptor, worker);
|
|
282
|
+
}
|
|
283
|
+
}).catch(error => {
|
|
284
|
+
if (this.lifecycleState !== 'started') {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
this.logger.error('Failed to start queue workers after application bootstrap readiness.', error, 'QueueLifecycleService');
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
runWorker(descriptor, worker) {
|
|
291
|
+
const runnableWorker = worker;
|
|
292
|
+
if (typeof runnableWorker.run !== 'function') {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
void Promise.resolve(runnableWorker.run()).catch(error => {
|
|
296
|
+
this.logger.error(`Failed to start queue worker ${descriptor.workerName} after application bootstrap.`, error, 'QueueLifecycleService');
|
|
297
|
+
});
|
|
251
298
|
}
|
|
252
299
|
async cleanupWorkerInitializationFailure(resources) {
|
|
253
300
|
if (resources.worker) {
|
|
@@ -314,19 +361,35 @@ class QueueLifecycleService {
|
|
|
314
361
|
}
|
|
315
362
|
this.lifecycleState = 'stopping';
|
|
316
363
|
this.shutdownPromise = (async () => {
|
|
364
|
+
await this.waitForInFlightStartup();
|
|
317
365
|
await this.closeInitializedResources();
|
|
318
366
|
await this.deadLetterManager.drainPendingWrites();
|
|
319
367
|
this.lifecycleState = 'stopped';
|
|
368
|
+
this.redisClient = undefined;
|
|
320
369
|
this.startPromise = undefined;
|
|
321
370
|
})();
|
|
322
371
|
await this.shutdownPromise;
|
|
323
372
|
}
|
|
373
|
+
async waitForInFlightStartup() {
|
|
374
|
+
const startup = this.startPromise;
|
|
375
|
+
if (!startup) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
await startup;
|
|
380
|
+
} catch {
|
|
381
|
+
// ensureStarted() owns startup rollback and preserves the original
|
|
382
|
+
// bootstrap error. Shutdown still continues so partially registered
|
|
383
|
+
// resources cannot outlive the application lifecycle.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
324
386
|
async closeInitializedResources() {
|
|
325
387
|
const workers = Array.from(this.workersByJobName.values());
|
|
326
388
|
const queues = Array.from(this.queuesByJobName.values());
|
|
327
389
|
const ownedConnections = this.ownedConnections.splice(0);
|
|
328
390
|
this.workersByJobName.clear();
|
|
329
391
|
this.queuesByJobName.clear();
|
|
392
|
+
this.readyWorkers.splice(0);
|
|
330
393
|
for (const worker of workers) {
|
|
331
394
|
await this.tryCloseWorker(worker);
|
|
332
395
|
}
|
|
@@ -339,9 +402,14 @@ class QueueLifecycleService {
|
|
|
339
402
|
}
|
|
340
403
|
async tryCloseWorker(worker) {
|
|
341
404
|
try {
|
|
342
|
-
await worker.close();
|
|
405
|
+
await withTimeout(worker.close(), this.options.workerShutdownTimeoutMs, () => new Error('queue worker shutdown timed out'));
|
|
343
406
|
} catch (error) {
|
|
344
|
-
this.logger.error('Failed to close queue worker
|
|
407
|
+
this.logger.error('Failed to close queue worker within shutdown timeout.', error, 'QueueLifecycleService');
|
|
408
|
+
try {
|
|
409
|
+
await worker.close(true);
|
|
410
|
+
} catch (forceCloseError) {
|
|
411
|
+
this.logger.error('Failed to force close queue worker during shutdown.', forceCloseError, 'QueueLifecycleService');
|
|
412
|
+
}
|
|
345
413
|
}
|
|
346
414
|
}
|
|
347
415
|
async tryCloseQueue(queue) {
|
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,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3F,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,mBAAmB,CAAC;IACpC,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,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;AAkFD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,uBAAuB,GAAG,2BAA2B,
|
|
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,sEAAsE;AACtE,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAE3F,oFAAoF;AACpF,MAAM,WAAW,uBAAuB;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,mBAAmB,CAAC;IACpC,uBAAuB,EAAE,MAAM,CAAC;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,uBAAuB,EAAE,MAAM,CAAC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,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;AAkFD;;;;;GAKG;AACH,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,uBAAuB,GAAG,2BAA2B,CAmB7G"}
|
package/dist/status.js
CHANGED
|
@@ -88,6 +88,7 @@ export function createQueuePlatformStatusSnapshot(input) {
|
|
|
88
88
|
lifecycleState: input.lifecycleState,
|
|
89
89
|
pendingDeadLetterWrites: input.pendingDeadLetterWrites,
|
|
90
90
|
queuesReady: input.queuesReady,
|
|
91
|
+
workerShutdownTimeoutMs: input.workerShutdownTimeoutMs,
|
|
91
92
|
workersDiscovered: input.workersDiscovered,
|
|
92
93
|
workersReady: input.workersReady
|
|
93
94
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface QueueModuleOptions {
|
|
|
38
38
|
defaultConcurrency?: number;
|
|
39
39
|
defaultDeadLetterMaxEntries?: number | false;
|
|
40
40
|
defaultRateLimiter?: QueueRateLimiterOptions;
|
|
41
|
+
/** Maximum time shutdown waits for active worker processors before forcing worker close. Defaults to `30_000`. */
|
|
42
|
+
workerShutdownTimeoutMs?: number;
|
|
41
43
|
}
|
|
42
44
|
/** Normalized queue options resolved once during module registration. */
|
|
43
45
|
export interface NormalizedQueueModuleOptions {
|
|
@@ -47,6 +49,7 @@ export interface NormalizedQueueModuleOptions {
|
|
|
47
49
|
defaultConcurrency: number;
|
|
48
50
|
defaultDeadLetterMaxEntries: number | false;
|
|
49
51
|
defaultRateLimiter?: QueueRateLimiterOptions;
|
|
52
|
+
workerShutdownTimeoutMs: number;
|
|
50
53
|
}
|
|
51
54
|
/** Metadata captured by {@link QueueWorker} during decorator evaluation. */
|
|
52
55
|
export interface QueueWorkerMetadata {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,oFAAoF;AACpF,MAAM,WAAW,YAAY,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACxD,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;CAC9B;AAED,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,aAAa,CAAC;AAEvD,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,qEAAqE;AACrE,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2BAA2B,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7C,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,oFAAoF;AACpF,MAAM,WAAW,YAAY,CAAC,IAAI,SAAS,MAAM,GAAG,MAAM;IACxD,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;CAC9B;AAED,sEAAsE;AACtE,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,aAAa,CAAC;AAEvD,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,gBAAgB,CAAC;CACzB;AAED,qEAAqE;AACrE,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,uBAAuB,CAAC;CACvC;AAED,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,2BAA2B,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC7C,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IAC7C,kHAAkH;IAClH,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,yEAAyE;AACzE,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,mBAAmB,CAAC;IACrC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,2BAA2B,EAAE,MAAM,GAAG,KAAK,CAAC;IAC5C,kBAAkB,CAAC,EAAE,uBAAuB,CAAC;IAC7C,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAED,4EAA4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,kBAAkB,CAAC;CAC7B;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,YAAY,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,uBAAuB,CAAC;IACtC,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,yEAAyE;AACzE,MAAM,WAAW,KAAK;IACpB;;;;;OAKG;IACH,OAAO,CAAC,IAAI,SAAS,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC1D"}
|
package/package.json
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"redis",
|
|
11
11
|
"dlq"
|
|
12
12
|
],
|
|
13
|
-
"version": "1.0.0
|
|
13
|
+
"version": "1.0.0",
|
|
14
14
|
"private": false,
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"repository": {
|
|
@@ -38,10 +38,10 @@
|
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"bullmq": "^5.58.0",
|
|
41
|
-
"@fluojs/core": "^1.0.0
|
|
42
|
-
"@fluojs/
|
|
43
|
-
"@fluojs/
|
|
44
|
-
"@fluojs/
|
|
41
|
+
"@fluojs/core": "^1.0.0",
|
|
42
|
+
"@fluojs/redis": "^1.0.0",
|
|
43
|
+
"@fluojs/runtime": "^1.0.0",
|
|
44
|
+
"@fluojs/di": "^1.0.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"vitest": "^3.2.4"
|