@fluojs/queue 1.0.0-beta.5 → 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 CHANGED
@@ -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
  워커 설정에서 최대 시도 횟수와 백오프 전략을 지정하여 일시적인 실패를 자동으로 처리할 수 있습니다.
@@ -123,11 +129,26 @@ Job은 JSON으로 직렬화 가능한 plain object여야 합니다. Queue는 enq
123
129
 
124
130
 
125
131
  ### 타입
126
- - `QueueModuleOptions`: 전역 설정(clientName, 기본 시도 횟수, `defaultBackoff`, 동시성, 전송률 제한, dead-letter retention 등) 위한 타입입니다.
132
+ - `Queue`: 애플리케이션 코드와 `QUEUE` 토큰에서 사용하는 `enqueue(job)` 호환성 facade입니다.
133
+ - `QueueJobType`: job payload class를 식별하고 rehydrate하는 데 사용하는 constructor 타입입니다.
134
+ - `QueueModuleOptions`: 전역 큐 설정(`global`, clientName, 기본 시도 횟수, `defaultBackoff`, 동시성, 전송률 제한, dead-letter retention 등)을 위한 타입입니다.
127
135
  - `QueueWorkerOptions`: 개별 작업 설정(시도 횟수, 백오프, 동시성, jobName, 전송률 제한 등)을 위한 타입입니다.
136
+ - `QueueBackoffType`: 지원되는 retry backoff strategy 이름(`fixed`, `exponential`)입니다.
128
137
  - `QueueBackoffOptions`: 재시도 백오프 설정(`type`, `delayMs`)을 위한 타입입니다.
138
+ - `QueueRateLimiterOptions`: worker 수준 distributed rate limiter 설정(`max`, `duration`)을 위한 타입입니다.
139
+ - `QueueLifecycleState`: Queue status adapter가 보고하는 lifecycle state(`idle`, `starting`, `started`, `stopping`, `stopped`)입니다.
140
+ - `QueueStatusAdapterInput`: `createQueuePlatformStatusSnapshot(...)`에 전달하는 normalized queue metrics 타입입니다.
141
+ - `QueuePlatformStatusSnapshot`: status helper가 반환하는 Queue 전용 readiness, health, ownership, detail snapshot 타입입니다.
142
+
143
+ `QueueModuleOptions`에는 `workerShutdownTimeoutMs`, `defaultDeadLetterMaxEntries` 같은 lifecycle 및 dead-letter retention 설정도 포함됩니다.
144
+
145
+ `QueueModuleOptions` 수명 주기/status 설정:
146
+
147
+ - `global`: queue module 등록을 global로 만들지 여부입니다. 기본값은 `true`이며, queue provider를 importing module graph 안에만 scope하고 싶으면 `false`를 지정합니다.
148
+ - `workerShutdownTimeoutMs`: 종료 중 active worker processor를 기다리는 최대 시간입니다. 시간이 지나면 BullMQ worker를 force-close합니다. 기본값은 `30_000`입니다.
149
+ - `defaultDeadLetterMaxEntries`: job별로 유지할 dead-letter record의 최대 개수이며, trimming을 끄려면 `false`를 지정합니다. 기본값은 `1_000`입니다.
129
150
 
