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.
Files changed (2) hide show
  1. package/README.md +211 -35
  2. 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 with BullMQ and Redis.
3
+ A NestJS library for atomic, sequential job processing per entity using BullMQ and Redis.
4
4
 
5
- ## What It Does
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
- ║ THE PROBLEM ║
10
- ╠═══════════════════════════════════════════════════════════════════════════════╣
11
- ║ ║
12
- ║ User has $100 balance. Two $80 withdrawals arrive at the same time: ║
13
- ║ ║
14
- ║ Withdraw $80 ──┐ ║
15
- ║ (API 1) │ ┌────────────────────┐ ║
16
- ║ ├───▶│ Balance: $100 │ ║
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
- ║ THE SOLUTION ║
28
- ╠═══════════════════════════════════════════════════════════════════════════════╣
29
- ║ ║
30
- ║ atomic-queues processes one transaction at a time per account: ║
31
- ║ ║
32
- ║ Withdraw $80 ──┐ ┌─────────────┐ ┌─────────────────────────────┐
33
- ║ (API 1) │ │ │ │ Worker processes queue: │
34
- ║ ├────▶│ Redis Queue │────▶│ │
35
- ║ Withdraw $80 ──┘ │ [W1] [W2] │ │ W1: $100 - $80 = $20 ✓ │
36
- ║ (API 2) │ │ │ W2: $20 < $80 = REJECTED ✓ │ ║
37
- ║ └─────────────┘ └─────────────────────────────┘
38
- ║ ║
39
- ║ Sequential processing: W1 completes first, W2 sees updated balance ║
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.7",
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",