atomic-queues 1.2.7 → 1.2.8
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 +211 -35
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,46 +1,222 @@
|
|
|
1
1
|
# atomic-queues
|
|
2
2
|
|
|
3
|
-
A NestJS library for atomic, sequential job processing per entity
|
|
3
|
+
A NestJS library for atomic, sequential job processing per entity using BullMQ and Redis.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
**atomic-queues** solves the fundamental concurrency problem in distributed systems: ensuring that operations on the same logical entity execute sequentially, even when requests arrive simultaneously across multiple service instances.
|
|
10
|
+
|
|
11
|
+
Rather than relying on distributed locks—which introduce contention, latency degradation, and complex failure modes—this library implements a **per-entity queue architecture** where each entity (user account, game table, order, document) has its own dedicated processing queue and worker.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## The Concurrency Problem
|
|
16
|
+
|
|
17
|
+
### Race Condition Scenario
|
|
18
|
+
|
|
19
|
+
Consider a financial system where a user with a $100 balance submits two concurrent $80 withdrawal requests:
|
|
6
20
|
|
|
7
21
|
```
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
║ Withdraw $80 ──┘ │ Both read $100 │ ║
|
|
18
|
-
║ (API 2) │ Both approve │ ║
|
|
19
|
-
║ │ Final: -$60 💥 │ ║
|
|
20
|
-
║ └────────────────────┘ ║
|
|
21
|
-
║ ║
|
|
22
|
-
║ Race condition: Both transactions see $100, both succeed, balance goes -$60 ║
|
|
23
|
-
║ ║
|
|
24
|
-
╚═══════════════════════════════════════════════════════════════════════════════╝
|
|
22
|
+
Time Request A Request B Database State
|
|
23
|
+
─────────────────────────────────────────────────────────────────────────────────
|
|
24
|
+
T₀ SELECT balance → $100 SELECT balance → $100 balance = $100
|
|
25
|
+
T₁ CHECK: $100 >= $80 ✓ CHECK: $100 >= $80 ✓
|
|
26
|
+
T₂ UPDATE: balance = $20 UPDATE: balance = $20 balance = $20
|
|
27
|
+
T₃ UPDATE: balance = -$60 balance = -$60
|
|
28
|
+
─────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
Result: Both withdrawals succeed. Balance becomes -$60. Integrity violated.
|
|
30
|
+
```
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
This occurs because both transactions read the balance before either writes, a classic **lost update anomaly**.
|
|
33
|
+
|
|
34
|
+
### Traditional Solutions and Their Limitations
|
|
35
|
+
|
|
36
|
+
| Approach | Mechanism | Failure Mode |
|
|
37
|
+
|----------|-----------|--------------|
|
|
38
|
+
| **Distributed Locks (Redlock)** | Acquire lock before operation, release after | Lock contention storms under high throughput; exponential latency degradation; lock holder failure requires TTL expiration |
|
|
39
|
+
| **Database Row Locks** | `SELECT ... FOR UPDATE` | Connection pool exhaustion; deadlock risk in multi-entity transactions; database becomes bottleneck |
|
|
40
|
+
| **Optimistic Concurrency Control** | Version numbers with conditional updates | Retry storms under contention; unbounded retries on hot entities; wasted compute cycles |
|
|
41
|
+
| **Application Semaphores** | In-memory mutex/semaphore | Single-process only; ineffective in horizontally scaled deployments |
|
|
42
|
+
|
|
43
|
+
**Fundamental limitation**: These approaches attempt to serialize access at the *moment of execution*. Under high contention, this creates a thundering herd where N requests compete for the same resource simultaneously.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## The Per-Entity Queue Architecture
|
|
48
|
+
|
|
49
|
+
### Design Principle
|
|
50
|
+
|
|
51
|
+
Instead of serializing at execution time, **serialize at ingestion time**:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
┌─────────────────────────────────────────┐
|
|
55
|
+
Request A ─┐ │ Per-Entity Queue │
|
|
56
|
+
│ │ ┌─────┐ ┌─────┐ ┌─────┐ │
|
|
57
|
+
Request B ─┼──▶ [Entity Router] ─┼─▶│ Op₁ │→│ Op₂ │→│ Op₃ │→ [Worker] ─┐ │
|
|
58
|
+
│ │ └─────┘ └─────┘ └─────┘ │ │
|
|
59
|
+
Request C ─┘ │ │ │
|
|
60
|
+
│ Sequential Processing ◄─────────┘ │
|
|
61
|
+
└─────────────────────────────────────────┘
|
|
42
62
|
```
|
|
43
63
|
|
|
64
|
+
Operations targeting the same entity are immediately routed to that entity's queue. A dedicated worker processes operations one at a time, guaranteeing:
|
|
65
|
+
|
|
66
|
+
1. **Serialized Execution**: Operations execute in FIFO order
|
|
67
|
+
2. **Consistent State Visibility**: Each operation sees the result of all prior operations
|
|
68
|
+
3. **Isolation**: No interleaving of concurrent modifications
|
|
69
|
+
|
|
70
|
+
### Correctness Under Load
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
Time Queue State Worker Execution Database State
|
|
74
|
+
───────────────────────────────────────────────────────────────────────────────────
|
|
75
|
+
T₀ [Withdraw $80, Withdraw $80] balance = $100
|
|
76
|
+
T₁ [Withdraw $80] Process Op₁: $100 - $80 balance = $20
|
|
77
|
+
T₂ [] Process Op₂: $20 < $80 → REJECT balance = $20
|
|
78
|
+
───────────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
Result: First withdrawal succeeds. Second is rejected. Integrity preserved.
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Comparative Analysis
|
|
85
|
+
|
|
86
|
+
### Behavioral Characteristics
|
|
87
|
+
|
|
88
|
+
| Characteristic | Distributed Locks | Per-Entity Queues |
|
|
89
|
+
|------------------------- |-------------------------------------- |------------------- |
|
|
90
|
+
| **Request Handling** | Block until lock acquired | Queue immediately, return |
|
|
91
|
+
| **Latency Distribution** | Bimodal (fast if uncontested) | Predictable (queue depth × avg processing time) |
|
|
92
|
+
| **Throughput Ceiling** | Limited by lock contention | Limited only by worker processing rate |
|
|
93
|
+
| **Failure Recovery** | Stuck locks until TTL expiration | Failed jobs retry or move to dead-letter queue |
|
|
94
|
+
| **Ordering Guarantees** | Non-deterministic (race to acquire) | Deterministic FIFO |
|
|
95
|
+
| **Observability** | Lock wait times difficult to measure | Queue depth, throughput directly observable |
|
|
96
|
+
|
|
97
|
+
### Scalability Profile
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Throughput
|
|
101
|
+
▲
|
|
102
|
+
│ ╭──── Per-Entity Queues
|
|
103
|
+
│ ╭───╯ (linear scaling)
|
|
104
|
+
│ ╭───╯
|
|
105
|
+
│ ╭───╯
|
|
106
|
+
│ ╭───╯
|
|
107
|
+
│ ╭───╯ ╭────── Distributed Locks
|
|
108
|
+
│ ╭───╯ ╭───╯ (contention ceiling)
|
|
109
|
+
│ ╭───╯ ╭───╯
|
|
110
|
+
│ ╭───╯ ╭───────╯
|
|
111
|
+
│╭───╯ ╭───────╯
|
|
112
|
+
├──────╯
|
|
113
|
+
└──────────────────────────────────────────────▶ Concurrent Requests
|
|
114
|
+
|
|
115
|
+
Lock-based systems hit a contention ceiling where adding more
|
|
116
|
+
requests increases wait time faster than throughput.
|
|
117
|
+
|
|
118
|
+
Queue-based systems scale linearly: each entity's queue is
|
|
119
|
+
independent, so Entity A's load doesn't affect Entity B.
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
### Horizontal Scaling Model
|
|
127
|
+
|
|
128
|
+
Workers are distributed across service instances via Redis-based coordination:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
┌─────────────────────────────────────────────────────────────────────────────────┐
|
|
132
|
+
│ REDIS CLUSTER │
|
|
133
|
+
│ ┌────────────────────────────────────────────────────────────────────────────┐ │
|
|
134
|
+
│ │ Entity Queues │ │
|
|
135
|
+
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
|
|
136
|
+
│ │ │ account:ACC-001 │ │ account:ACC-002 │ │ account:ACC-003 │ ... │ │
|
|
137
|
+
│ │ │ [op₁][op₂][op₃] │ │ [op₁] │ │ [op₁][op₂] │ │ │
|
|
138
|
+
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
|
|
139
|
+
│ │ │ │ │ │ │
|
|
140
|
+
│ │ ┌────────┴───────────────────┴───────────────────┴────────┐ │ │
|
|
141
|
+
│ │ │ Worker Heartbeat Registry │ │ │
|
|
142
|
+
│ │ │ ACC-001 → node-1 | ACC-002 → node-2 | ACC-003 → node-1 │ │ │
|
|
143
|
+
│ │ └──────────────────────────────────────────────────────────┘ │ │
|
|
144
|
+
│ └────────────────────────────────────────────────────────────────────────────┘ │
|
|
145
|
+
└─────────────────────────────────────────────────────────────────────────────────┘
|
|
146
|
+
│ │ │
|
|
147
|
+
▼ ▼ ▼
|
|
148
|
+
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
|
|
149
|
+
│ Service Node 1 │ │ Service Node 2 │ │ Service Node 3 │
|
|
150
|
+
│ │ │ │ │ │
|
|
151
|
+
│ ┌───────────────┐ │ │ ┌───────────────┐ │ │ ┌───────────────┐ │
|
|
152
|
+
│ │ Worker ACC-001│ │ │ │ Worker ACC-002│ │ │ │ Worker ACC-004│ │
|
|
153
|
+
│ │ Worker ACC-003│ │ │ │ Worker ACC-005│ │ │ │ Worker ACC-006│ │
|
|
154
|
+
│ └───────────────┘ │ │ └───────────────┘ │ │ └───────────────┘ │
|
|
155
|
+
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Properties:**
|
|
159
|
+
- Each entity has exactly one active worker at any time (enforced via heartbeat TTL)
|
|
160
|
+
- Workers spawn on-demand when jobs arrive for an entity
|
|
161
|
+
- Workers terminate after configurable idle period
|
|
162
|
+
- Node failure → heartbeat expires → worker respawns on healthy node
|
|
163
|
+
|
|
164
|
+
### Dynamic Worker Lifecycle
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
Job Arrives for Entity X
|
|
168
|
+
│
|
|
169
|
+
▼
|
|
170
|
+
┌─────────────────────┐
|
|
171
|
+
│ Worker exists for X? │
|
|
172
|
+
└──────────┬──────────┘
|
|
173
|
+
│
|
|
174
|
+
┌────────────────┴────────────────┐
|
|
175
|
+
│ NO │ YES
|
|
176
|
+
▼ ▼
|
|
177
|
+
┌─────────────────────┐ ┌─────────────────────┐
|
|
178
|
+
│ Spawn worker for X │ │ Job added to queue │
|
|
179
|
+
│ Register heartbeat │ │ Worker will process │
|
|
180
|
+
└─────────────────────┘ └─────────────────────┘
|
|
181
|
+
│
|
|
182
|
+
▼
|
|
183
|
+
┌─────────────────────┐
|
|
184
|
+
│ Process jobs until │◄─────── Idle Timeout
|
|
185
|
+
│ queue empty + idle │ (configurable)
|
|
186
|
+
└──────────┬──────────┘
|
|
187
|
+
│
|
|
188
|
+
▼
|
|
189
|
+
┌─────────────────────┐
|
|
190
|
+
│ Worker terminates │
|
|
191
|
+
│ Heartbeat expires │
|
|
192
|
+
└─────────────────────┘
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Resource Efficiency**: A system with 1 million registered accounts but 10,000 active accounts maintains only 10,000 workers.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Use Cases
|
|
200
|
+
|
|
201
|
+
### Recommended Applications
|
|
202
|
+
|
|
203
|
+
| Domain | Entity Type | Operations |
|
|
204
|
+
|-------------------|-----------------|-----------------------------------------------------|
|
|
205
|
+
| **Finance** | Account, Wallet | Deposits, withdrawals, transfers, balance queries |
|
|
206
|
+
| **Gaming** | Game, Match | Player actions, state transitions, bet processing |
|
|
207
|
+
| **E-Commerce** | Order, Cart | Add/remove items, apply discounts, checkout |
|
|
208
|
+
| **Collaboration** | Document | Edits, comments, permission changes |
|
|
209
|
+
| **IoT** | Device | Command dispatch, state synchronization |
|
|
210
|
+
|
|
211
|
+
### When to Use Alternative Approaches
|
|
212
|
+
|
|
213
|
+
- **Read-heavy workloads**: Use caching layers (Redis, Memcached) or read replicas
|
|
214
|
+
- **Parallelizable operations**: Use standard job queues (BullMQ, SQS) without entity affinity
|
|
215
|
+
- **Fire-and-forget notifications**: Use pub/sub (Redis Pub/Sub, Kafka) without ordering guarantees
|
|
216
|
+
- **Short critical sections (<10ms)**: Distributed locks may suffice if contention is low
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
44
220
|
## Installation
|
|
45
221
|
|
|
46
222
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atomic-queues",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"description": "A plug-and-play NestJS library for atomic process handling per entity with BullMQ, Redis distributed locking, and dynamic worker management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|