atomic-queues 1.0.13
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 +686 -0
- package/dist/decorators/decorators.d.ts +67 -0
- package/dist/decorators/decorators.d.ts.map +1 -0
- package/dist/decorators/decorators.js +91 -0
- package/dist/decorators/decorators.js.map +1 -0
- package/dist/decorators/index.d.ts +2 -0
- package/dist/decorators/index.d.ts.map +1 -0
- package/dist/decorators/index.js +18 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/domain/index.d.ts +5 -0
- package/dist/domain/index.d.ts.map +1 -0
- package/dist/domain/index.js +21 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/domain/interfaces.d.ts +614 -0
- package/dist/domain/interfaces.d.ts.map +1 -0
- package/dist/domain/interfaces.js +19 -0
- package/dist/domain/interfaces.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/module/atomic-queues.module.d.ts +97 -0
- package/dist/module/atomic-queues.module.d.ts.map +1 -0
- package/dist/module/atomic-queues.module.js +197 -0
- package/dist/module/atomic-queues.module.js.map +1 -0
- package/dist/module/index.d.ts +2 -0
- package/dist/module/index.d.ts.map +1 -0
- package/dist/module/index.js +18 -0
- package/dist/module/index.js.map +1 -0
- package/dist/services/constants.d.ts +10 -0
- package/dist/services/constants.d.ts.map +1 -0
- package/dist/services/constants.js +13 -0
- package/dist/services/constants.js.map +1 -0
- package/dist/services/cron-manager/cron-manager.service.d.ts +188 -0
- package/dist/services/cron-manager/cron-manager.service.d.ts.map +1 -0
- package/dist/services/cron-manager/cron-manager.service.js +534 -0
- package/dist/services/cron-manager/cron-manager.service.js.map +1 -0
- package/dist/services/cron-manager/index.d.ts +2 -0
- package/dist/services/cron-manager/index.d.ts.map +1 -0
- package/dist/services/cron-manager/index.js +18 -0
- package/dist/services/cron-manager/index.js.map +1 -0
- package/dist/services/index-manager/index-manager.service.d.ts +146 -0
- package/dist/services/index-manager/index-manager.service.d.ts.map +1 -0
- package/dist/services/index-manager/index-manager.service.js +337 -0
- package/dist/services/index-manager/index-manager.service.js.map +1 -0
- package/dist/services/index-manager/index.d.ts +2 -0
- package/dist/services/index-manager/index.d.ts.map +1 -0
- package/dist/services/index-manager/index.js +18 -0
- package/dist/services/index-manager/index.js.map +1 -0
- package/dist/services/index.d.ts +10 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +26 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/job-processor/index.d.ts +2 -0
- package/dist/services/job-processor/index.d.ts.map +1 -0
- package/dist/services/job-processor/index.js +18 -0
- package/dist/services/job-processor/index.js.map +1 -0
- package/dist/services/job-processor/job-processor.service.d.ts +156 -0
- package/dist/services/job-processor/job-processor.service.d.ts.map +1 -0
- package/dist/services/job-processor/job-processor.service.js +331 -0
- package/dist/services/job-processor/job-processor.service.js.map +1 -0
- package/dist/services/queue-manager/index.d.ts +2 -0
- package/dist/services/queue-manager/index.d.ts.map +1 -0
- package/dist/services/queue-manager/index.js +18 -0
- package/dist/services/queue-manager/index.js.map +1 -0
- package/dist/services/queue-manager/queue-manager.service.d.ts +128 -0
- package/dist/services/queue-manager/queue-manager.service.d.ts.map +1 -0
- package/dist/services/queue-manager/queue-manager.service.js +308 -0
- package/dist/services/queue-manager/queue-manager.service.js.map +1 -0
- package/dist/services/resource-lock/index.d.ts +2 -0
- package/dist/services/resource-lock/index.d.ts.map +1 -0
- package/dist/services/resource-lock/index.js +18 -0
- package/dist/services/resource-lock/index.js.map +1 -0
- package/dist/services/resource-lock/resource-lock.service.d.ts +124 -0
- package/dist/services/resource-lock/resource-lock.service.d.ts.map +1 -0
- package/dist/services/resource-lock/resource-lock.service.js +379 -0
- package/dist/services/resource-lock/resource-lock.service.js.map +1 -0
- package/dist/services/service-queue/index.d.ts +2 -0
- package/dist/services/service-queue/index.d.ts.map +1 -0
- package/dist/services/service-queue/index.js +18 -0
- package/dist/services/service-queue/index.js.map +1 -0
- package/dist/services/service-queue/service-queue.service.d.ts +232 -0
- package/dist/services/service-queue/service-queue.service.d.ts.map +1 -0
- package/dist/services/service-queue/service-queue.service.js +647 -0
- package/dist/services/service-queue/service-queue.service.js.map +1 -0
- package/dist/services/shutdown-state/index.d.ts +2 -0
- package/dist/services/shutdown-state/index.d.ts.map +1 -0
- package/dist/services/shutdown-state/index.js +18 -0
- package/dist/services/shutdown-state/index.js.map +1 -0
- package/dist/services/shutdown-state/shutdown-state.service.d.ts +69 -0
- package/dist/services/shutdown-state/shutdown-state.service.d.ts.map +1 -0
- package/dist/services/shutdown-state/shutdown-state.service.js +127 -0
- package/dist/services/shutdown-state/shutdown-state.service.js.map +1 -0
- package/dist/services/worker-manager/index.d.ts +2 -0
- package/dist/services/worker-manager/index.d.ts.map +1 -0
- package/dist/services/worker-manager/index.js +18 -0
- package/dist/services/worker-manager/index.js.map +1 -0
- package/dist/services/worker-manager/worker-manager.service.d.ts +163 -0
- package/dist/services/worker-manager/worker-manager.service.d.ts.map +1 -0
- package/dist/services/worker-manager/worker-manager.service.js +460 -0
- package/dist/services/worker-manager/worker-manager.service.js.map +1 -0
- package/dist/utils/helpers.d.ts +124 -0
- package/dist/utils/helpers.d.ts.map +1 -0
- package/dist/utils/helpers.js +229 -0
- package/dist/utils/helpers.js.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +18 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
# @nestjs/atomic-queues
|
|
2
|
+
|
|
3
|
+
A plug-and-play NestJS library for atomic process handling per entity with BullMQ, Redis distributed locking, and dynamic worker management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
`@nestjs/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.
|
|
8
|
+
|
|
9
|
+
### Problem It Solves
|
|
10
|
+
|
|
11
|
+
In distributed systems, you often need to:
|
|
12
|
+
- Process jobs **sequentially** for a specific entity (user, order, session)
|
|
13
|
+
- **Dynamically spawn workers** based on load
|
|
14
|
+
- **Prevent race conditions** when multiple services handle the same entity
|
|
15
|
+
- **Scale horizontally** while maintaining per-entity ordering guarantees
|
|
16
|
+
|
|
17
|
+
This library solves all of these with a single, cohesive module.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Example Scenario: Order Processing System
|
|
22
|
+
|
|
23
|
+
Imagine an e-commerce platform where each customer can place multiple orders. Each order goes through several stages: validation, payment, inventory reservation, and shipping. These stages **must happen in sequence** for each order, but different orders can be processed in parallel.
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
Customer A places Order 1 → [validate] → [pay] → [reserve] → [ship]
|
|
27
|
+
Customer A places Order 2 → [validate] → [pay] → [reserve] → [ship]
|
|
28
|
+
Customer B places Order 3 → [validate] → [pay] → [reserve] → [ship]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Without atomic queues**: Race conditions, duplicate payments, inventory overselling.
|
|
32
|
+
**With atomic queues**: Each order gets its own queue and worker, ensuring sequential processing.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
### High-Level Flow
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
┌─────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
42
|
+
│ @nestjs/atomic-queues ARCHITECTURE │
|
|
43
|
+
└─────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
44
|
+
|
|
45
|
+
┌─────────────────────┐
|
|
46
|
+
│ External Triggers │
|
|
47
|
+
│ (WebSocket, HTTP, │
|
|
48
|
+
│ Cron, Pub/Sub) │
|
|
49
|
+
└──────────┬──────────┘
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
53
|
+
│ APPLICATION LAYER │
|
|
54
|
+
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
|
|
55
|
+
│ │ QueueManagerService │ │
|
|
56
|
+
│ │ │ │
|
|
57
|
+
│ │ queueManager.addJob(entityQueue, jobName, { entityId, action, payload }) │ │
|
|
58
|
+
│ │ │ │
|
|
59
|
+
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
|
|
60
|
+
└──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
61
|
+
│
|
|
62
|
+
▼
|
|
63
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
64
|
+
│ REDIS (BullMQ) │
|
|
65
|
+
│ │
|
|
66
|
+
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
|
67
|
+
│ │ entity-A-q │ │ entity-B-q │ │ entity-C-q │ │ entity-N-q │ │
|
|
68
|
+
│ │ │ │ │ │ │ │ │ │
|
|
69
|
+
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
|
|
70
|
+
│ │ │ Job 1 │ │ │ │ Job 1 │ │ │ │ Job 1 │ │ │ │ Job 1 │ │ │
|
|
71
|
+
│ │ │ Job 2 │ │ │ │ Job 2 │ │ │ └─────────┘ │ │ │ Job 2 │ │ │
|
|
72
|
+
│ │ │ Job 3 │ │ │ └─────────┘ │ │ │ │ │ Job 3 │ │ │
|
|
73
|
+
│ │ │ ... │ │ │ │ │ │ │ │ ... │ │ │
|
|
74
|
+
│ │ └─────────┘ │ │ │ │ │ │ └─────────┘ │ │
|
|
75
|
+
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
|
|
76
|
+
│ │ │ │ │ │
|
|
77
|
+
└───────────┼────────────────────┼────────────────────┼────────────────────┼───────────────────┘
|
|
78
|
+
│ │ │ │
|
|
79
|
+
▼ ▼ ▼ ▼
|
|
80
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
81
|
+
│ WORKER LAYER (Per-Entity) │
|
|
82
|
+
│ │
|
|
83
|
+
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
|
|
84
|
+
│ │ Worker A │ │ Worker B │ │ Worker C │ │ Worker N │ │
|
|
85
|
+
│ │ concurrency=1 │ │ concurrency=1 │ │ concurrency=1 │ │ concurrency=1 │ │
|
|
86
|
+
│ │ │ │ │ │ │ │ │ │
|
|
87
|
+
│ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │ │
|
|
88
|
+
│ │ │Heartbeat│ │ │ │Heartbeat│ │ │ │Heartbeat│ │ │ │Heartbeat│ │ │
|
|
89
|
+
│ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │ │ TTL=3s │ │ │
|
|
90
|
+
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
|
|
91
|
+
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
|
|
92
|
+
│ │ │ │ │ │
|
|
93
|
+
│ │ WorkerManagerService (Lifecycle, Heartbeats, Shutdown Signals) │
|
|
94
|
+
│ └────────────────────┴────────────────────┴────────────────────┘ │
|
|
95
|
+
│ │ │
|
|
96
|
+
└──────────────────────────────────────────┼───────────────────────────────────────────────────┘
|
|
97
|
+
│
|
|
98
|
+
▼
|
|
99
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
100
|
+
│ JOB PROCESSOR SERVICE │
|
|
101
|
+
│ │
|
|
102
|
+
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
|
|
103
|
+
│ │ JobProcessorRegistry │ │
|
|
104
|
+
│ │ │ │
|
|
105
|
+
│ │ @JobProcessor('validate') @JobProcessor('pay') @JobProcessor('ship') │ │
|
|
106
|
+
│ │ class ValidateProcessor {} class PayProcessor {} class ShipProcessor {} │ │
|
|
107
|
+
│ │ │ │
|
|
108
|
+
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
|
|
109
|
+
│ │ │
|
|
110
|
+
│ ▼ │
|
|
111
|
+
│ ┌───────────────────────────────────────────────────────────────────────────────────────┐ │
|
|
112
|
+
│ │ CQRS CommandBus / QueryBus │ │
|
|
113
|
+
│ │ │ │
|
|
114
|
+
│ │ commandBus.execute(new ValidateOrderCommand(...)) │ │
|
|
115
|
+
│ │ commandBus.execute(new ProcessPaymentCommand(...)) │ │
|
|
116
|
+
│ │ │ │
|
|
117
|
+
│ └───────────────────────────────────────────────────────────────────────────────────────┘ │
|
|
118
|
+
│ │
|
|
119
|
+
└──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
┌──────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
123
|
+
│ SUPPORTING SERVICES │
|
|
124
|
+
│ │
|
|
125
|
+
│ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐ │
|
|
126
|
+
│ │ CronManagerService │ │ IndexManagerService │ │ ResourceLockService │ │
|
|
127
|
+
│ │ │ │ │ │ │ │
|
|
128
|
+
│ │ • Poll for entities │ │ • Track jobs per │ │ • Lua-based atomic │ │
|
|
129
|
+
│ │ needing workers │ │ entity │ │ locks │ │
|
|
130
|
+
│ │ • Spawn workers on │ │ • Track worker states │ │ • Lock pooling │ │
|
|
131
|
+
│ │ demand │ │ • Track queue states │ │ • TTL-based expiry │ │
|
|
132
|
+
│ │ • Terminate idle │ │ • Cleanup on entity │ │ • Owner tracking │ │
|
|
133
|
+
│ │ workers │ │ completion │ │ │ │
|
|
134
|
+
│ │ │ │ │ │ │ │
|
|
135
|
+
│ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ │
|
|
136
|
+
│ │
|
|
137
|
+
└──────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Detailed Component Interaction
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
144
|
+
│ COMPLETE JOB LIFECYCLE │
|
|
145
|
+
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
146
|
+
|
|
147
|
+
1. JOB CREATION 2. WORKER SPAWNING 3. JOB PROCESSING
|
|
148
|
+
───────────────── ────────────────── ─────────────────
|
|
149
|
+
|
|
150
|
+
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
151
|
+
│ Service │ │ CronManager │ │ Worker │
|
|
152
|
+
│ (HTTP/WS) │ │ Service │ │ (BullMQ) │
|
|
153
|
+
└──────┬──────┘ └────────┬────────┘ └────────┬────────┘
|
|
154
|
+
│ │ │
|
|
155
|
+
│ 1. Receive request │ 1. Every N seconds │ 1. Poll queue
|
|
156
|
+
│ (create order, etc) │ check entities │ for jobs
|
|
157
|
+
▼ │ with pending jobs │
|
|
158
|
+
┌─────────────┐ ▼ ▼
|
|
159
|
+
│ Queue │ ┌─────────────────┐ ┌─────────────────┐
|
|
160
|
+
│ Manager │ │ Index │ │ Job │
|
|
161
|
+
│ Service │ │ Manager │ │ Processor │
|
|
162
|
+
└──────┬──────┘ └────────┬────────┘ │ Registry │
|
|
163
|
+
│ │ └────────┬────────┘
|
|
164
|
+
│ 2. Get/create queue │ 2. Return entities │
|
|
165
|
+
│ for entity │ with job counts │ 2. Lookup processor
|
|
166
|
+
▼ │ │ by job name
|
|
167
|
+
┌─────────────┐ ▼ ▼
|
|
168
|
+
│ Redis │ ┌─────────────────┐ ┌─────────────────┐
|
|
169
|
+
│ Queue │◄────────────────── │ Worker │ │ @JobProcessor │
|
|
170
|
+
│ (entity-X) │ │ Manager │ │ Handler Class │
|
|
171
|
+
└──────┬──────┘ └────────┬────────┘ └────────┬────────┘
|
|
172
|
+
│ │ │
|
|
173
|
+
│ 3. Add job to queue │ 3. Spawn worker │ 3. Execute
|
|
174
|
+
│ (FIFO ordered) │ for entity │ command/query
|
|
175
|
+
▼ │ ▼
|
|
176
|
+
┌─────────────┐ ▼ ┌─────────────────┐
|
|
177
|
+
│ Index │ ┌─────────────────┐ │ CommandBus │
|
|
178
|
+
│ Manager │ │ New Worker │ │ / QueryBus │
|
|
179
|
+
└─────────────┘ │ (concurrency=1)│ └────────┬────────┘
|
|
180
|
+
│ └─────────────────┘ │
|
|
181
|
+
│ 4. Track job in index │ 4. Domain
|
|
182
|
+
│ for entity │ logic
|
|
183
|
+
▼ ▼
|
|
184
|
+
┌─────────────────────────────────────────────────────────────────────────────────────┐
|
|
185
|
+
│ REDIS │
|
|
186
|
+
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
187
|
+
│ │ Queues │ │ Workers │ │ Indices │ │ Locks │ │
|
|
188
|
+
│ │ (BullMQ) │ │ (Heartbeat) │ │ (Jobs/Qs) │ │ (Lua Atom) │ │
|
|
189
|
+
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
190
|
+
└─────────────────────────────────────────────────────────────────────────────────────┘
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
4. JOB COMPLETION 5. WORKER TERMINATION 6. GRACEFUL SHUTDOWN
|
|
194
|
+
───────────────── ───────────────────── ────────────────────
|
|
195
|
+
|
|
196
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
197
|
+
│ Worker │ │ CronManager │ │ SIGTERM/INT │
|
|
198
|
+
│ completes │ │ Service │ │ Signal │
|
|
199
|
+
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
|
200
|
+
│ │ │
|
|
201
|
+
│ 1. Job finished │ 1. Check worker │ 1. Caught by
|
|
202
|
+
│ │ idle time │ process handler
|
|
203
|
+
▼ │ ▼
|
|
204
|
+
┌─────────────────┐ ▼ ┌─────────────────┐
|
|
205
|
+
│ Index │ ┌─────────────────┐ │ Worker │
|
|
206
|
+
│ Manager │ │ No pending │ │ Manager │
|
|
207
|
+
└────────┬────────┘ │ jobs for │ └────────┬────────┘
|
|
208
|
+
│ │ entity? │ │
|
|
209
|
+
│ 2. Remove job from └────────┬────────┘ │ 2. Signal all
|
|
210
|
+
│ entity index │ │ workers to close
|
|
211
|
+
▼ │ YES ▼
|
|
212
|
+
┌─────────────────┐ ▼ ┌─────────────────┐
|
|
213
|
+
│ Check pending │ ┌─────────────────┐ │ Redis │
|
|
214
|
+
│ jobs for │ │ Worker │ │ Pub/Sub │
|
|
215
|
+
│ entity │ │ Manager │ │ (shutdown │
|
|
216
|
+
└────────┬────────┘ └────────┬────────┘ │ channel) │
|
|
217
|
+
│ │ └────────┬────────┘
|
|
218
|
+
│ 3. If no pending │ 2. Signal worker │
|
|
219
|
+
│ jobs, cleanup │ to close │ 3. Workers receive
|
|
220
|
+
▼ ▼ │ shutdown signal
|
|
221
|
+
┌─────────────────┐ ┌─────────────────┐ ▼
|
|
222
|
+
│ Entity indices │ │ Worker │ ┌─────────────────┐
|
|
223
|
+
│ cleaned up │ │ gracefully │ │ Workers │
|
|
224
|
+
│ │ │ closes │ │ finish │
|
|
225
|
+
└─────────────────┘ └─────────────────┘ │ current job │
|
|
226
|
+
│ then exit │
|
|
227
|
+
└─────────────────┘
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Multi-Node Cluster Architecture
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
234
|
+
│ MULTI-NODE CLUSTER DEPLOYMENT │
|
|
235
|
+
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
236
|
+
|
|
237
|
+
┌─────────────────┐
|
|
238
|
+
│ Load Balancer │
|
|
239
|
+
└────────┬────────┘
|
|
240
|
+
│
|
|
241
|
+
┌──────────────────────────────┼──────────────────────────────┐
|
|
242
|
+
│ │ │
|
|
243
|
+
▼ ▼ ▼
|
|
244
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
245
|
+
│ Node 1 │ │ Node 2 │ │ Node 3 │
|
|
246
|
+
│ (PM2 Cluster) │ │ (PM2 Cluster) │ │ (K8s Pod) │
|
|
247
|
+
├─────────────────┤ ├─────────────────┤ ├─────────────────┤
|
|
248
|
+
│ │ │ │ │ │
|
|
249
|
+
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
|
|
250
|
+
│ │ Worker A │ │ │ │ Worker C │ │ │ │ Worker E │ │
|
|
251
|
+
│ │(Entity 1) │ │ │ │(Entity 3) │ │ │ │(Entity 5) │ │
|
|
252
|
+
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
|
|
253
|
+
│ │ │ │ │ │
|
|
254
|
+
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
|
|
255
|
+
│ │ Worker B │ │ │ │ Worker D │ │ │ │ Worker F │ │
|
|
256
|
+
│ │(Entity 2) │ │ │ │(Entity 4) │ │ │ │(Entity 6) │ │
|
|
257
|
+
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
|
|
258
|
+
│ │ │ │ │ │
|
|
259
|
+
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
|
260
|
+
│ │ │
|
|
261
|
+
└──────────────────────────────┼──────────────────────────────┘
|
|
262
|
+
│
|
|
263
|
+
▼
|
|
264
|
+
┌─────────────────────────────────────────────────────────────────────────────────┐
|
|
265
|
+
│ REDIS CLUSTER │
|
|
266
|
+
│ │
|
|
267
|
+
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
|
268
|
+
│ │ BullMQ Queues │ │
|
|
269
|
+
│ │ entity-1-queue │ entity-2-queue │ entity-3-queue │ ... │ entity-N-q │ │
|
|
270
|
+
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
|
271
|
+
│ │
|
|
272
|
+
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
|
273
|
+
│ │ Worker Heartbeats (TTL) │ │
|
|
274
|
+
│ │ aq:workers:entity-1-worker │ aq:workers:entity-2-worker │ ... │ │
|
|
275
|
+
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
|
276
|
+
│ │
|
|
277
|
+
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
|
278
|
+
│ │ Job/Entity Indices │ │
|
|
279
|
+
│ │ aq:idx:entity:jobs │ aq:idx:entity:queues │ aq:idx:entity:workers │ │
|
|
280
|
+
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
|
281
|
+
│ │
|
|
282
|
+
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
|
|
283
|
+
│ │ Pub/Sub Shutdown Channels │ │
|
|
284
|
+
│ │ aq:worker:entity-1-worker:shutdown │ aq:worker:entity-2-worker:shut │ │
|
|
285
|
+
│ └─────────────────────────────────────────────────────────────────────────┘ │
|
|
286
|
+
│ │
|
|
287
|
+
└─────────────────────────────────────────────────────────────────────────────────┘
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
KEY GUARANTEES:
|
|
291
|
+
───────────────
|
|
292
|
+
✓ Only ONE worker processes jobs for each entity (concurrency=1)
|
|
293
|
+
✓ Jobs for same entity are processed in FIFO order
|
|
294
|
+
✓ Worker heartbeats detected across all nodes
|
|
295
|
+
✓ Graceful shutdown via Redis pub/sub (not local signals)
|
|
296
|
+
✓ Any node can spawn workers for any entity
|
|
297
|
+
✓ Dead workers detected via TTL expiration
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## Features
|
|
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 @nestjs/atomic-queues bullmq ioredis
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Quick Start
|
|
323
|
+
|
|
324
|
+
### 1. Import the Module
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { Module } from '@nestjs/common';
|
|
328
|
+
import { AtomicQueuesModule } from '@nestjs/atomic-queues';
|
|
329
|
+
|
|
330
|
+
@Module({
|
|
331
|
+
imports: [
|
|
332
|
+
AtomicQueuesModule.forRoot({
|
|
333
|
+
redis: {
|
|
334
|
+
host: 'localhost',
|
|
335
|
+
port: 6379,
|
|
336
|
+
},
|
|
337
|
+
enableCronManager: true,
|
|
338
|
+
cronInterval: 5000,
|
|
339
|
+
keyPrefix: 'myapp',
|
|
340
|
+
}),
|
|
341
|
+
],
|
|
342
|
+
})
|
|
343
|
+
export class AppModule {}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### 2. Async Configuration
|
|
347
|
+
|
|
348
|
+
```typescript
|
|
349
|
+
import { Module } from '@nestjs/common';
|
|
350
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
351
|
+
import { AtomicQueuesModule } from '@nestjs/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
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { Injectable } from '@nestjs/common';
|
|
378
|
+
import { JobProcessor, JobProcessorRegistry } from '@nestjs/atomic-queues';
|
|
379
|
+
import { CommandBus } from '@nestjs/cqrs';
|
|
380
|
+
|
|
381
|
+
@Injectable()
|
|
382
|
+
@JobProcessor('validate-order')
|
|
383
|
+
export class ValidateOrderProcessor {
|
|
384
|
+
constructor(private readonly commandBus: CommandBus) {}
|
|
385
|
+
|
|
386
|
+
async process(job: Job) {
|
|
387
|
+
const { orderId, items } = job.data;
|
|
388
|
+
await this.commandBus.execute(new ValidateOrderCommand(orderId, items));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
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
|
+
```
|
|
403
|
+
|
|
404
|
+
### 4. Queue Jobs
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
import { Injectable } from '@nestjs/common';
|
|
408
|
+
import { QueueManagerService, IndexManagerService } from '@nestjs/atomic-queues';
|
|
409
|
+
|
|
410
|
+
@Injectable()
|
|
411
|
+
export class OrderService {
|
|
412
|
+
constructor(
|
|
413
|
+
private readonly queueManager: QueueManagerService,
|
|
414
|
+
private readonly indexManager: IndexManagerService,
|
|
415
|
+
) {}
|
|
416
|
+
|
|
417
|
+
async createOrder(orderId: string, items: any[], amount: number) {
|
|
418
|
+
const queue = this.queueManager.getOrCreateEntityQueue('order', orderId);
|
|
419
|
+
|
|
420
|
+
// Queue validation job
|
|
421
|
+
const job = await this.queueManager.addJob(queue.name, 'validate-order', { orderId, items });
|
|
422
|
+
|
|
423
|
+
// Queue payment job (will run after validation completes due to FIFO)
|
|
424
|
+
await this.queueManager.addJob(queue.name, 'process-payment', { orderId, amount });
|
|
425
|
+
|
|
426
|
+
// Track job for scaling decisions
|
|
427
|
+
await this.indexManager.indexJob('order', orderId, job.id!);
|
|
428
|
+
|
|
429
|
+
return orderId;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### 5. Create Workers
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import { Injectable } from '@nestjs/common';
|
|
438
|
+
import { WorkerManagerService, JobProcessorRegistry } from '@nestjs/atomic-queues';
|
|
439
|
+
|
|
440
|
+
@Injectable()
|
|
441
|
+
export class OrderWorkerService {
|
|
442
|
+
constructor(
|
|
443
|
+
private readonly workerManager: WorkerManagerService,
|
|
444
|
+
private readonly jobRegistry: JobProcessorRegistry,
|
|
445
|
+
) {}
|
|
446
|
+
|
|
447
|
+
async createOrderWorker(orderId: string) {
|
|
448
|
+
const queueName = `order-${orderId}-queue`;
|
|
449
|
+
|
|
450
|
+
await this.workerManager.createWorker({
|
|
451
|
+
workerName: `${orderId}-worker`,
|
|
452
|
+
queueName,
|
|
453
|
+
processor: async (job) => {
|
|
454
|
+
const processor = this.jobRegistry.getProcessor(job.name);
|
|
455
|
+
if (!processor) {
|
|
456
|
+
throw new Error(`No processor for job: ${job.name}`);
|
|
457
|
+
}
|
|
458
|
+
await processor.process(job);
|
|
459
|
+
},
|
|
460
|
+
events: {
|
|
461
|
+
onReady: async (worker, name) => {
|
|
462
|
+
console.log(`Worker ${name} is ready`);
|
|
463
|
+
},
|
|
464
|
+
onCompleted: async (job, name) => {
|
|
465
|
+
console.log(`Job ${job.id} completed by ${name}`);
|
|
466
|
+
},
|
|
467
|
+
onFailed: async (job, error, name) => {
|
|
468
|
+
console.error(`Job ${job?.id} failed in ${name}:`, error.message);
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Core Services
|
|
479
|
+
|
|
480
|
+
### QueueManagerService
|
|
481
|
+
|
|
482
|
+
Manages dynamic queue creation and destruction per entity.
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
// Get or create a queue for an entity
|
|
486
|
+
const queue = queueManager.getOrCreateEntityQueue('order', '123');
|
|
487
|
+
|
|
488
|
+
// Add a job to a queue
|
|
489
|
+
await queueManager.addJob(queue.name, 'process', { data: 'hello' });
|
|
490
|
+
|
|
491
|
+
// Get job counts
|
|
492
|
+
const counts = await queueManager.getJobCounts(queue.name);
|
|
493
|
+
|
|
494
|
+
// Close a queue
|
|
495
|
+
await queueManager.closeQueue(queue.name);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### WorkerManagerService
|
|
499
|
+
|
|
500
|
+
Manages worker lifecycle with heartbeat-based liveness tracking.
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// Create a worker
|
|
504
|
+
await workerManager.createWorker({
|
|
505
|
+
workerName: 'my-worker',
|
|
506
|
+
queueName: 'my-queue',
|
|
507
|
+
processor: async (job) => { /* process job */ },
|
|
508
|
+
config: {
|
|
509
|
+
concurrency: 1,
|
|
510
|
+
heartbeatTTL: 3,
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
// Check if worker exists
|
|
515
|
+
const exists = await workerManager.workerExists('my-worker');
|
|
516
|
+
|
|
517
|
+
// Signal worker to close via Redis pub/sub
|
|
518
|
+
await workerManager.signalWorkerClose('my-worker');
|
|
519
|
+
|
|
520
|
+
// Get all workers for an entity
|
|
521
|
+
const workers = await workerManager.getEntityWorkers('order', '123');
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### ResourceLockService
|
|
525
|
+
|
|
526
|
+
Provides distributed resource locking using Redis Lua scripts.
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
// Acquire a lock
|
|
530
|
+
const result = await lockService.acquireLock(
|
|
531
|
+
'resource', // resourceType
|
|
532
|
+
'resource-123', // resourceId
|
|
533
|
+
'owner-456', // ownerId
|
|
534
|
+
'service', // ownerType
|
|
535
|
+
60, // TTL in seconds
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
if (result.acquired) {
|
|
539
|
+
try {
|
|
540
|
+
// Do work with the locked resource
|
|
541
|
+
} finally {
|
|
542
|
+
await lockService.releaseLock('resource', 'resource-123');
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Get first available resource from a pool
|
|
547
|
+
const available = await lockService.getAvailableResource(
|
|
548
|
+
'resource',
|
|
549
|
+
['res-1', 'res-2', 'res-3'],
|
|
550
|
+
'owner-456',
|
|
551
|
+
'service',
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### CronManagerService
|
|
556
|
+
|
|
557
|
+
Automatic worker scaling based on demand.
|
|
558
|
+
|
|
559
|
+
```typescript
|
|
560
|
+
// Register entity type for automatic scaling
|
|
561
|
+
cronManager.registerEntityType({
|
|
562
|
+
entityType: 'order',
|
|
563
|
+
getDesiredWorkerCount: async (orderId) => {
|
|
564
|
+
// Return how many workers this entity needs
|
|
565
|
+
return 1;
|
|
566
|
+
},
|
|
567
|
+
getActiveEntityIds: async () => {
|
|
568
|
+
return Object.keys(await indexManager.getEntitiesWithJobs('order'));
|
|
569
|
+
},
|
|
570
|
+
maxWorkersPerEntity: 5,
|
|
571
|
+
onSpawnWorker: async (orderId) => {
|
|
572
|
+
await orderWorkerService.createOrderWorker(orderId);
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
// Start the cron manager
|
|
577
|
+
cronManager.start(5000);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### IndexManagerService
|
|
581
|
+
|
|
582
|
+
Track jobs, workers, and queue states.
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
// Index a job
|
|
586
|
+
await indexManager.indexJob('order', '123', 'job-456');
|
|
587
|
+
|
|
588
|
+
// Get all entities with pending jobs
|
|
589
|
+
const entitiesWithJobs = await indexManager.getEntitiesWithJobs('order');
|
|
590
|
+
// Returns: { '123': 5, '456': 2 } (entityId: jobCount)
|
|
591
|
+
|
|
592
|
+
// Track queue existence
|
|
593
|
+
await indexManager.indexEntityQueue('order', '123');
|
|
594
|
+
|
|
595
|
+
// Clean up all indices for an entity
|
|
596
|
+
await indexManager.cleanupEntityIndices('order', '123');
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
## Configuration Options
|
|
602
|
+
|
|
603
|
+
```typescript
|
|
604
|
+
interface IAtomicQueuesModuleConfig {
|
|
605
|
+
// Redis connection
|
|
606
|
+
redis: {
|
|
607
|
+
host?: string;
|
|
608
|
+
port?: number;
|
|
609
|
+
password?: string;
|
|
610
|
+
db?: number;
|
|
611
|
+
url?: string;
|
|
612
|
+
maxRetriesPerRequest?: number | null;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Worker defaults
|
|
616
|
+
workerDefaults?: {
|
|
617
|
+
concurrency?: number; // Default: 1
|
|
618
|
+
stalledInterval?: number; // Default: 1000ms
|
|
619
|
+
lockDuration?: number; // Default: 30000ms
|
|
620
|
+
maxStalledCount?: number; // Default: MAX_SAFE_INTEGER
|
|
621
|
+
heartbeatTTL?: number; // Default: 3 seconds
|
|
622
|
+
heartbeatInterval?: number; // Default: 1000ms
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Queue defaults
|
|
626
|
+
queueDefaults?: {
|
|
627
|
+
defaultJobOptions?: {
|
|
628
|
+
removeOnComplete?: boolean;
|
|
629
|
+
removeOnFail?: boolean;
|
|
630
|
+
attempts?: number;
|
|
631
|
+
backoff?: { type: 'fixed' | 'exponential'; delay: number };
|
|
632
|
+
priority?: number;
|
|
633
|
+
};
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Cron manager
|
|
637
|
+
enableCronManager?: boolean; // Default: false
|
|
638
|
+
cronInterval?: number; // Default: 5000ms
|
|
639
|
+
|
|
640
|
+
// Key prefix for Redis keys
|
|
641
|
+
keyPrefix?: string; // Default: 'aq'
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Graceful Shutdown
|
|
648
|
+
|
|
649
|
+
The library handles graceful shutdown automatically via Redis pub/sub:
|
|
650
|
+
|
|
651
|
+
1. On `SIGTERM`/`SIGINT`, the node publishes shutdown signals to Redis
|
|
652
|
+
2. All workers (even on other nodes) subscribed to shutdown channels receive the signal
|
|
653
|
+
3. Workers finish their current job (with configurable timeout)
|
|
654
|
+
4. Heartbeat TTLs expire, marking workers as dead
|
|
655
|
+
5. Resources are cleaned up
|
|
656
|
+
|
|
657
|
+
```typescript
|
|
658
|
+
// Manual shutdown
|
|
659
|
+
await workerManager.signalNodeWorkersClose();
|
|
660
|
+
await workerManager.waitForWorkersToClose(30000);
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Use Cases
|
|
666
|
+
|
|
667
|
+
### 1. Per-Order Processing (E-commerce)
|
|
668
|
+
Each order has its own queue ensuring stages (validate → pay → ship) happen sequentially.
|
|
669
|
+
|
|
670
|
+
### 2. Per-User Message Queues (Chat/Messaging)
|
|
671
|
+
Each user has their own queue for message delivery, ensuring order.
|
|
672
|
+
|
|
673
|
+
### 3. Per-Tenant Job Processing (SaaS)
|
|
674
|
+
Each tenant's jobs are isolated and processed sequentially.
|
|
675
|
+
|
|
676
|
+
### 4. Per-Document Processing (Document Management)
|
|
677
|
+
Each document goes through OCR → validation → storage in sequence.
|
|
678
|
+
|
|
679
|
+
### 5. Per-Device Commands (IoT)
|
|
680
|
+
Each device receives commands in order, preventing race conditions.
|
|
681
|
+
|
|
682
|
+
---
|
|
683
|
+
|
|
684
|
+
## License
|
|
685
|
+
|
|
686
|
+
MIT
|