queen-mq 0.1.0

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 (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,989 @@
1
+ # Partition-Level Locking Design (Simplified - No Worker ID)
2
+
3
+ ## Core Concept
4
+ Each partition can be "leased" to exactly one consumer per consumer group at a time. We don't track WHO has the lease, only THAT the partition is locked for a specific consumer group. This simplifies the design for stateless consumers.
5
+
6
+ ## Database Schema Changes
7
+
8
+ ### 1. Updated Messages Table
9
+
10
+ ```sql
11
+ -- Modify messages table with new fields
12
+ ALTER TABLE queen.messages
13
+ ALTER COLUMN transaction_id TYPE VARCHAR(255), -- Change from UUID to string
14
+ ADD COLUMN IF NOT EXISTS trace_id UUID DEFAULT gen_random_uuid(); -- For tracking multi-step pipelines
15
+
16
+ -- Add index for trace_id lookups
17
+ CREATE INDEX idx_messages_trace_id ON queen.messages(trace_id);
18
+
19
+ -- Add index for transaction_id (string now)
20
+ CREATE INDEX idx_messages_transaction_id ON queen.messages(transaction_id);
21
+ ```
22
+
23
+ ### 2. Queue Size Tracking and Limits
24
+
25
+ ```sql
26
+ -- Add max_queue_size to queues table
27
+ ALTER TABLE queen.queues
28
+ ADD COLUMN IF NOT EXISTS max_queue_size INTEGER DEFAULT 0; -- 0 = unlimited
29
+
30
+ -- Index for fast queue depth checks
31
+ CREATE INDEX idx_messages_status_pending_processing
32
+ ON queen.messages_status(message_id, status)
33
+ WHERE status IN ('pending', 'processing') OR status IS NULL;
34
+ ```
35
+
36
+ ### 3. New Table: partition_leases
37
+
38
+ ```sql
39
+ CREATE TABLE IF NOT EXISTS queen.partition_leases (
40
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
41
+ partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
42
+ consumer_group VARCHAR(255) DEFAULT '__QUEUE_MODE__', -- '__QUEUE_MODE__' for queue mode
43
+ lease_expires_at TIMESTAMPTZ NOT NULL,
44
+ message_batch JSONB, -- Store IDs of messages in this lease
45
+ created_at TIMESTAMPTZ DEFAULT NOW(),
46
+ released_at TIMESTAMPTZ, -- When lease was released (NULL if active)
47
+ UNIQUE(partition_id, consumer_group) -- One active lease per partition per consumer group
48
+ );
49
+
50
+ -- Index for finding expired leases
51
+ CREATE INDEX idx_partition_leases_expires ON queen.partition_leases(lease_expires_at)
52
+ WHERE released_at IS NULL;
53
+
54
+ -- Index for active leases
55
+ CREATE INDEX idx_partition_leases_active ON queen.partition_leases(partition_id, consumer_group)
56
+ WHERE released_at IS NULL;
57
+ ```
58
+
59
+ ## Push Operation with Queue Size Limits
60
+
61
+ ### Check Queue Capacity Before Push
62
+
63
+ ```sql
64
+ -- Push with queue size check and new fields
65
+ WITH queue_check AS (
66
+ -- Check if queue has capacity
67
+ SELECT
68
+ q.id as queue_id,
69
+ q.max_queue_size,
70
+ COUNT(m.id) as current_depth,
71
+ CASE
72
+ WHEN q.max_queue_size = 0 THEN true -- Unlimited
73
+ WHEN COUNT(m.id) < q.max_queue_size THEN true
74
+ ELSE false
75
+ END as has_capacity
76
+ FROM queen.queues q
77
+ LEFT JOIN queen.partitions p ON q.id = p.queue_id
78
+ LEFT JOIN queen.messages m ON p.id = m.partition_id
79
+ LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
80
+ AND ms.consumer_group = '__QUEUE_MODE__'
81
+ WHERE q.name = $1
82
+ AND (ms.status IS NULL OR ms.status IN ('pending', 'processing'))
83
+ GROUP BY q.id, q.max_queue_size
84
+ ),
85
+ partition_select AS (
86
+ -- Only proceed if queue has capacity
87
+ SELECT
88
+ p.id as partition_id,
89
+ qc.queue_id,
90
+ qc.current_depth,
91
+ qc.max_queue_size
92
+ FROM queue_check qc
93
+ JOIN queen.partitions p ON p.queue_id = qc.queue_id
94
+ WHERE qc.has_capacity = true
95
+ AND ($2 IS NULL OR p.name = $2) -- Specific partition or default
96
+ ORDER BY p.last_activity ASC -- Round-robin distribution
97
+ LIMIT 1
98
+ FOR UPDATE OF p SKIP LOCKED
99
+ ),
100
+ insert_message AS (
101
+ INSERT INTO queen.messages (
102
+ transaction_id, -- Now a string
103
+ trace_id, -- New field for pipeline tracking
104
+ partition_id,
105
+ payload,
106
+ is_encrypted
107
+ )
108
+ SELECT
109
+ $3, -- transaction_id (string)
110
+ $4, -- trace_id (UUID, can be NULL for single messages)
111
+ partition_id,
112
+ $5, -- payload
113
+ $6 -- is_encrypted
114
+ FROM partition_select
115
+ RETURNING *
116
+ )
117
+ SELECT
118
+ im.*,
119
+ ps.current_depth + 1 as new_depth,
120
+ ps.max_queue_size,
121
+ CASE
122
+ WHEN ps.max_queue_size > 0 THEN
123
+ ps.max_queue_size - (ps.current_depth + 1)
124
+ ELSE NULL
125
+ END as remaining_capacity
126
+ FROM insert_message im
127
+ CROSS JOIN partition_select ps;
128
+ ```
129
+
130
+ ### Batch Push with Capacity Check
131
+
132
+ ```sql
133
+ -- Batch push with atomic capacity check
134
+ WITH queue_check AS (
135
+ SELECT
136
+ q.id as queue_id,
137
+ q.max_queue_size,
138
+ COUNT(m.id) as current_depth,
139
+ $2::integer as batch_size, -- Number of messages to push
140
+ CASE
141
+ WHEN q.max_queue_size = 0 THEN true -- Unlimited
142
+ WHEN COUNT(m.id) + $2 <= q.max_queue_size THEN true
143
+ ELSE false
144
+ END as has_capacity_for_batch
145
+ FROM queen.queues q
146
+ LEFT JOIN queen.partitions p ON q.id = p.queue_id
147
+ LEFT JOIN queen.messages m ON p.id = m.partition_id
148
+ LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
149
+ AND ms.consumer_group = '__QUEUE_MODE__'
150
+ WHERE q.name = $1
151
+ AND (ms.status IS NULL OR ms.status IN ('pending', 'processing'))
152
+ GROUP BY q.id, q.max_queue_size
153
+ ),
154
+ check_result AS (
155
+ SELECT * FROM queue_check
156
+ WHERE has_capacity_for_batch = true
157
+ )
158
+ -- Only insert if capacity check passes
159
+ INSERT INTO queen.messages (transaction_id, trace_id, partition_id, payload, is_encrypted)
160
+ SELECT
161
+ unnest($3::text[]) as transaction_id, -- Array of transaction IDs (strings)
162
+ unnest($4::uuid[]) as trace_id, -- Array of trace IDs
163
+ p.id as partition_id,
164
+ unnest($5::jsonb[]) as payload,
165
+ unnest($6::boolean[]) as is_encrypted
166
+ FROM check_result cr
167
+ JOIN queen.partitions p ON p.queue_id = cr.queue_id
168
+ WHERE ($7 IS NULL OR p.name = $7); -- Optional partition name
169
+ ```
170
+
171
+ ### Error Handling for Queue Full
172
+
173
+ ```javascript
174
+ // In the push implementation
175
+ async function pushMessage(queue, message, options = {}) {
176
+ const { partition, traceId } = options;
177
+
178
+ try {
179
+ const result = await db.query(pushQuery, [
180
+ queue,
181
+ partition,
182
+ message.transactionId || generateTransactionId(), // String ID
183
+ traceId || null, // Optional trace ID for pipeline tracking
184
+ message.payload,
185
+ message.isEncrypted || false
186
+ ]);
187
+
188
+ if (result.rows.length === 0) {
189
+ throw new Error(`Queue '${queue}' is full. Max capacity reached.`);
190
+ }
191
+
192
+ return {
193
+ messageId: result.rows[0].id,
194
+ transactionId: result.rows[0].transaction_id,
195
+ traceId: result.rows[0].trace_id,
196
+ queueDepth: result.rows[0].new_depth,
197
+ remainingCapacity: result.rows[0].remaining_capacity
198
+ };
199
+ } catch (error) {
200
+ if (error.message.includes('is full')) {
201
+ // Queue is at capacity
202
+ throw new QueueFullError(queue, error.message);
203
+ }
204
+ throw error;
205
+ }
206
+ }
207
+ ```
208
+
209
+ ## Pop Operation Flow
210
+
211
+ ### Step 1: Acquire Partition Lease
212
+
213
+ ```sql
214
+ WITH available_partitions AS (
215
+ -- Find partitions with messages that don't have active leases
216
+ SELECT DISTINCT p.id, p.name, MIN(m.created_at) as oldest_message
217
+ FROM queen.partitions p
218
+ JOIN queen.messages m ON m.partition_id = p.id
219
+ LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
220
+ AND ms.consumer_group = $consumer_group
221
+ LEFT JOIN queen.partition_leases pl ON p.id = pl.partition_id
222
+ AND pl.consumer_group = $consumer_group
223
+ AND pl.released_at IS NULL
224
+ AND pl.lease_expires_at > NOW()
225
+ WHERE p.queue_id = $queue_id
226
+ AND pl.id IS NULL -- No active lease
227
+ AND (ms.id IS NULL OR ms.status IN ('pending', 'failed'))
228
+ GROUP BY p.id, p.name
229
+ ORDER BY oldest_message -- Fairness: oldest message first
230
+ LIMIT 1
231
+ FOR UPDATE OF p SKIP LOCKED -- Prevent race on partition selection
232
+ ),
233
+ acquire_lease AS (
234
+ INSERT INTO queen.partition_leases (
235
+ partition_id,
236
+ consumer_group,
237
+ lease_expires_at
238
+ )
239
+ SELECT
240
+ id,
241
+ $consumer_group,
242
+ NOW() + INTERVAL '1 second' * $lease_time
243
+ FROM available_partitions
244
+ ON CONFLICT (partition_id, consumer_group)
245
+ WHERE released_at IS NULL
246
+ DO UPDATE SET
247
+ lease_expires_at = EXCLUDED.lease_expires_at -- Extend lease if somehow exists
248
+ RETURNING partition_id, id as lease_id
249
+ )
250
+ SELECT * FROM acquire_lease;
251
+ ```
252
+
253
+ ### Step 2: Get Messages from Leased Partition
254
+
255
+ ```sql
256
+ -- Now that we have a lease on partition_id, get messages
257
+ WITH messages_to_process AS (
258
+ SELECT m.*
259
+ FROM queen.messages m
260
+ LEFT JOIN queen.messages_status ms ON m.id = ms.message_id
261
+ AND ms.consumer_group = $consumer_group
262
+ WHERE m.partition_id = $partition_id -- The partition we just leased
263
+ AND (ms.id IS NULL OR ms.status IN ('pending', 'failed'))
264
+ ORDER BY m.created_at ASC
265
+ LIMIT $batch_size
266
+ FOR UPDATE OF m -- Lock these specific messages
267
+ ),
268
+ update_lease AS (
269
+ -- Store which messages are part of this lease
270
+ UPDATE queen.partition_leases
271
+ SET message_batch = (SELECT jsonb_agg(id) FROM messages_to_process)
272
+ WHERE partition_id = $partition_id
273
+ AND consumer_group = $consumer_group
274
+ AND released_at IS NULL
275
+ )
276
+ -- Insert status records and return messages
277
+ INSERT INTO queen.messages_status (message_id, consumer_group, status, lease_expires_at)
278
+ SELECT id, $consumer_group, 'processing', NOW() + INTERVAL '1 second' * $lease_time
279
+ FROM messages_to_process
280
+ ON CONFLICT (message_id, consumer_group) DO UPDATE
281
+ SET status = 'processing', lease_expires_at = EXCLUDED.lease_expires_at
282
+ RETURNING (SELECT json_agg(row_to_json(m)) FROM messages_to_process m);
283
+ ```
284
+
285
+ ### Step 3: ACK Releases the Partition Lease
286
+
287
+ ```sql
288
+ -- When messages are ACKed, release the partition lease
289
+ WITH ack_messages AS (
290
+ UPDATE queen.messages_status
291
+ SET status = 'completed',
292
+ completed_at = NOW()
293
+ WHERE message_id = ANY($message_ids)
294
+ AND consumer_group = $consumer_group
295
+ RETURNING message_id
296
+ )
297
+ -- Release the partition lease
298
+ UPDATE queen.partition_leases
299
+ SET released_at = NOW()
300
+ WHERE partition_id = $partition_id
301
+ AND consumer_group = $consumer_group
302
+ AND released_at IS NULL;
303
+ ```
304
+
305
+ ## Lease Expiration Handling
306
+
307
+ ### Background Process: Reclaim Expired Leases
308
+
309
+ ```sql
310
+ -- Run periodically (every few seconds)
311
+ WITH expired_leases AS (
312
+ UPDATE queen.partition_leases
313
+ SET released_at = NOW()
314
+ WHERE lease_expires_at < NOW()
315
+ AND released_at IS NULL
316
+ RETURNING partition_id, consumer_group, message_batch
317
+ )
318
+ -- Reset message status for messages in expired leases
319
+ UPDATE queen.messages_status ms
320
+ SET status = 'pending',
321
+ retry_count = retry_count + 1,
322
+ failed_at = NOW(),
323
+ error_message = 'Partition lease expired'
324
+ FROM expired_leases el
325
+ WHERE ms.message_id = ANY(
326
+ SELECT jsonb_array_elements_text(el.message_batch)::uuid
327
+ )
328
+ AND ms.consumer_group = el.consumer_group;
329
+ ```
330
+
331
+ ## Benefits of This Approach
332
+
333
+ ### 1. **True Sequential Processing**
334
+ - Only one worker processes a partition at a time
335
+ - Messages K1, K2, K3 MUST complete before K4, K5, K6 can start
336
+ - Absolute FIFO guarantee
337
+
338
+ ### 2. **Per Consumer Group Isolation**
339
+ - Queue mode: One worker per partition
340
+ - Bus mode: One worker per partition per consumer group
341
+ - Different consumer groups don't block each other
342
+
343
+ ### 3. **Automatic Failover**
344
+ - If a worker dies, lease expires automatically
345
+ - Another worker can claim the partition
346
+ - Messages are retried in order
347
+
348
+ ### 4. **Clear Ownership**
349
+ - partition_leases table shows exactly who owns what
350
+ - Easy to debug and monitor
351
+ - Can implement partition stealing for load balancing
352
+
353
+ ## Trade-offs
354
+
355
+ ### Pros:
356
+ - ✅ Absolute ordering guarantees
357
+ - ✅ No out-of-order processing possible
358
+ - ✅ Simple mental model
359
+ - ✅ Predictable behavior
360
+ - ✅ Works well for ordered event streams
361
+
362
+ ### Cons:
363
+ - ❌ Lower throughput (one worker per partition)
364
+ - ❌ Potential for partition imbalance
365
+ - ❌ Slower processing if one partition has many messages
366
+ - ❌ Worker underutilization if fewer partitions than workers
367
+
368
+ ## Implementation Priority
369
+
370
+ 1. **Phase 1: Basic Partition Leasing**
371
+ - Create partition_leases table
372
+ - Implement lease acquisition in pop
373
+ - Implement lease release in ACK
374
+ - Add lease expiration handling
375
+
376
+ 2. **Phase 2: Monitoring & Observability**
377
+ - Add metrics for lease utilization
378
+ - Track partition processing times
379
+ - Monitor worker efficiency
380
+
381
+ ## Simplified Flow (No Worker ID Needed!)
382
+
383
+ ### The Key Insight
384
+ We don't need to track WHO has the partition, just THAT it's locked for a consumer group.
385
+
386
+ ### Producer (Unchanged)
387
+ ```javascript
388
+ // Messages go to partitions as before
389
+ await client.push('orders', {
390
+ orderId: '123',
391
+ action: 'create'
392
+ }, { partition: 'customer_456' });
393
+ ```
394
+
395
+ ### Consumer Flow
396
+
397
+ #### Step 1: Consumer A calls pop()
398
+ ```javascript
399
+ const messages = await client.pop('orders', { batch: 10 });
400
+ ```
401
+ **Behind the scenes:**
402
+ 1. Find an available partition (no active lease for this consumer group)
403
+ 2. Create lease: `(partition_K, consumer_group_X) -> locked until T+300s`
404
+ 3. Return messages from partition K only
405
+
406
+ #### Step 2: Consumer B calls pop() (while A is processing)
407
+ ```javascript
408
+ const messages = await client.pop('orders', { batch: 10 });
409
+ ```
410
+ **Behind the scenes:**
411
+ 1. Partition K is locked for consumer_group_X
412
+ 2. Find a different available partition (e.g., Partition L)
413
+ 3. Create lease: `(partition_L, consumer_group_X) -> locked until T+300s`
414
+ 4. Return messages from partition L only
415
+
416
+ #### Step 3: Consumer A finishes and ACKs
417
+ ```javascript
418
+ await client.ack(messages);
419
+ ```
420
+ **Behind the scenes:**
421
+ 1. Mark messages as completed
422
+ 2. Release lease: `(partition_K, consumer_group_X) -> released`
423
+ 3. Partition K is now available for any consumer
424
+
425
+ ### The Beauty: No Worker ID Required!
426
+ - We only track: `(partition_id, consumer_group) -> lease_expires_at`
427
+ - Any consumer can acquire an available partition
428
+ - Any consumer can release a partition by ACKing its messages
429
+ - If consumer crashes, lease expires automatically
430
+
431
+ ## Monitoring Queries
432
+
433
+ ### Active Leases (Simplified - No Worker ID)
434
+ ```sql
435
+ SELECT
436
+ q.name as queue_name,
437
+ p.name as partition_name,
438
+ pl.consumer_group,
439
+ pl.lease_expires_at,
440
+ jsonb_array_length(pl.message_batch) as message_count,
441
+ EXTRACT(EPOCH FROM (pl.lease_expires_at - NOW())) as seconds_remaining
442
+ FROM queen.partition_leases pl
443
+ JOIN queen.partitions p ON pl.partition_id = p.id
444
+ JOIN queen.queues q ON p.queue_id = q.id
445
+ WHERE pl.released_at IS NULL
446
+ ORDER BY q.name, p.name;
447
+ ```
448
+
449
+ ### Partition Utilization
450
+ ```sql
451
+ SELECT
452
+ q.name as queue_name,
453
+ COUNT(DISTINCT p.id) as total_partitions,
454
+ COUNT(DISTINCT pl.partition_id) as leased_partitions,
455
+ ROUND(100.0 * COUNT(DISTINCT pl.partition_id) / NULLIF(COUNT(DISTINCT p.id), 0), 2) as utilization_pct
456
+ FROM queen.queues q
457
+ LEFT JOIN queen.partitions p ON q.id = p.queue_id
458
+ LEFT JOIN queen.partition_leases pl ON p.id = pl.partition_id
459
+ AND pl.released_at IS NULL
460
+ AND pl.lease_expires_at > NOW()
461
+ GROUP BY q.name;
462
+ ```
463
+
464
+ ### Stuck Partitions (Leases About to Expire)
465
+ ```sql
466
+ SELECT
467
+ q.name as queue_name,
468
+ p.name as partition_name,
469
+ pl.consumer_group,
470
+ pl.lease_expires_at,
471
+ pl.created_at as lease_started_at,
472
+ EXTRACT(EPOCH FROM (NOW() - pl.created_at)) as lease_held_seconds
473
+ FROM queen.partition_leases pl
474
+ JOIN queen.partitions p ON pl.partition_id = p.id
475
+ JOIN queen.queues q ON p.queue_id = q.id
476
+ WHERE pl.released_at IS NULL
477
+ AND pl.lease_expires_at < NOW() + INTERVAL '30 seconds'
478
+ ORDER BY pl.lease_expires_at;
479
+ ```
480
+
481
+ ## New Features: Transaction ID, Trace ID, and Queue Limits
482
+
483
+ ### 1. Transaction ID as String
484
+ **Change:** `transaction_id` is now `VARCHAR(255)` instead of `UUID`
485
+
486
+ **Benefits:**
487
+ - More flexibility for client-generated IDs
488
+ - Support for external system IDs (order IDs, request IDs, etc.)
489
+ - Easier integration with existing systems
490
+ - Human-readable identifiers possible
491
+
492
+ **Example Usage:**
493
+ ```javascript
494
+ await push('orders', {
495
+ transactionId: 'ORDER-2024-001234', // Meaningful string ID
496
+ payload: { ... }
497
+ });
498
+ ```
499
+
500
+ ### 2. Trace ID for Pipeline Tracking
501
+ **New Field:** `trace_id UUID` for tracking messages across multi-step pipelines
502
+
503
+ **Use Cases:**
504
+ - Track related messages through multiple queues
505
+ - Correlate events in distributed systems
506
+ - Debug message flows in complex pipelines
507
+ - Audit trails for multi-step processes
508
+
509
+ **Example Pipeline:**
510
+ ```javascript
511
+ const traceId = generateUUID();
512
+
513
+ // Step 1: Order received
514
+ await push('order-intake', {
515
+ transactionId: 'ORDER-001',
516
+ traceId: traceId, // Same trace ID
517
+ payload: { action: 'validate' }
518
+ });
519
+
520
+ // Step 2: Payment processing
521
+ await push('payment-queue', {
522
+ transactionId: 'PAYMENT-001',
523
+ traceId: traceId, // Same trace ID links them
524
+ payload: { action: 'charge' }
525
+ });
526
+
527
+ // Step 3: Fulfillment
528
+ await push('shipping-queue', {
529
+ transactionId: 'SHIP-001',
530
+ traceId: traceId, // Same trace ID
531
+ payload: { action: 'ship' }
532
+ });
533
+
534
+ // Query all messages in pipeline
535
+ SELECT * FROM queen.messages WHERE trace_id = $traceId;
536
+ ```
537
+
538
+ ### 3. Max Queue Size Limits
539
+ **New Field:** `max_queue_size INTEGER` on queues table
540
+
541
+ **Features:**
542
+ - Prevent queue overflow
543
+ - Backpressure mechanism
544
+ - Resource protection
545
+ - Configurable per queue (0 = unlimited)
546
+
547
+ **Configuration:**
548
+ ```sql
549
+ -- Set max size for a queue
550
+ UPDATE queen.queues
551
+ SET max_queue_size = 10000
552
+ WHERE name = 'high-volume-queue';
553
+
554
+ -- Check current capacity
555
+ SELECT
556
+ queue_name,
557
+ current_depth,
558
+ max_queue_size,
559
+ remaining_capacity
560
+ FROM queen.queue_depths
561
+ WHERE queue_name = 'high-volume-queue';
562
+ ```
563
+
564
+ **Error Handling:**
565
+ ```javascript
566
+ try {
567
+ await push('limited-queue', message);
568
+ } catch (error) {
569
+ if (error.code === 'QUEUE_FULL') {
570
+ // Implement backpressure strategy
571
+ await delay(1000);
572
+ await retryWithExponentialBackoff();
573
+ }
574
+ }
575
+ ```
576
+
577
+ ## Summary: Why This Design is Right
578
+
579
+ ### The Problem We're Solving
580
+ - Current system allows multiple workers to get messages from the same partition
581
+ - Messages can complete out of order (Worker B finishes K4-K6 before Worker A finishes K1-K3)
582
+ - This breaks FIFO guarantees within partitions
583
+
584
+ ### The Solution: Partition Leasing (Without Worker ID)
585
+ 1. **One consumer at a time per partition per consumer group**
586
+ 2. **Track only (partition, consumer_group) -> lease_expires_at**
587
+ 3. **No need to track which specific worker/process has the lease**
588
+
589
+ ### Why No Worker ID?
590
+ - **Stateless consumers**: Any consumer instance can handle any message
591
+ - **Simpler design**: Just track if partition is locked, not who has it
592
+ - **Easier scaling**: Add/remove consumer instances without registration
593
+ - **Natural failover**: Lease expires, any consumer can claim it
594
+
595
+ ### The Flow
596
+ 1. Consumer calls `pop()` → System finds unlocked partition → Creates lease
597
+ 2. Consumer gets messages from ONLY that partition (true FIFO)
598
+ 3. Consumer calls `ack()` → Lease released → Partition available again
599
+ 4. If consumer crashes → Lease expires automatically → Another consumer takes over
600
+
601
+ ### Key Benefits
602
+ - ✅ **Absolute FIFO within partitions**: Messages K1-K3 MUST complete before K4-K6 can start
603
+ - ✅ **Simple state management**: Just (partition, consumer_group, lease_expires_at)
604
+ - ✅ **Automatic recovery**: Lease expiration handles failures
605
+ - ✅ **Per-consumer-group isolation**: Different groups don't interfere
606
+
607
+ ### Trade-offs Accepted
608
+ - ❌ Lower throughput (one consumer per partition)
609
+ - ❌ But this is REQUIRED for true FIFO ordering
610
+ - ❌ Mitigated by having multiple partitions
611
+
612
+ This design ensures **correctness over performance** - exactly what's needed for ordered message processing.
613
+
614
+ ## Additional Query Improvements to Implement
615
+
616
+ ### 1. Security and Transaction Hardening
617
+
618
+ #### Whitelist Transaction Isolation Levels
619
+ ```javascript
620
+ // In database/connection.js
621
+ const VALID_ISOLATION_LEVELS = [
622
+ 'READ COMMITTED',
623
+ 'REPEATABLE READ',
624
+ 'SERIALIZABLE'
625
+ ];
626
+
627
+ export const withTransaction = async (pool, callback, isolationLevel = 'READ COMMITTED') => {
628
+ if (!VALID_ISOLATION_LEVELS.includes(isolationLevel)) {
629
+ throw new Error(`Invalid isolation level: ${isolationLevel}`);
630
+ }
631
+
632
+ const client = await pool.connect();
633
+ try {
634
+ await client.query('BEGIN');
635
+ await client.query(`SET TRANSACTION ISOLATION LEVEL ${isolationLevel}`);
636
+
637
+ // Set timeouts to prevent long blocks
638
+ await client.query('SET LOCAL statement_timeout = 30000'); // 30 seconds
639
+ await client.query('SET LOCAL lock_timeout = 5000'); // 5 seconds
640
+
641
+ const result = await callback(client);
642
+ await client.query('COMMIT');
643
+ return result;
644
+ } catch (error) {
645
+ await client.query('ROLLBACK');
646
+
647
+ // Retry on serialization failures and deadlocks
648
+ if (error.code === '40001' || error.code === '40P01') {
649
+ // Implement exponential backoff retry
650
+ return retryTransaction(pool, callback, isolationLevel);
651
+ }
652
+ throw error;
653
+ } finally {
654
+ client.release();
655
+ }
656
+ };
657
+ ```
658
+
659
+ #### Retry Logic for Contention
660
+ ```javascript
661
+ const retryTransaction = async (pool, callback, isolationLevel, attempt = 1, maxAttempts = 3) => {
662
+ if (attempt > maxAttempts) {
663
+ throw new Error('Max transaction retry attempts exceeded');
664
+ }
665
+
666
+ // Exponential backoff: 100ms, 200ms, 400ms
667
+ const delay = Math.min(100 * Math.pow(2, attempt - 1), 1000);
668
+ await new Promise(resolve => setTimeout(resolve, delay));
669
+
670
+ try {
671
+ return await withTransaction(pool, callback, isolationLevel);
672
+ } catch (error) {
673
+ if ((error.code === '40001' || error.code === '40P01') && attempt < maxAttempts) {
674
+ return retryTransaction(pool, callback, isolationLevel, attempt + 1, maxAttempts);
675
+ }
676
+ throw error;
677
+ }
678
+ };
679
+ ```
680
+
681
+ ### 2. Optimized Pop Query with Partition Leasing
682
+
683
+ #### Complete Pop Implementation with All Improvements
684
+ ```sql
685
+ -- Pop with partition leasing, optimized predicates, and clean RETURNING
686
+ WITH queue_config AS (
687
+ -- Precompute time thresholds once
688
+ SELECT
689
+ id as queue_id,
690
+ delayed_processing,
691
+ max_wait_time_seconds,
692
+ window_buffer,
693
+ lease_time,
694
+ NOW() - INTERVAL '1 second' * delayed_processing as earliest_available,
695
+ NOW() - INTERVAL '1 second' * max_wait_time_seconds as latest_created,
696
+ NOW() - INTERVAL '1 second' * window_buffer as window_cutoff
697
+ FROM queen.queues
698
+ WHERE name = $1
699
+ ),
700
+ available_partitions AS (
701
+ -- Find partitions with available messages and no active lease
702
+ SELECT DISTINCT
703
+ p.id,
704
+ p.name,
705
+ MIN(m.created_at) as oldest_message,
706
+ COUNT(m.id) as pending_count
707
+ FROM queen.partitions p
708
+ JOIN queue_config qc ON p.queue_id = qc.queue_id
709
+ JOIN queen.messages m ON m.partition_id = p.id
710
+ WHERE NOT EXISTS (
711
+ -- No active lease for this partition/consumer group
712
+ SELECT 1 FROM queen.partition_leases pl
713
+ WHERE pl.partition_id = p.id
714
+ AND pl.consumer_group = $2
715
+ AND pl.released_at IS NULL
716
+ AND pl.lease_expires_at > NOW()
717
+ )
718
+ AND m.created_at <= qc.earliest_available
719
+ AND (qc.max_wait_time_seconds = 0 OR m.created_at > qc.latest_created)
720
+ AND NOT EXISTS (
721
+ -- No messages in window buffer
722
+ SELECT 1 FROM queen.messages m2
723
+ WHERE m2.partition_id = p.id
724
+ AND qc.window_buffer > 0
725
+ AND m2.created_at > qc.window_cutoff
726
+ )
727
+ AND NOT EXISTS (
728
+ -- Message not already processed/processing
729
+ SELECT 1 FROM queen.messages_status ms
730
+ WHERE ms.message_id = m.id
731
+ AND ms.consumer_group = $2
732
+ AND ms.status NOT IN ('pending', 'failed')
733
+ )
734
+ GROUP BY p.id, p.name
735
+ ORDER BY
736
+ oldest_message ASC, -- Fairness: oldest first
737
+ pending_count DESC -- Then by most messages
738
+ LIMIT 1
739
+ FOR UPDATE OF p SKIP LOCKED
740
+ ),
741
+ acquire_lease AS (
742
+ INSERT INTO queen.partition_leases (
743
+ partition_id,
744
+ consumer_group,
745
+ lease_expires_at
746
+ )
747
+ SELECT
748
+ id,
749
+ $2,
750
+ NOW() + INTERVAL '1 second' * (SELECT lease_time FROM queue_config)
751
+ FROM available_partitions
752
+ ON CONFLICT (partition_id, consumer_group) DO NOTHING
753
+ RETURNING partition_id, id as lease_id, lease_expires_at
754
+ ),
755
+ messages_to_process AS (
756
+ SELECT
757
+ m.id,
758
+ m.transaction_id, -- Now a string
759
+ m.trace_id, -- New field for pipeline tracking
760
+ m.payload,
761
+ m.is_encrypted,
762
+ m.created_at,
763
+ p.name as partition_name,
764
+ q.name as queue_name,
765
+ al.lease_id,
766
+ al.lease_expires_at
767
+ FROM acquire_lease al
768
+ JOIN queen.messages m ON m.partition_id = al.partition_id
769
+ JOIN queen.partitions p ON p.id = al.partition_id
770
+ JOIN queen.queues q ON q.id = p.queue_id
771
+ JOIN queue_config qc ON q.id = qc.queue_id
772
+ WHERE NOT EXISTS (
773
+ SELECT 1 FROM queen.messages_status ms
774
+ WHERE ms.message_id = m.id
775
+ AND ms.consumer_group = $2
776
+ AND ms.status NOT IN ('pending', 'failed')
777
+ )
778
+ AND m.created_at <= qc.earliest_available
779
+ AND (qc.max_wait_time_seconds = 0 OR m.created_at > qc.latest_created)
780
+ ORDER BY m.created_at ASC
781
+ LIMIT $3
782
+ FOR UPDATE OF m SKIP LOCKED
783
+ ),
784
+ update_lease AS (
785
+ UPDATE queen.partition_leases
786
+ SET message_batch = (SELECT jsonb_agg(id) FROM messages_to_process)
787
+ WHERE id = (SELECT lease_id FROM messages_to_process LIMIT 1)
788
+ ),
789
+ insert_status AS (
790
+ INSERT INTO queen.messages_status (
791
+ message_id,
792
+ consumer_group,
793
+ status,
794
+ lease_expires_at,
795
+ processing_at
796
+ )
797
+ SELECT
798
+ id,
799
+ $2,
800
+ 'processing',
801
+ lease_expires_at,
802
+ NOW()
803
+ FROM messages_to_process
804
+ ON CONFLICT (message_id, consumer_group)
805
+ DO UPDATE SET
806
+ status = 'processing',
807
+ lease_expires_at = EXCLUDED.lease_expires_at,
808
+ processing_at = EXCLUDED.processing_at,
809
+ retry_count = queen.messages_status.retry_count
810
+ )
811
+ -- Clean RETURNING without correlated subqueries
812
+ SELECT
813
+ id as message_id,
814
+ transaction_id, -- String now
815
+ trace_id, -- For pipeline tracking
816
+ payload,
817
+ is_encrypted,
818
+ created_at,
819
+ partition_name,
820
+ queue_name
821
+ FROM messages_to_process;
822
+ ```
823
+
824
+ ### 3. Optimized ACK with Partition Release
825
+
826
+ ```sql
827
+ -- ACK messages and release partition lease
828
+ WITH ack_messages AS (
829
+ UPDATE queen.messages_status
830
+ SET
831
+ status = 'completed',
832
+ completed_at = NOW()
833
+ WHERE message_id = ANY($1::uuid[])
834
+ AND consumer_group = $2
835
+ AND status = 'processing'
836
+ RETURNING message_id
837
+ ),
838
+ get_partition AS (
839
+ -- Find which partition these messages belong to
840
+ SELECT DISTINCT m.partition_id
841
+ FROM queen.messages m
842
+ WHERE m.id = ANY(SELECT message_id FROM ack_messages)
843
+ LIMIT 1
844
+ )
845
+ -- Release the partition lease
846
+ UPDATE queen.partition_leases pl
847
+ SET released_at = NOW()
848
+ FROM get_partition gp
849
+ WHERE pl.partition_id = gp.partition_id
850
+ AND pl.consumer_group = $2
851
+ AND pl.released_at IS NULL
852
+ RETURNING pl.partition_id;
853
+ ```
854
+
855
+ ### 4. Keyset Pagination for listMessages API
856
+
857
+ ```sql
858
+ -- Keyset pagination using (created_at, id) for stable ordering
859
+ CREATE INDEX idx_messages_pagination ON queen.messages(created_at DESC, id DESC);
860
+
861
+ -- First page
862
+ SELECT * FROM queen.messages
863
+ WHERE partition_id = $partition_id
864
+ ORDER BY created_at DESC, id DESC
865
+ LIMIT $limit;
866
+
867
+ -- Next pages (pass last_created_at and last_id from previous page)
868
+ SELECT * FROM queen.messages
869
+ WHERE partition_id = $partition_id
870
+ AND (created_at, id) < ($last_created_at, $last_id)
871
+ ORDER BY created_at DESC, id DESC
872
+ LIMIT $limit;
873
+ ```
874
+
875
+ ### 5. Improved Retention Service
876
+
877
+ ```sql
878
+ -- Retention that respects schema v2 and doesn't delete unprocessed messages
879
+ WITH retention_candidates AS (
880
+ SELECT
881
+ m.id,
882
+ m.partition_id,
883
+ q.retention_seconds,
884
+ q.completed_retention_seconds
885
+ FROM queen.messages m
886
+ JOIN queen.partitions p ON m.partition_id = p.id
887
+ JOIN queen.queues q ON p.queue_id = q.id
888
+ WHERE q.retention_enabled = true
889
+ AND (
890
+ -- Completed messages past completed retention
891
+ EXISTS (
892
+ SELECT 1 FROM queen.messages_status ms
893
+ WHERE ms.message_id = m.id
894
+ AND ms.status = 'completed'
895
+ AND ms.completed_at < NOW() - INTERVAL '1 second' * q.completed_retention_seconds
896
+ )
897
+ OR
898
+ -- Any messages past general retention (if not processing)
899
+ (
900
+ m.created_at < NOW() - INTERVAL '1 second' * q.retention_seconds
901
+ AND NOT EXISTS (
902
+ SELECT 1 FROM queen.messages_status ms
903
+ WHERE ms.message_id = m.id
904
+ AND ms.status IN ('processing', 'pending')
905
+ )
906
+ )
907
+ )
908
+ ),
909
+ deleted AS (
910
+ DELETE FROM queen.messages
911
+ WHERE id IN (SELECT id FROM retention_candidates)
912
+ RETURNING id, partition_id
913
+ )
914
+ -- Log retention activity
915
+ INSERT INTO queen.retention_history (queue_id, partition_id, messages_deleted, deleted_at)
916
+ SELECT
917
+ p.queue_id,
918
+ d.partition_id,
919
+ COUNT(*),
920
+ NOW()
921
+ FROM deleted d
922
+ JOIN queen.partitions p ON d.partition_id = p.id
923
+ GROUP BY p.queue_id, d.partition_id;
924
+ ```
925
+
926
+ ### 6. Performance Indexes
927
+
928
+ ```sql
929
+ -- Critical indexes for the new design
930
+ CREATE INDEX idx_partition_leases_active_lookup
931
+ ON queen.partition_leases(partition_id, consumer_group, lease_expires_at)
932
+ WHERE released_at IS NULL;
933
+
934
+ CREATE INDEX idx_messages_partition_created
935
+ ON queen.messages(partition_id, created_at ASC);
936
+
937
+ CREATE INDEX idx_messages_status_lookup
938
+ ON queen.messages_status(message_id, consumer_group, status);
939
+
940
+ CREATE INDEX idx_messages_status_processing
941
+ ON queen.messages_status(consumer_group, status)
942
+ WHERE status = 'processing';
943
+
944
+ -- Partial index for finding available messages
945
+ CREATE INDEX idx_messages_available
946
+ ON queen.messages(partition_id, created_at)
947
+ WHERE id NOT IN (
948
+ SELECT message_id FROM queen.messages_status
949
+ WHERE status NOT IN ('pending', 'failed')
950
+ );
951
+ ```
952
+
953
+ ## Implementation Checklist
954
+
955
+ ### Phase 1: Schema Changes
956
+ - [ ] Alter messages table: change transaction_id to VARCHAR(255)
957
+ - [ ] Add trace_id UUID column to messages table
958
+ - [ ] Add max_queue_size column to queues table
959
+ - [ ] Create partition_leases table
960
+ - [ ] Add all required indexes
961
+
962
+ ### Phase 2: Core Functionality
963
+ - [ ] Implement push with queue size checking
964
+ - [ ] Add transaction hardening with timeouts
965
+ - [ ] Implement retry logic for deadlocks
966
+ - [ ] Rewrite pop logic with partition leasing
967
+ - [ ] Update ACK to release partition leases
968
+ - [ ] Implement lease expiration background job
969
+
970
+ ### Phase 3: Query Optimization
971
+ - [ ] Optimize predicates with NOT EXISTS
972
+ - [ ] Precompute time thresholds in CTEs
973
+ - [ ] Clean up RETURNING clauses
974
+ - [ ] Implement keyset pagination
975
+ - [ ] Add performance indexes
976
+
977
+ ### Phase 4: Supporting Features
978
+ - [ ] Update retention service for new schema
979
+ - [ ] Add trace_id tracking utilities
980
+ - [ ] Implement queue capacity monitoring
981
+ - [ ] Add backpressure handling in client SDK
982
+
983
+ ### Phase 5: Testing & Documentation
984
+ - [ ] Test partition leasing under load
985
+ - [ ] Test queue capacity limits
986
+ - [ ] Test trace_id pipeline tracking
987
+ - [ ] Test string transaction_ids
988
+ - [ ] Update API documentation
989
+ - [ ] Write migration guide