130
- `QueueModuleOptions`에는 `defaultDeadLetterMaxEntries` 같은 dead-letter retention 설정도 포함됩니다.
151
+ `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
152
 
132
153
  singleton `@QueueWorker()` provider/controller만 등록됩니다. request/transient worker는 discovery 중 건너뜁니다.
133
154
 
package/README.md CHANGED
@@ -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.
@@ -123,11 +129,26 @@ Treat low-level provider assembly as an internal implementation detail: low-leve
123
129
 
124
130
 
125
131
  ### Types
126
- - `QueueModuleOptions`: Global queue settings (clientName, default attempts, `defaultBackoff`, concurrency, rate limiting, dead-letter retention).
132
+ - `Queue`: Compatibility facade with `enqueue(job)` for application code and the `QUEUE` token.
133
+ - `QueueJobType`: Constructor type used to identify and rehydrate a job payload class.
134
+ - `QueueModuleOptions`: Global queue settings (`global`, clientName, default attempts, `defaultBackoff`, concurrency, rate limiting, dead-letter retention).
127
135
  - `QueueWorkerOptions`: Per-job settings (attempts, backoff, concurrency, jobName, rate limiting).
136
+ - `QueueBackoffType`: Supported retry backoff strategy names (`fixed`, `exponential`).
128
137
  - `QueueBackoffOptions`: Retry backoff settings (`type`, `delayMs`).
138
+ - `QueueRateLimiterOptions`: Worker-level distributed rate limiter settings (`max`, `duration`).
139
+ - `QueueLifecycleState`: Lifecycle states reported by Queue status adapters (`idle`, `starting`, `started`, `stopping`, `stopped`).
140
+ - `QueueStatusAdapterInput`: Normalized queue metrics passed to `createQueuePlatformStatusSnapshot(...)`.
141
+ - `QueuePlatformStatusSnapshot`: Queue-specific readiness, health, ownership, and detail snapshot returned by the status helper.
142
+
143
+ `QueueModuleOptions` also includes lifecycle and dead-letter retention controls such as `workerShutdownTimeoutMs` and `defaultDeadLetterMaxEntries`.
144
+
145
+ `QueueModuleOptions` lifecycle/status controls:
146
+
147
+ - `global`: whether the queue module registration is global. Defaults to `true`; set `false` when queue providers should stay scoped to the importing module graph.
148
+ - `workerShutdownTimeoutMs`: maximum time to wait for active worker processors during shutdown before force-closing the BullMQ worker. Defaults to `30_000`.
149
+ - `defaultDeadLetterMaxEntries`: maximum retained dead-letter records per job, or `false` to disable trimming. Defaults to `1_000`.
129
150
 
130
- `QueueModuleOptions` also includes dead-letter retention controls such as `defaultDeadLetterMaxEntries`.
151
+ `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
152
 
132
153
  Only singleton `@QueueWorker()` providers/controllers are registered. Request/transient workers are skipped during discovery.
133
154
 
