atomic-queues 1.0.16 → 1.1.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.md +373 -71
- package/dist/decorators/decorators.d.ts +204 -19
- package/dist/decorators/decorators.d.ts.map +1 -1
- package/dist/decorators/decorators.js +245 -25
- package/dist/decorators/decorators.js.map +1 -1
- package/dist/module/atomic-queues.module.d.ts.map +1 -1
- package/dist/module/atomic-queues.module.js +13 -2
- package/dist/module/atomic-queues.module.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/processor-discovery/index.d.ts +2 -0
- package/dist/services/processor-discovery/index.d.ts.map +1 -0
- package/dist/services/processor-discovery/index.js +18 -0
- package/dist/services/processor-discovery/index.js.map +1 -0
- package/dist/services/processor-discovery/processor-discovery.service.d.ts +141 -0
- package/dist/services/processor-discovery/processor-discovery.service.d.ts.map +1 -0
- package/dist/services/processor-discovery/processor-discovery.service.js +378 -0
- package/dist/services/processor-discovery/processor-discovery.service.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A plug-and-play NestJS library for atomic process handling per entity with BullM
|
|
|
4
4
|
|
|
5
5
|
## Overview
|
|
6
6
|
|
|
7
|
-
`atomic-queues` provides a unified architecture for handling atomic, sequential processing of jobs on a per-entity basis. It abstracts the complexity of managing dynamic queues, workers, and distributed locking into a simple, declarative API
|
|
7
|
+
`atomic-queues` provides a unified architecture for handling atomic, sequential processing of jobs on a per-entity basis. It abstracts the complexity of managing dynamic queues, workers, and distributed locking into a simple, **declarative decorator-based API**.
|
|
8
8
|
|
|
9
9
|
### Problem It Solves
|
|
10
10
|
|
|
@@ -18,18 +18,359 @@ This library solves all of these with a single, cohesive module.
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- **Decorator-based API**: Use `@WorkerProcessor` and `@JobHandler` for declarative job routing
|
|
24
|
+
- **Auto-discovery**: Processors and scalers are automatically discovered and registered
|
|
25
|
+
- **Dynamic Per-Entity Queues**: Automatically create and manage queues for each entity
|
|
26
|
+
- **Worker Lifecycle Management**: Heartbeat-based worker tracking with TTL expiration
|
|
27
|
+
- **Distributed Resource Locking**: Atomic lock acquisition
|
|
28
|
+
- **Graceful Shutdown**: Coordinated shutdown via Redis pub/sub across cluster nodes
|
|
29
|
+
- **Cron-based Scaling**: Automatic worker spawning and termination based on demand
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install atomic-queues bullmq ioredis
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Start (Decorator-based API) ✨
|
|
42
|
+
|
|
43
|
+
The recommended way to use `atomic-queues` is with the decorator-based API for clean, declarative code.
|
|
44
|
+
|
|
45
|
+
### 1. Import the Module
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { Module } from '@nestjs/common';
|
|
49
|
+
import { AtomicQueuesModule } from 'atomic-queues';
|
|
50
|
+
|
|
51
|
+
@Module({
|
|
52
|
+
imports: [
|
|
53
|
+
AtomicQueuesModule.forRootAsync({
|
|
54
|
+
imports: [ConfigModule],
|
|
55
|
+
useFactory: (configService: ConfigService) => ({
|
|
56
|
+
redis: {
|
|
57
|
+
url: configService.get('REDIS_URL'),
|
|
58
|
+
},
|
|
59
|
+
keyPrefix: 'myapp',
|
|
60
|
+
enableCronManager: true,
|
|
61
|
+
workerDefaults: {
|
|
62
|
+
concurrency: 1,
|
|
63
|
+
heartbeatTTL: 3,
|
|
64
|
+
},
|
|
65
|
+
}),
|
|
66
|
+
inject: [ConfigService],
|
|
67
|
+
}),
|
|
68
|
+
],
|
|
69
|
+
})
|
|
70
|
+
export class AppModule {}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 2. Create a Worker Processor
|
|
74
|
+
|
|
75
|
+
Use `@WorkerProcessor` to define a processor class and `@JobHandler` to route jobs to methods:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
import { Injectable } from '@nestjs/common';
|
|
79
|
+
import { CommandBus } from '@nestjs/cqrs';
|
|
80
|
+
import { Job } from 'bullmq';
|
|
81
|
+
import { WorkerProcessor, JobHandler } from 'atomic-queues';
|
|
82
|
+
|
|
83
|
+
@WorkerProcessor({
|
|
84
|
+
entityType: 'order',
|
|
85
|
+
queueName: (orderId) => `order-${orderId}-queue`,
|
|
86
|
+
workerName: (orderId) => `order-${orderId}-worker`,
|
|
87
|
+
workerConfig: {
|
|
88
|
+
concurrency: 1,
|
|
89
|
+
heartbeatTTL: 3,
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
@Injectable()
|
|
93
|
+
export class OrderWorkerProcessor {
|
|
94
|
+
constructor(private readonly commandBus: CommandBus) {}
|
|
95
|
+
|
|
96
|
+
@JobHandler('validate')
|
|
97
|
+
async handleValidate(job: Job, orderId: string) {
|
|
98
|
+
const { items } = job.data;
|
|
99
|
+
return this.commandBus.execute(new ValidateOrderCommand(orderId, items));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@JobHandler('process-payment')
|
|
103
|
+
async handlePayment(job: Job, orderId: string) {
|
|
104
|
+
const { amount } = job.data;
|
|
105
|
+
return this.commandBus.execute(new ProcessPaymentCommand(orderId, amount));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
@JobHandler('ship')
|
|
109
|
+
async handleShip(job: Job, orderId: string) {
|
|
110
|
+
return this.commandBus.execute(new ShipOrderCommand(orderId));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Wildcard handler for any unmatched job names
|
|
114
|
+
@JobHandler('*')
|
|
115
|
+
async handleOther(job: Job, orderId: string) {
|
|
116
|
+
console.log(`Unknown job type: ${job.name} for order ${orderId}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### 3. Create an Entity Scaler
|
|
122
|
+
|
|
123
|
+
Use `@EntityScaler` to define scaling logic with decorated methods:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { Injectable } from '@nestjs/common';
|
|
127
|
+
import { EntityScaler, GetActiveEntities, GetDesiredWorkerCount } from 'atomic-queues';
|
|
128
|
+
|
|
129
|
+
@EntityScaler({
|
|
130
|
+
entityType: 'order',
|
|
131
|
+
maxWorkersPerEntity: 1,
|
|
132
|
+
})
|
|
133
|
+
@Injectable()
|
|
134
|
+
export class OrderEntityScaler {
|
|
135
|
+
constructor(private readonly orderRepository: OrderRepository) {}
|
|
136
|
+
|
|
137
|
+
@GetActiveEntities()
|
|
138
|
+
async getActiveOrders(): Promise<string[]> {
|
|
139
|
+
// Return order IDs that have pending work
|
|
140
|
+
return this.orderRepository.findPendingOrderIds();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@GetDesiredWorkerCount()
|
|
144
|
+
async getWorkerCount(orderId: string): Promise<number> {
|
|
145
|
+
// Each order gets exactly 1 worker
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 4. Register in Your Module
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
@Module({
|
|
155
|
+
imports: [AtomicQueuesModule.forRootAsync({ ... })],
|
|
156
|
+
providers: [
|
|
157
|
+
OrderWorkerProcessor, // Auto-discovered by @WorkerProcessor
|
|
158
|
+
OrderEntityScaler, // Auto-discovered by @EntityScaler
|
|
159
|
+
],
|
|
160
|
+
})
|
|
161
|
+
export class OrderModule {}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 5. Queue Jobs
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { Injectable } from '@nestjs/common';
|
|
168
|
+
import { QueueManagerService } from 'atomic-queues';
|
|
169
|
+
|
|
170
|
+
@Injectable()
|
|
171
|
+
export class OrderService {
|
|
172
|
+
constructor(private readonly queueManager: QueueManagerService) {}
|
|
173
|
+
|
|
174
|
+
async createOrder(orderId: string, items: any[], amount: number) {
|
|
175
|
+
const queue = this.queueManager.getOrCreateQueue(`order-${orderId}-queue`);
|
|
176
|
+
|
|
177
|
+
// Jobs are processed in order (FIFO) by the worker
|
|
178
|
+
await queue.add('validate', { items });
|
|
179
|
+
await queue.add('process-payment', { amount });
|
|
180
|
+
await queue.add('ship', {});
|
|
181
|
+
|
|
182
|
+
return orderId;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
That's it! The library will:
|
|
188
|
+
1. **Auto-discover** your `OrderWorkerProcessor` and `OrderEntityScaler`
|
|
189
|
+
2. **Create workers** for active jobs via `CronManagerService`
|
|
190
|
+
3. **Route jobs** to the correct `@JobHandler` method
|
|
191
|
+
4. **Clean up** workers when jobs are complete
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Decorators Reference
|
|
196
|
+
|
|
197
|
+
### @WorkerProcessor(options)
|
|
198
|
+
|
|
199
|
+
Class decorator that marks a service as a worker processor for an entity type.
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
@WorkerProcessor({
|
|
203
|
+
entityType: string; // Required: Entity type (e.g., 'order', 'user')
|
|
204
|
+
queueName?: string | ((entityId: string) => string); // Queue name or function
|
|
205
|
+
workerName?: string | ((entityId: string) => string); // Worker name or function
|
|
206
|
+
workerConfig?: {
|
|
207
|
+
concurrency?: number; // Default: 1
|
|
208
|
+
stalledInterval?: number; // Default: 1000ms
|
|
209
|
+
lockDuration?: number; // Default: 30000ms
|
|
210
|
+
heartbeatTTL?: number; // Default: 3 seconds
|
|
211
|
+
heartbeatInterval?: number; // Default: 1000ms
|
|
212
|
+
};
|
|
213
|
+
})
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### @JobHandler(jobName)
|
|
217
|
+
|
|
218
|
+
Method decorator that routes jobs with a specific name to this handler.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
@JobHandler('validate') // Handles jobs named 'validate'
|
|
222
|
+
async handleValidate(job: Job, entityId: string) { ... }
|
|
223
|
+
|
|
224
|
+
@JobHandler('*') // Wildcard: handles any unmatched job
|
|
225
|
+
async handleOther(job: Job, entityId: string) { ... }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### @EntityScaler(options)
|
|
229
|
+
|
|
230
|
+
Class decorator for entity scaling configuration.
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
@EntityScaler({
|
|
234
|
+
entityType: string; // Required: Entity type to scale
|
|
235
|
+
maxWorkersPerEntity?: number; // Default: 1
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### @GetActiveEntities()
|
|
240
|
+
|
|
241
|
+
Method decorator marking the method that returns active entity IDs.
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
@GetActiveEntities()
|
|
245
|
+
async getActiveOrders(): Promise<string[]> {
|
|
246
|
+
return ['order-1', 'order-2'];
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### @GetDesiredWorkerCount()
|
|
251
|
+
|
|
252
|
+
Method decorator for desired worker count calculation.
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
@GetDesiredWorkerCount()
|
|
256
|
+
async getWorkerCount(entityId: string): Promise<number> {
|
|
257
|
+
return 1;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### @OnSpawnWorker() / @OnTerminateWorker()
|
|
22
262
|
|
|
23
|
-
|
|
263
|
+
Optional method decorators for custom spawn/terminate logic.
|
|
24
264
|
|
|
265
|
+
```typescript
|
|
266
|
+
@OnSpawnWorker()
|
|
267
|
+
async customSpawn(entityId: string): Promise<void> {
|
|
268
|
+
console.log(`Spawning worker for ${entityId}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
@OnTerminateWorker()
|
|
272
|
+
async customTerminate(entityId: string, workerId: string): Promise<void> {
|
|
273
|
+
console.log(`Terminating worker ${workerId} for ${entityId}`);
|
|
274
|
+
}
|
|
25
275
|
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Migration Guide
|
|
280
|
+
|
|
281
|
+
### Migrating from Manual Registration to Decorators
|
|
282
|
+
|
|
283
|
+
**Before (Manual Registration):**
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// order-job.processor.ts (one file per job type)
|
|
287
|
+
@Injectable()
|
|
288
|
+
@JobProcessor('validate-order')
|
|
289
|
+
export class ValidateOrderProcessor {
|
|
290
|
+
async process(job: Job) {
|
|
291
|
+
// validation logic
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// order-worker.service.ts (manual worker creation)
|
|
296
|
+
@Injectable()
|
|
297
|
+
export class OrderWorkerService {
|
|
298
|
+
constructor(
|
|
299
|
+
private workerManager: WorkerManagerService,
|
|
300
|
+
private jobRegistry: JobProcessorRegistry,
|
|
301
|
+
) {}
|
|
302
|
+
|
|
303
|
+
async createOrderWorker(orderId: string) {
|
|
304
|
+
await this.workerManager.createWorker({
|
|
305
|
+
workerName: `order-${orderId}-worker`,
|
|
306
|
+
queueName: `order-${orderId}-queue`,
|
|
307
|
+
processor: async (job) => {
|
|
308
|
+
const processor = this.jobRegistry.getProcessor(job.name);
|
|
309
|
+
await processor.process(job);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// app.module.ts (manual entity type registration)
|
|
316
|
+
cronManager.registerEntityType({
|
|
317
|
+
entityType: 'order',
|
|
318
|
+
getActiveEntityIds: async () => [...],
|
|
319
|
+
getDesiredWorkerCount: async (id) => 1,
|
|
320
|
+
onSpawnWorker: async (id) => orderWorkerService.createOrderWorker(id),
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**After (Decorator-based):**
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// table-worker.processor.ts (single file with all handlers)
|
|
328
|
+
@WorkerProcessor({
|
|
329
|
+
entityType: 'order',
|
|
330
|
+
queueName: (id) => `order-${id}-queue`,
|
|
331
|
+
workerName: (id) => `order-${id}-worker`,
|
|
332
|
+
})
|
|
333
|
+
@Injectable()
|
|
334
|
+
export class OrderWorkerProcessor {
|
|
335
|
+
@JobHandler('validate-order')
|
|
336
|
+
async handleValidate(job: Job, orderId: string) {
|
|
337
|
+
// validation logic
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
@JobHandler('process-payment')
|
|
341
|
+
async handlePayment(job: Job, orderId: string) {
|
|
342
|
+
// payment logic
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// table-entity.scaler.ts (scaling config in one place)
|
|
347
|
+
@EntityScaler({ entityType: 'order', maxWorkersPerEntity: 1 })
|
|
348
|
+
@Injectable()
|
|
349
|
+
export class OrderEntityScaler {
|
|
350
|
+
@GetActiveEntities()
|
|
351
|
+
async getActiveOrders(): Promise<string[]> { return [...]; }
|
|
352
|
+
|
|
353
|
+
@GetDesiredWorkerCount()
|
|
354
|
+
async getWorkerCount(id: string): Promise<number> { return 1; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// app.module.ts (just provide the classes, auto-discovery handles the rest)
|
|
358
|
+
@Module({
|
|
359
|
+
providers: [OrderWorkerProcessor, OrderEntityScaler],
|
|
360
|
+
})
|
|
361
|
+
export class OrderModule {}
|
|
29
362
|
```
|
|
30
363
|
|
|
31
|
-
|
|
32
|
-
|
|
364
|
+
### Key Benefits of Migration
|
|
365
|
+
|
|
366
|
+
| Aspect | Manual API | Decorator API |
|
|
367
|
+
|--------|-----------|---------------|
|
|
368
|
+
| **Job routing** | Manual switch/case or registry lookup | Automatic via `@JobHandler` |
|
|
369
|
+
| **Worker creation** | Explicit service method | Auto-generated by library |
|
|
370
|
+
| **Scaling config** | Imperative `registerEntityType()` call | Declarative `@EntityScaler` class |
|
|
371
|
+
| **Entity ID access** | Manual parsing from job data | Injected as method parameter |
|
|
372
|
+
| **Code organization** | Multiple files and services | Single processor class per entity type |
|
|
373
|
+
| **Registration** | Manual in `onModuleInit` | Auto-discovered at startup |
|
|
33
374
|
|
|
34
375
|
---
|
|
35
376
|
|
|
@@ -299,29 +640,11 @@ Customer B places Order 3 → [validate] → [pay] → [reserve] → [ship]
|
|
|
299
640
|
|
|
300
641
|
---
|
|
301
642
|
|
|
302
|
-
##
|
|
303
|
-
|
|
304
|
-
- **Dynamic Per-Entity Queues**: Automatically create and manage queues for each entity (user, order, session, etc.)
|
|
305
|
-
- **Worker Lifecycle Management**: Heartbeat-based worker tracking with TTL expiration
|
|
306
|
-
- **Distributed Resource Locking**: Atomic lock acquisition using Lua scripts
|
|
307
|
-
- **Graceful Shutdown**: Coordinated shutdown via Redis pub/sub across cluster nodes
|
|
308
|
-
- **Cron-based Scaling**: Automatic worker spawning and termination based on demand
|
|
309
|
-
- **Job Processor Registry**: Decorator-based job handler registration
|
|
310
|
-
- **Index Tracking**: Track jobs, workers, and queue states across entities
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
|
-
## Installation
|
|
315
|
-
|
|
316
|
-
```bash
|
|
317
|
-
npm install atomic-queues bullmq ioredis
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
---
|
|
643
|
+
## Manual API (Legacy)
|
|
321
644
|
|
|
322
|
-
|
|
645
|
+
The manual API is still available for advanced use cases or gradual migration. **For most use cases, prefer the decorator-based API above.**
|
|
323
646
|
|
|
324
|
-
### 1.
|
|
647
|
+
### 1. Module Configuration
|
|
325
648
|
|
|
326
649
|
```typescript
|
|
327
650
|
import { Module } from '@nestjs/common';
|
|
@@ -343,35 +666,7 @@ import { AtomicQueuesModule } from 'atomic-queues';
|
|
|
343
666
|
export class AppModule {}
|
|
344
667
|
```
|
|
345
668
|
|
|
346
|
-
### 2.
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
import { Module } from '@nestjs/common';
|
|
350
|
-
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
351
|
-
import { AtomicQueuesModule } from 'atomic-queues';
|
|
352
|
-
|
|
353
|
-
@Module({
|
|
354
|
-
imports: [
|
|
355
|
-
AtomicQueuesModule.forRootAsync({
|
|
356
|
-
imports: [ConfigModule],
|
|
357
|
-
useFactory: (configService: ConfigService) => ({
|
|
358
|
-
redis: {
|
|
359
|
-
url: configService.get('REDIS_URL'),
|
|
360
|
-
},
|
|
361
|
-
enableCronManager: true,
|
|
362
|
-
workerDefaults: {
|
|
363
|
-
concurrency: 1,
|
|
364
|
-
heartbeatTTL: 3,
|
|
365
|
-
},
|
|
366
|
-
}),
|
|
367
|
-
inject: [ConfigService],
|
|
368
|
-
}),
|
|
369
|
-
],
|
|
370
|
-
})
|
|
371
|
-
export class AppModule {}
|
|
372
|
-
```
|
|
373
|
-
|
|
374
|
-
### 3. Register Job Processors
|
|
669
|
+
### 2. Register Job Processors Manually
|
|
375
670
|
|
|
376
671
|
```typescript
|
|
377
672
|
import { Injectable } from '@nestjs/common';
|
|
@@ -388,20 +683,9 @@ export class ValidateOrderProcessor {
|
|
|
388
683
|
await this.commandBus.execute(new ValidateOrderCommand(orderId, items));
|
|
389
684
|
}
|
|
390
685
|
}
|
|
391
|
-
|
|
392
|
-
@Injectable()
|
|
393
|
-
@JobProcessor('process-payment')
|
|
394
|
-
export class ProcessPaymentProcessor {
|
|
395
|
-
constructor(private readonly commandBus: CommandBus) {}
|
|
396
|
-
|
|
397
|
-
async process(job: Job) {
|
|
398
|
-
const { orderId, amount } = job.data;
|
|
399
|
-
await this.commandBus.execute(new ProcessPaymentCommand(orderId, amount));
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
686
|
```
|
|
403
687
|
|
|
404
|
-
###
|
|
688
|
+
### 3. Queue Jobs Manually
|
|
405
689
|
|
|
406
690
|
```typescript
|
|
407
691
|
import { Injectable } from '@nestjs/common';
|
|
@@ -431,7 +715,7 @@ export class OrderService {
|
|
|
431
715
|
}
|
|
432
716
|
```
|
|
433
717
|
|
|
434
|
-
###
|
|
718
|
+
### 4. Create Workers Manually
|
|
435
719
|
|
|
436
720
|
```typescript
|
|
437
721
|
import { Injectable } from '@nestjs/common';
|
|
@@ -556,6 +840,24 @@ const available = await lockService.getAvailableResource(
|
|
|
556
840
|
|
|
557
841
|
Automatic worker scaling based on demand.
|
|
558
842
|
|
|
843
|
+
**Recommended: Use `@EntityScaler` decorator (see Quick Start section above)**
|
|
844
|
+
|
|
845
|
+
The decorator-based approach is preferred as it's cleaner and auto-discovered:
|
|
846
|
+
|
|
847
|
+
```typescript
|
|
848
|
+
@EntityScaler({ entityType: 'order', maxWorkersPerEntity: 1 })
|
|
849
|
+
@Injectable()
|
|
850
|
+
export class OrderEntityScaler {
|
|
851
|
+
@GetActiveEntities()
|
|
852
|
+
async getActiveOrders(): Promise<string[]> { ... }
|
|
853
|
+
|
|
854
|
+
@GetDesiredWorkerCount()
|
|
855
|
+
async getWorkerCount(orderId: string): Promise<number> { return 1; }
|
|
856
|
+
}
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
**Legacy API (Manual Registration):**
|
|
860
|
+
|
|
559
861
|
```typescript
|
|
560
862
|
// Register entity type for automatic scaling
|
|
561
863
|
cronManager.registerEntityType({
|