queen-mq 0.1.0 → 0.1.2
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/API.md +862 -752
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +1705 -1051
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-complete-client.js +260 -0
- package/examples/test-dashboard-api.js +200 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +17 -4
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +300 -31
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +361 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +561 -0
- package/src/test/enterprise-tests.js +637 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
- package/src/routes/analytics.js +0 -812
|
@@ -1,989 +0,0 @@
|
|
|
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
|