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
|
@@ -5,37 +5,66 @@ import config from '../config.js';
|
|
|
5
5
|
const EVICTION_INTERVAL = config.JOBS.EVICTION_INTERVAL;
|
|
6
6
|
|
|
7
7
|
// Evict messages that exceeded max wait time for a queue
|
|
8
|
+
// Note: Eviction is already handled by POP query filtering old messages
|
|
9
|
+
// This service optionally moves them to DLQ for visibility
|
|
8
10
|
const evictMessages = async (client, queueName, maxWaitTimeSeconds) => {
|
|
9
11
|
if (!maxWaitTimeSeconds || maxWaitTimeSeconds <= 0) return 0;
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
13
|
+
try {
|
|
14
|
+
// Find old messages that haven't been consumed and move to DLQ
|
|
15
|
+
// Use a WHERE EXISTS check to ensure message still exists before inserting into DLQ
|
|
16
|
+
const result = await client.query(`
|
|
17
|
+
WITH queue_partitions AS (
|
|
18
|
+
SELECT p.id, p.queue_id
|
|
19
|
+
FROM queen.partitions p
|
|
20
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
21
|
+
WHERE q.name = $1
|
|
22
|
+
),
|
|
23
|
+
old_unconsumed_messages AS (
|
|
24
|
+
SELECT m.id, m.partition_id, m.created_at
|
|
25
|
+
FROM queen.messages m
|
|
26
|
+
JOIN queue_partitions qp ON m.partition_id = qp.id
|
|
27
|
+
LEFT JOIN queen.partition_consumers pc ON pc.partition_id = m.partition_id
|
|
28
|
+
AND pc.consumer_group = '__QUEUE_MODE__'
|
|
29
|
+
WHERE m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
30
|
+
-- Message has not been consumed yet
|
|
31
|
+
AND ((m.created_at, m.id) > (COALESCE(pc.last_consumed_created_at, '1970-01-01'::timestamptz),
|
|
32
|
+
COALESCE(pc.last_consumed_id, '00000000-0000-0000-0000-000000000000'::uuid))
|
|
33
|
+
OR pc.last_consumed_id IS NULL)
|
|
34
|
+
)
|
|
35
|
+
INSERT INTO queen.dead_letter_queue (
|
|
36
|
+
message_id,
|
|
37
|
+
partition_id,
|
|
38
|
+
consumer_group,
|
|
39
|
+
error_message,
|
|
40
|
+
original_created_at
|
|
41
|
+
)
|
|
42
|
+
SELECT
|
|
43
|
+
om.id,
|
|
44
|
+
om.partition_id,
|
|
45
|
+
'__QUEUE_MODE__',
|
|
46
|
+
'Message exceeded maximum wait time',
|
|
47
|
+
om.created_at
|
|
48
|
+
FROM old_unconsumed_messages om
|
|
49
|
+
WHERE EXISTS (
|
|
50
|
+
SELECT 1 FROM queen.messages m WHERE m.id = om.id
|
|
51
|
+
)
|
|
52
|
+
ON CONFLICT DO NOTHING
|
|
53
|
+
RETURNING message_id
|
|
54
|
+
`, [queueName, maxWaitTimeSeconds]);
|
|
55
|
+
|
|
56
|
+
return result.rowCount || 0;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
// Catch foreign key violations (messages deleted between SELECT and INSERT)
|
|
59
|
+
if (error.code === '23503') {
|
|
60
|
+
// Foreign key constraint violation - message was deleted
|
|
61
|
+
// This is a harmless race condition, just log it
|
|
62
|
+
log(`${LogTypes.EVICTION} | Queue: ${queueName} | Warning: Message deleted during eviction (race condition)`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
// Re-throw other errors
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
39
68
|
};
|
|
40
69
|
|
|
41
70
|
// Evict messages during pop operation (inline eviction)
|
|
@@ -16,30 +16,44 @@ const retentionQueue = async (client, queue) => {
|
|
|
16
16
|
|
|
17
17
|
let totalDeleted = 0;
|
|
18
18
|
|
|
19
|
-
// Delete old
|
|
19
|
+
// Delete old unconsumed messages from all partitions in this queue
|
|
20
20
|
if (retentionSeconds > 0) {
|
|
21
21
|
const result = await client.query(`
|
|
22
|
-
DELETE FROM queen.messages
|
|
23
|
-
WHERE partition_id IN (
|
|
22
|
+
DELETE FROM queen.messages m
|
|
23
|
+
WHERE m.partition_id IN (
|
|
24
24
|
SELECT id FROM queen.partitions WHERE queue_id = $1
|
|
25
25
|
)
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
AND m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
27
|
+
AND NOT EXISTS (
|
|
28
|
+
-- Message has been consumed by at least one consumer
|
|
29
|
+
SELECT 1
|
|
30
|
+
FROM queen.partition_consumers pc
|
|
31
|
+
WHERE pc.partition_id = m.partition_id
|
|
32
|
+
AND (m.created_at, m.id) <= (pc.last_consumed_created_at, pc.last_consumed_id)
|
|
33
|
+
)
|
|
28
34
|
RETURNING id
|
|
29
35
|
`, [queue.id, retentionSeconds]);
|
|
30
36
|
|
|
31
37
|
totalDeleted += result.rowCount || 0;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
// Delete
|
|
40
|
+
// Delete old consumed messages from all partitions in this queue
|
|
41
|
+
// (messages consumed by ALL consumer groups on that partition)
|
|
35
42
|
if (completedRetentionSeconds > 0) {
|
|
36
43
|
const result = await client.query(`
|
|
37
|
-
DELETE FROM queen.messages
|
|
38
|
-
WHERE partition_id IN (
|
|
44
|
+
DELETE FROM queen.messages m
|
|
45
|
+
WHERE m.partition_id IN (
|
|
39
46
|
SELECT id FROM queen.partitions WHERE queue_id = $1
|
|
40
47
|
)
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
AND m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
49
|
+
AND NOT EXISTS (
|
|
50
|
+
-- No consumer group that hasn't consumed this message yet
|
|
51
|
+
SELECT 1
|
|
52
|
+
FROM queen.partition_consumers pc
|
|
53
|
+
WHERE pc.partition_id = m.partition_id
|
|
54
|
+
AND ((m.created_at, m.id) > (pc.last_consumed_created_at, pc.last_consumed_id)
|
|
55
|
+
OR pc.last_consumed_id IS NULL)
|
|
56
|
+
)
|
|
43
57
|
RETURNING id
|
|
44
58
|
`, [queue.id, completedRetentionSeconds]);
|
|
45
59
|
|
|
@@ -48,7 +62,6 @@ const retentionQueue = async (client, queue) => {
|
|
|
48
62
|
|
|
49
63
|
// Log retention if messages were deleted
|
|
50
64
|
if (totalDeleted > 0) {
|
|
51
|
-
// Note: We might need to update retention_history table to use queue_id instead
|
|
52
65
|
log(`${LogTypes.RETENTION} | Queue: ${queue.name} | Count: ${totalDeleted} | RetentionSeconds: ${retentionSeconds} | CompletedRetentionSeconds: ${completedRetentionSeconds}`);
|
|
53
66
|
}
|
|
54
67
|
|
|
@@ -76,6 +89,23 @@ const cleanupEmptyPartitions = async (client) => {
|
|
|
76
89
|
return result.rowCount;
|
|
77
90
|
};
|
|
78
91
|
|
|
92
|
+
// Clean up old metrics data (messages_consumed table)
|
|
93
|
+
const cleanupOldMetrics = async (client) => {
|
|
94
|
+
const retentionDays = config.JOBS.METRICS_RETENTION_DAYS;
|
|
95
|
+
|
|
96
|
+
// Delete metrics data older than the retention period
|
|
97
|
+
const result = await client.query(`
|
|
98
|
+
DELETE FROM queen.messages_consumed
|
|
99
|
+
WHERE acked_at < NOW() - INTERVAL '1 day' * $1
|
|
100
|
+
`, [retentionDays]);
|
|
101
|
+
|
|
102
|
+
if (result.rowCount > 0) {
|
|
103
|
+
log(`📊 Deleted ${result.rowCount} old metrics records (older than ${retentionDays} days)`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result.rowCount;
|
|
107
|
+
};
|
|
108
|
+
|
|
79
109
|
// Main retention function
|
|
80
110
|
const performRetention = async (pool) => {
|
|
81
111
|
const client = await pool.connect();
|
|
@@ -97,6 +127,9 @@ const performRetention = async (pool) => {
|
|
|
97
127
|
// Cleanup empty partitions
|
|
98
128
|
await cleanupEmptyPartitions(client);
|
|
99
129
|
|
|
130
|
+
// Cleanup old metrics data
|
|
131
|
+
await cleanupOldMetrics(client);
|
|
132
|
+
|
|
100
133
|
return totalDeleted;
|
|
101
134
|
} catch (error) {
|
|
102
135
|
log('Retention error:', error);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Test Migration Issues and Fixes
|
|
2
|
+
|
|
3
|
+
## Summary
|
|
4
|
+
|
|
5
|
+
When migrating tests from the old `queenClient` interface to the new minimalist `Queen` client interface, we discovered client bugs that needed fixing. The system is transactional - ACKs immediately release partition locks when awaited.
|
|
6
|
+
|
|
7
|
+
## Client Bugs Fixed
|
|
8
|
+
|
|
9
|
+
### 1. Null Payload Handling in `push()`
|
|
10
|
+
**Issue**: `typeof null === 'object'` is `true` in JavaScript, causing the code to try to access properties on `null` payloads.
|
|
11
|
+
|
|
12
|
+
**Location**: `src/client/client.js` line 153
|
|
13
|
+
|
|
14
|
+
**Fix**:
|
|
15
|
+
```javascript
|
|
16
|
+
// Before
|
|
17
|
+
const isMessageObject = typeof item === 'object' &&
|
|
18
|
+
(item._payload || item._transactionId || item._traceId);
|
|
19
|
+
|
|
20
|
+
// After
|
|
21
|
+
const isMessageObject = typeof item === 'object' && item !== null &&
|
|
22
|
+
(item._payload || item._transactionId || item._traceId);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Why**: Tests correctly validate edge cases including null payloads. The client must handle them gracefully.
|
|
26
|
+
|
|
27
|
+
### 2. Null Handling in `ack()`
|
|
28
|
+
**Issue**: Same null-checking issue when extracting transaction ID from message object.
|
|
29
|
+
|
|
30
|
+
**Location**: `src/client/client.js` line 330
|
|
31
|
+
|
|
32
|
+
**Fix**:
|
|
33
|
+
```javascript
|
|
34
|
+
// Before
|
|
35
|
+
const transactionId = typeof message === 'object' ?
|
|
36
|
+
(message.transactionId || message.id) :
|
|
37
|
+
message;
|
|
38
|
+
|
|
39
|
+
// After
|
|
40
|
+
const transactionId = typeof message === 'object' && message !== null ?
|
|
41
|
+
(message.transactionId || message.id) :
|
|
42
|
+
message;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 3. Consumer Group Parsing for Namespace/Task Addresses
|
|
46
|
+
**Issue**: The `#parseAddress()` method didn't extract consumer groups from namespace/task patterns like `namespace:billing@group-a`.
|
|
47
|
+
|
|
48
|
+
**Location**: `src/client/client.js` line 77-101
|
|
49
|
+
|
|
50
|
+
**Fix**: Extract `@group` portion before parsing namespace/task segments:
|
|
51
|
+
```javascript
|
|
52
|
+
#parseAddress(address) {
|
|
53
|
+
if (address.includes('namespace:') || address.includes('task:')) {
|
|
54
|
+
const parts = {};
|
|
55
|
+
|
|
56
|
+
// Extract consumer group if present (after @)
|
|
57
|
+
let workingAddress = address;
|
|
58
|
+
const atIndex = address.lastIndexOf('@');
|
|
59
|
+
if (atIndex > 0) {
|
|
60
|
+
parts.consumerGroup = address.substring(atIndex + 1);
|
|
61
|
+
workingAddress = address.substring(0, atIndex);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const segments = workingAddress.split('/');
|
|
65
|
+
for (const segment of segments) {
|
|
66
|
+
if (segment.startsWith('namespace:')) {
|
|
67
|
+
parts.namespace = segment.substring(10);
|
|
68
|
+
} else if (segment.startsWith('task:')) {
|
|
69
|
+
parts.task = segment.substring(5);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return parts;
|
|
73
|
+
}
|
|
74
|
+
// ...
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. Defensive Filtering of Null Messages
|
|
79
|
+
**Issue**: Server might return null values in the `messages` array response.
|
|
80
|
+
|
|
81
|
+
**Location**: `src/client/client.js` in `take()` method
|
|
82
|
+
|
|
83
|
+
**Fix**: Filter out null/undefined messages when yielding:
|
|
84
|
+
```javascript
|
|
85
|
+
// Yield each message (filter out any null/undefined values)
|
|
86
|
+
for (const message of result.messages) {
|
|
87
|
+
if (message) { // Skip null/undefined messages
|
|
88
|
+
yield message;
|
|
89
|
+
count++;
|
|
90
|
+
if (limit && count >= limit) return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Test Fixes
|
|
96
|
+
|
|
97
|
+
### 1. Consumer Group Isolation Test
|
|
98
|
+
**Problem**: Test was ACKing all messages as completed for both groups, but the test expects group2 to have some failed messages to verify isolation.
|
|
99
|
+
|
|
100
|
+
**Solution**: Actually fail the first 2 messages for group2:
|
|
101
|
+
```javascript
|
|
102
|
+
// Fail first 2 messages for group2, complete the rest
|
|
103
|
+
for (let i = 0; i < group2Messages.length; i++) {
|
|
104
|
+
if (i < 2) {
|
|
105
|
+
await client.ack(group2Messages[i], false, {
|
|
106
|
+
error: 'Test failure',
|
|
107
|
+
group: 'group2'
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
await client.ack(group2Messages[i], true, { group: 'group2' });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 2. Empty and Null Payloads Test
|
|
116
|
+
**Problem**: If server returns null message objects in the array, the test would crash.
|
|
117
|
+
|
|
118
|
+
**Solution**: Add defensive check (though client now filters these):
|
|
119
|
+
```javascript
|
|
120
|
+
for await (const msg of client.take(queue, { limit: 10 })) {
|
|
121
|
+
if (!msg) {
|
|
122
|
+
throw new Error('Received null/undefined message from take()');
|
|
123
|
+
}
|
|
124
|
+
messages.push(msg);
|
|
125
|
+
await client.ack(msg);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Key Learnings
|
|
130
|
+
|
|
131
|
+
### System is Transactional ✅
|
|
132
|
+
|
|
133
|
+
The Queen system is **transactional**. When you `await client.ack(msg)`:
|
|
134
|
+
- The ACK completes atomically
|
|
135
|
+
- The partition lock is released immediately
|
|
136
|
+
- No "timing issues" or delays needed
|
|
137
|
+
|
|
138
|
+
### Correct Pattern
|
|
139
|
+
```javascript
|
|
140
|
+
// ✅ This works correctly:
|
|
141
|
+
for await (const msg of client.take(queue, { limit: 10 })) {
|
|
142
|
+
await client.ack(msg); // Partition unlocked when this completes
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Next consumer can immediately access the partition:
|
|
146
|
+
for await (const msg of client.take(queue, { limit: 10 })) {
|
|
147
|
+
// Gets messages right away
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Edge Case Handling
|
|
152
|
+
|
|
153
|
+
1. **Null payloads are valid**: Tests should validate that the system handles `null`, `undefined`, empty objects, and empty strings as payloads.
|
|
154
|
+
|
|
155
|
+
2. **Client must be defensive**: Check for `typeof x === 'object' && x !== null` when you need to access properties.
|
|
156
|
+
|
|
157
|
+
3. **Server responses should be filtered**: If server returns null messages in an array, filter them out client-side.
|
|
158
|
+
|
|
159
|
+
### Test Design Principles
|
|
160
|
+
|
|
161
|
+
1. **Test real edge cases**: null, undefined, empty strings, large payloads, etc.
|
|
162
|
+
|
|
163
|
+
2. **Don't add artificial delays**: The system is transactional. If you need a delay, something is wrong.
|
|
164
|
+
|
|
165
|
+
3. **Verify isolation**: When testing consumer groups, actually test that different groups can have different statuses for the same messages.
|
|
166
|
+
|
|
167
|
+
4. **Use appropriate batch sizes**: Match batch size to expected message count for efficient testing.
|
|
168
|
+
|
|
169
|
+
## Final Results
|
|
170
|
+
|
|
171
|
+
After fixing client bugs and test issues:
|
|
172
|
+
- 33/33 tests should pass
|
|
173
|
+
- No timing sensitivity
|
|
174
|
+
- Clean, transactional behavior throughout
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Queen Message Queue Test Suite
|
|
2
|
+
|
|
3
|
+
This directory contains the comprehensive test suite for the Queen Message Queue system, using the new minimalist `Queen` client interface.
|
|
4
|
+
|
|
5
|
+
## Test Structure
|
|
6
|
+
|
|
7
|
+
The test suite is organized into focused, modular files:
|
|
8
|
+
|
|
9
|
+
### Core Files
|
|
10
|
+
|
|
11
|
+
- **`test-new.js`** - Main test runner that orchestrates all tests
|
|
12
|
+
- **`utils.js`** - Shared utilities (logging, database helpers, result tracking)
|
|
13
|
+
|
|
14
|
+
### Test Categories
|
|
15
|
+
|
|
16
|
+
#### 1. Core Features (`core-tests.js`)
|
|
17
|
+
- Queue creation policy
|
|
18
|
+
- Single/batch message push
|
|
19
|
+
- Queue configuration
|
|
20
|
+
- Take and acknowledgment
|
|
21
|
+
- Delayed processing
|
|
22
|
+
- FIFO ordering within partitions
|
|
23
|
+
|
|
24
|
+
#### 2. Partition Locking (`partition-locking-tests.js`)
|
|
25
|
+
- Partition locking in queue mode
|
|
26
|
+
- Partition locking in bus mode
|
|
27
|
+
- Specific partition requests with locking
|
|
28
|
+
- Namespace/task filtering with partition locking
|
|
29
|
+
|
|
30
|
+
#### 3. Enterprise Features (`enterprise-tests.js`)
|
|
31
|
+
- Message encryption/decryption
|
|
32
|
+
- Retention policies (pending & completed messages)
|
|
33
|
+
- Message eviction
|
|
34
|
+
- Combined enterprise features
|
|
35
|
+
- Enterprise error handling
|
|
36
|
+
|
|
37
|
+
#### 4. Bus Mode Features (`bus-mode-tests.js`)
|
|
38
|
+
- Consumer groups
|
|
39
|
+
- Mixed mode (queue + bus)
|
|
40
|
+
- Subscription modes (all vs new messages)
|
|
41
|
+
- Consumer group isolation
|
|
42
|
+
|
|
43
|
+
#### 5. Edge Cases (`edge-case-tests.js`)
|
|
44
|
+
- Empty and null payloads
|
|
45
|
+
- Very large payloads
|
|
46
|
+
- Concurrent push/take operations
|
|
47
|
+
- Retry limit exhaustion
|
|
48
|
+
- Lease expiration and redelivery
|
|
49
|
+
- SQL injection prevention
|
|
50
|
+
- XSS prevention
|
|
51
|
+
|
|
52
|
+
#### 6. Advanced Patterns (`advanced-pattern-tests.js`)
|
|
53
|
+
- Multi-stage pipeline workflow
|
|
54
|
+
- Fan-out/fan-in pattern
|
|
55
|
+
- Dead letter queue pattern
|
|
56
|
+
- Circuit breaker pattern
|
|
57
|
+
- Message deduplication
|
|
58
|
+
|
|
59
|
+
## New vs Old Interface
|
|
60
|
+
|
|
61
|
+
### Old Interface (test.js)
|
|
62
|
+
```javascript
|
|
63
|
+
import { createQueenClient } from '../client/queenClient.js';
|
|
64
|
+
|
|
65
|
+
const client = createQueenClient({ baseUrls: [...] });
|
|
66
|
+
|
|
67
|
+
// Configure
|
|
68
|
+
await client.configure({ queue: 'myqueue', options: {} });
|
|
69
|
+
|
|
70
|
+
// Push
|
|
71
|
+
await client.push({ items: [{ queue: 'myqueue', partition: 'Default', payload: {...} }] });
|
|
72
|
+
|
|
73
|
+
// Pop
|
|
74
|
+
const result = await client.pop({ queue: 'myqueue', batch: 10 });
|
|
75
|
+
|
|
76
|
+
// Ack
|
|
77
|
+
await client.ack(transactionId, 'completed', null, consumerGroup);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### New Interface (test-new.js)
|
|
81
|
+
```javascript
|
|
82
|
+
import { Queen } from '../client/client.js';
|
|
83
|
+
|
|
84
|
+
const client = new Queen({ baseUrls: [...] });
|
|
85
|
+
|
|
86
|
+
// Configure
|
|
87
|
+
await client.queue('myqueue', {}, { namespace, task });
|
|
88
|
+
|
|
89
|
+
// Push
|
|
90
|
+
await client.push('myqueue/partition', payload);
|
|
91
|
+
|
|
92
|
+
// Take (async iterator)
|
|
93
|
+
for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
|
|
94
|
+
// Process message
|
|
95
|
+
await client.ack(msg, true, { group: 'mygroup' });
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Key Differences
|
|
100
|
+
|
|
101
|
+
1. **Address Format**: The new interface uses a unified address format:
|
|
102
|
+
- `"queue"` - Simple queue
|
|
103
|
+
- `"queue/partition"` - Specific partition
|
|
104
|
+
- `"queue@group"` - Consumer group
|
|
105
|
+
- `"queue/partition@group"` - Partition + group
|
|
106
|
+
- `"namespace:name"` - Namespace filter
|
|
107
|
+
- `"task:name"` - Task filter
|
|
108
|
+
- `"namespace:billing/task:process"` - Combined filters
|
|
109
|
+
|
|
110
|
+
2. **Async Iterator**: `pop()` → `take()` using async iteration
|
|
111
|
+
```javascript
|
|
112
|
+
for await (const msg of client.take(address, options)) {
|
|
113
|
+
// Handle message
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
3. **Simplified Methods**: 4 core methods instead of many:
|
|
118
|
+
- `client.queue(name, options, metadata)` - Configure
|
|
119
|
+
- `client.push(address, payload, options)` - Send
|
|
120
|
+
- `client.take(address, options)` - Receive (async iterator)
|
|
121
|
+
- `client.ack(message, status, context)` - Acknowledge
|
|
122
|
+
|
|
123
|
+
4. **Message Properties**: Can be in payload or options:
|
|
124
|
+
```javascript
|
|
125
|
+
// In options
|
|
126
|
+
await client.push('queue', { data }, { transactionId: 'txn-1' });
|
|
127
|
+
|
|
128
|
+
// In payload
|
|
129
|
+
await client.push('queue', { data, transactionId: 'txn-1' });
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Running Tests
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# Make sure Queen servers are running first
|
|
136
|
+
# And database is accessible
|
|
137
|
+
|
|
138
|
+
cd /Users/alice/Work/queen
|
|
139
|
+
|
|
140
|
+
# Run all tests
|
|
141
|
+
nvm use 22 && node src/test/test-new.js
|
|
142
|
+
|
|
143
|
+
# Run specific test category
|
|
144
|
+
node src/test/test-new.js core # Core features only
|
|
145
|
+
node src/test/test-new.js partition # Partition locking tests only
|
|
146
|
+
node src/test/test-new.js enterprise # Enterprise features only
|
|
147
|
+
node src/test/test-new.js bus # Bus mode tests only
|
|
148
|
+
node src/test/test-new.js edge # Edge cases only
|
|
149
|
+
node src/test/test-new.js advanced # Advanced patterns only
|
|
150
|
+
|
|
151
|
+
# Show help
|
|
152
|
+
node src/test/test-new.js help
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Available Test Categories
|
|
156
|
+
|
|
157
|
+
- **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
|
|
158
|
+
- **`partition`** (or `locking`) - Partition locking in queue and bus modes
|
|
159
|
+
- **`enterprise`** - Enterprise features (encryption, retention, eviction)
|
|
160
|
+
- **`bus`** - Bus mode features (consumer groups, mixed mode, subscription modes, isolation)
|
|
161
|
+
- **`edge`** - Edge cases (null payloads, large payloads, concurrency, SQL injection, XSS)
|
|
162
|
+
- **`advanced`** (or `pattern`) - Advanced patterns (pipeline, fan-out/fan-in, DLQ, circuit breaker, deduplication)
|
|
163
|
+
|
|
164
|
+
## Test Configuration
|
|
165
|
+
|
|
166
|
+
Tests use environment variables for configuration:
|
|
167
|
+
- `PG_HOST` - PostgreSQL host (default: localhost)
|
|
168
|
+
- `PG_PORT` - PostgreSQL port (default: 5432)
|
|
169
|
+
- `PG_DB` - Database name (default: postgres)
|
|
170
|
+
- `PG_USER` - Database user (default: postgres)
|
|
171
|
+
- `PG_PASSWORD` - Database password (default: postgres)
|
|
172
|
+
- `QUEEN_ENCRYPTION_KEY` - Optional encryption key for encryption tests
|
|
173
|
+
|
|
174
|
+
## Adding New Tests
|
|
175
|
+
|
|
176
|
+
1. Create a test function in the appropriate category file:
|
|
177
|
+
```javascript
|
|
178
|
+
export async function testMyFeature(client) {
|
|
179
|
+
startTest('My Feature Test', 'category');
|
|
180
|
+
try {
|
|
181
|
+
// Test code here
|
|
182
|
+
passTest('Feature works correctly');
|
|
183
|
+
} catch (error) {
|
|
184
|
+
failTest(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
2. Import and add to `test-new.js`:
|
|
190
|
+
```javascript
|
|
191
|
+
import { testMyFeature } from './category-tests.js';
|
|
192
|
+
|
|
193
|
+
// In runTests():
|
|
194
|
+
await runTest(() => testMyFeature(client));
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Notes
|
|
198
|
+
|
|
199
|
+
- All tests automatically clean up test data before and after running
|
|
200
|
+
- Tests use the `test-*`, `edge-*`, `pattern-*`, and `workflow-*` queue name prefixes
|
|
201
|
+
- The test suite supports both single-server and multi-server configurations
|
|
202
|
+
- Some enterprise tests (encryption, retention) are skipped if not configured
|
|
203
|
+
|