@@ -1 +1 @@
1
- {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAuB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExF,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAiBpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,kBAAkB,CAWvG"}
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAuB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExF,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAepF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,kBAAkB,CAavG"}
@@ -1,6 +1,5 @@
1
1
  import { ensureMetadataSymbol } from '@fluojs/core/internal';
2
2
  import { queueWorkerMetadataSymbol } from './metadata.js';
3
- ensureMetadataSymbol();
4
3
  function getStandardMetadataBag(metadata) {
5
4
  return metadata;
6
5
  }
@@ -39,6 +38,7 @@ function defineStandardQueueWorkerMetadata(metadata, workerMetadata) {
39
38
  */
40
39
  export function QueueWorker(jobType, options = {}) {
41
40
  const decorator = (_value, context) => {
41
+ ensureMetadataSymbol();
42
42
  const metadata = {
43
43
  jobType,
44
44
  options: {
@@ -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;AAqCnF;;GAEG;AACH,qBAAa,WAAW;IACtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,OAAO,CAAC,OAAO,GAAE,kBAAuB,GAAG,UAAU;CAS7D"}
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
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,6 +56,8 @@ 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;
@@ -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,KAAK,EACV,iBAAiB,EACjB,cAAc,EACd,sBAAsB,EACtB,qBAAqB,EACrB,eAAe,EAChB,MAAM,iBAAiB,CAAC;AASzB,OAAO,KAAK,EACV,4BAA4B,EAC5B,KAAK,EAIN,MAAM,YAAY,CAAC;AAkGpB;;;;;GAKG;AACH,qBACa,qBAAsB,YAAW,KAAK,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,eAAe;IAY/G,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,eAAe;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAdzB,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,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;IAKtC,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;YAWd,aAAa;YAwBb,cAAc;YAed,oBAAoB;YASpB,kBAAkB;IAgBhC,OAAO,CAAC,cAAc;YAQR,iBAAiB;YAOjB,yBAAyB;IAyBvC,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,oBAAoB;IAa5B,OAAO,CAAC,mBAAmB;IAkB3B,OAAO,CAAC,0BAA0B;IAkBlC,OAAO,CAAC,0BAA0B;IASlC,OAAO,CAAC,yBAAyB;YASnB,kCAAkC;YAkBlC,qBAAqB;YAiBrB,aAAa;YAOb,oBAAoB;IAyBlC,OAAO,CAAC,sBAAsB;YAQhB,QAAQ;YAyBR,sBAAsB;YAgBtB,yBAAyB;YAqBzB,cAAc;YAQd,aAAa;YAQb,uBAAuB;CAOtC"}
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() {
@@ -133,6 +138,7 @@ class QueueLifecycleService {
133
138
  lifecycleState: this.lifecycleState,
134
139
  pendingDeadLetterWrites: this.deadLetterManager.pendingWriteCount,
135
140
  queuesReady: this.queuesByJobName.size,
141
+ workerShutdownTimeoutMs: this.options.workerShutdownTimeoutMs,
136
142
  workersDiscovered: this.descriptorsByJobType.size,
137
143
  workersReady: this.workersByJobName.size
138
144
  });
@@ -166,6 +172,7 @@ class QueueLifecycleService {
166
172
  await this.initializeWorkers(redis);
167
173
  if (this.lifecycleState === 'starting') {
168
174
  this.lifecycleState = 'started';
175
+ this.scheduleReadyWorkers();
169
176
  }
170
177
  }
171
178
  async handleStartupFailure() {
@@ -230,6 +237,7 @@ class QueueLifecycleService {
230
237
  }
231
238
  createWorkerOptions(descriptor, workerConnection) {
232
239
  return {
240
+ autorun: false,
233
241
  concurrency: descriptor.concurrency,
234
242
  connection: workerConnection,
235
243
  ...this.createWorkerLimiterOptions(descriptor)
@@ -255,6 +263,38 @@ class QueueLifecycleService {
255
263
  this.queuesByJobName.set(descriptor.jobName, resources.queue);
256
264
  this.workersByJobName.set(descriptor.jobName, resources.worker);
257
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
+ });
258
298
  }
259
299
  async cleanupWorkerInitializationFailure(resources) {
260
300
  if (resources.worker) {
@@ -349,6 +389,7 @@ class QueueLifecycleService {
349
389
  const ownedConnections = this.ownedConnections.splice(0);
350
390
  this.workersByJobName.clear();
351
391
  this.queuesByJobName.clear();
392
+ this.readyWorkers.splice(0);
352
393
  for (const worker of workers) {
353
394
  await this.tryCloseWorker(worker);
354
395
  }
@@ -361,9 +402,14 @@ class QueueLifecycleService {
361
402
  }
362
403
  async tryCloseWorker(worker) {
363
404
  try {
364
- await worker.close();
405
+ await withTimeout(worker.close(), this.options.workerShutdownTimeoutMs, () => new Error('queue worker shutdown timed out'));
365
406
  } catch (error) {
366
- this.logger.error('Failed to close queue worker during shutdown.', error, 'QueueLifecycleService');
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
+ }
367
413
  }
368
414
  }
369
415
  async tryCloseQueue(queue) {
package/dist/status.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface QueueStatusAdapterInput {
7
7
  lifecycleState: QueueLifecycleState;
8
8
  pendingDeadLetterWrites: number;
9
9
  queuesReady: number;
10
+ workerShutdownTimeoutMs: number;
10
11
  workersDiscovered: number;
11
12
  workersReady: number;
12
13
  }
@@ -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,CAkB7G"}
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 {
@@ -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;CAC9C;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;CAC9C;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"}
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-beta.5",
13
+ "version": "1.0.1",
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-beta.5",
42
- "@fluojs/di": "^1.0.0-beta.7",
43
- "@fluojs/redis": "^1.0.0-beta.4",
44
- "@fluojs/runtime": "^1.0.0-beta.12"
41
+ "@fluojs/core": "^1.0.3",
42
+ "@fluojs/di": "^1.0.3",
43
+ "@fluojs/redis": "^1.0.1",
44
+ "@fluojs/runtime": "^1.1.2"
45
45
  },
46
46
  "devDependencies": {
47
47
  "vitest": "^3.2.4"