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
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Complete Test Suite for Queen Message Queue System
|
|
5
|
+
* Using the new minimalist Queen client interface
|
|
6
|
+
*
|
|
7
|
+
* Test Categories:
|
|
8
|
+
* 1. Core Features
|
|
9
|
+
* 2. Partition Locking
|
|
10
|
+
* 3. Enterprise Features (coming soon)
|
|
11
|
+
* 4. Bus Mode Features (coming soon)
|
|
12
|
+
* 5. Edge Cases (coming soon)
|
|
13
|
+
* 6. Advanced Scenarios & Patterns (coming soon)
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Queen } from '../client/client.js';
|
|
17
|
+
import {
|
|
18
|
+
TEST_CONFIG,
|
|
19
|
+
initDb,
|
|
20
|
+
closeDb,
|
|
21
|
+
cleanupTestData,
|
|
22
|
+
printResults,
|
|
23
|
+
log,
|
|
24
|
+
resetResults
|
|
25
|
+
} from './utils.js';
|
|
26
|
+
|
|
27
|
+
// Import test modules
|
|
28
|
+
import {
|
|
29
|
+
testQueueCreationPolicy,
|
|
30
|
+
testSingleMessagePush,
|
|
31
|
+
testBatchMessagePush,
|
|
32
|
+
testQueueConfiguration,
|
|
33
|
+
testTakeAndAcknowledgment,
|
|
34
|
+
testDelayedProcessing,
|
|
35
|
+
testPartitionFIFOOrdering
|
|
36
|
+
} from './core-tests.js';
|
|
37
|
+
|
|
38
|
+
import {
|
|
39
|
+
testPartitionLocking,
|
|
40
|
+
testBusPartitionLocking,
|
|
41
|
+
testSpecificPartitionLocking,
|
|
42
|
+
testNamespaceTaskFiltering,
|
|
43
|
+
testNamespaceTaskBusMode
|
|
44
|
+
} from './partition-locking-tests.js';
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
testMessageEncryption,
|
|
48
|
+
testRetentionPendingMessages,
|
|
49
|
+
testRetentionCompletedMessages,
|
|
50
|
+
testPartitionRetention,
|
|
51
|
+
testConsumerEncryption,
|
|
52
|
+
testMessageEviction,
|
|
53
|
+
testCombinedEnterpriseFeatures,
|
|
54
|
+
testEnterpriseErrorHandling
|
|
55
|
+
} from './enterprise-tests.js';
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
testBusConsumerGroups,
|
|
59
|
+
testMixedMode,
|
|
60
|
+
testConsumerGroupSubscriptionModes,
|
|
61
|
+
testConsumerGroupIsolation
|
|
62
|
+
} from './bus-mode-tests.js';
|
|
63
|
+
|
|
64
|
+
import {
|
|
65
|
+
testEmptyAndNullPayloads,
|
|
66
|
+
testVeryLargePayloads,
|
|
67
|
+
testConcurrentPushOperations,
|
|
68
|
+
testConcurrentTakeOperations,
|
|
69
|
+
testManyConsumerGroups,
|
|
70
|
+
testRetryLimitExhaustion,
|
|
71
|
+
testLeaseExpiration,
|
|
72
|
+
testSQLInjectionPrevention,
|
|
73
|
+
testXSSPrevention
|
|
74
|
+
} from './edge-case-tests.js';
|
|
75
|
+
|
|
76
|
+
import {
|
|
77
|
+
testMultiStagePipeline,
|
|
78
|
+
testFanOutFanIn,
|
|
79
|
+
testComplexPriorityScenarios,
|
|
80
|
+
testDynamicPriorityAdjustment,
|
|
81
|
+
testSagaPattern,
|
|
82
|
+
testRateLimiting,
|
|
83
|
+
testTimeBatchProcessing,
|
|
84
|
+
testEventSourcing,
|
|
85
|
+
testQueueMetrics,
|
|
86
|
+
testMultipleConsumersSinglePartitionOrdering,
|
|
87
|
+
testDeadLetterQueuePattern,
|
|
88
|
+
testCircuitBreaker,
|
|
89
|
+
testMessageDeduplication
|
|
90
|
+
} from './advanced-pattern-tests.js';
|
|
91
|
+
|
|
92
|
+
// Helper to run a test with error handling and delay
|
|
93
|
+
async function runTest(testFn) {
|
|
94
|
+
try {
|
|
95
|
+
await testFn();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
log(`Unexpected error in test: ${error.message}`, 'error');
|
|
98
|
+
}
|
|
99
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay between tests
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Parse command-line arguments
|
|
103
|
+
const args = process.argv.slice(2);
|
|
104
|
+
const testFilter = args[0]; // e.g., 'core', 'bus', 'edge', 'enterprise', 'partition', 'advanced'
|
|
105
|
+
|
|
106
|
+
// Show help if requested
|
|
107
|
+
if (testFilter === 'help' || testFilter === '--help' || testFilter === '-h') {
|
|
108
|
+
console.log('Usage: node src/test/test-new.js [category]');
|
|
109
|
+
console.log('\nAvailable test categories:');
|
|
110
|
+
console.log(' core - Core features (queue creation, push, take, ack, etc.)');
|
|
111
|
+
console.log(' partition - Partition locking tests');
|
|
112
|
+
console.log(' enterprise - Enterprise features (encryption, retention, eviction)');
|
|
113
|
+
console.log(' bus - Bus mode features (consumer groups, mixed mode)');
|
|
114
|
+
console.log(' edge - Edge cases (null payloads, large payloads, SQL injection, etc.)');
|
|
115
|
+
console.log(' advanced - Advanced patterns (pipeline, fan-out, DLQ, circuit breaker)');
|
|
116
|
+
console.log('\nExamples:');
|
|
117
|
+
console.log(' node src/test/test-new.js # Run all tests');
|
|
118
|
+
console.log(' node src/test/test-new.js core # Run only core tests');
|
|
119
|
+
console.log(' node src/test/test-new.js bus # Run only bus mode tests');
|
|
120
|
+
console.log(' node src/test/test-new.js edge # Run only edge case tests');
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function runTests() {
|
|
125
|
+
console.log('🚀 Starting Queen Message Queue Test Suite');
|
|
126
|
+
console.log(' Using the new minimalist Queen client interface\n');
|
|
127
|
+
if (testFilter) {
|
|
128
|
+
console.log(` 🎯 Running only: ${testFilter} tests\n`);
|
|
129
|
+
}
|
|
130
|
+
console.log('='.repeat(80));
|
|
131
|
+
|
|
132
|
+
let client;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
// Initialize database connection
|
|
136
|
+
await initDb();
|
|
137
|
+
log('Database connection established');
|
|
138
|
+
|
|
139
|
+
// Initialize client
|
|
140
|
+
client = new Queen({
|
|
141
|
+
baseUrls: TEST_CONFIG.baseUrls,
|
|
142
|
+
timeout: 30000,
|
|
143
|
+
retryAttempts: 3
|
|
144
|
+
});
|
|
145
|
+
log('Queen client initialized');
|
|
146
|
+
|
|
147
|
+
// Check for encryption key
|
|
148
|
+
if (process.env.QUEEN_ENCRYPTION_KEY) {
|
|
149
|
+
log('Encryption key detected - encryption tests will run', 'info');
|
|
150
|
+
} else {
|
|
151
|
+
log('No encryption key found - encryption tests will be skipped', 'warning');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Clean up any existing test data
|
|
155
|
+
await cleanupTestData();
|
|
156
|
+
|
|
157
|
+
// ============================================
|
|
158
|
+
// CORE FEATURE TESTS
|
|
159
|
+
// ============================================
|
|
160
|
+
if (!testFilter || testFilter === 'core') {
|
|
161
|
+
console.log('\n📦 CORE FEATURES');
|
|
162
|
+
console.log('-'.repeat(40));
|
|
163
|
+
|
|
164
|
+
await runTest(() => testQueueCreationPolicy(client));
|
|
165
|
+
await runTest(() => testSingleMessagePush(client));
|
|
166
|
+
await runTest(() => testBatchMessagePush(client));
|
|
167
|
+
await runTest(() => testQueueConfiguration(client));
|
|
168
|
+
await runTest(() => testTakeAndAcknowledgment(client));
|
|
169
|
+
await runTest(() => testDelayedProcessing(client));
|
|
170
|
+
await runTest(() => testPartitionFIFOOrdering(client));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ============================================
|
|
174
|
+
// PARTITION LOCKING TESTS
|
|
175
|
+
// ============================================
|
|
176
|
+
if (!testFilter || testFilter === 'partition' || testFilter === 'locking') {
|
|
177
|
+
console.log('\n🔒 PARTITION LOCKING');
|
|
178
|
+
console.log('-'.repeat(40));
|
|
179
|
+
|
|
180
|
+
await runTest(() => testPartitionLocking(client));
|
|
181
|
+
await runTest(() => testBusPartitionLocking(client));
|
|
182
|
+
await runTest(() => testSpecificPartitionLocking(client));
|
|
183
|
+
await runTest(() => testNamespaceTaskFiltering(client));
|
|
184
|
+
await runTest(() => testNamespaceTaskBusMode(client));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ============================================
|
|
188
|
+
// ENTERPRISE FEATURE TESTS
|
|
189
|
+
// ============================================
|
|
190
|
+
if (!testFilter || testFilter === 'enterprise') {
|
|
191
|
+
console.log('\n🏢 ENTERPRISE FEATURES');
|
|
192
|
+
console.log('-'.repeat(40));
|
|
193
|
+
|
|
194
|
+
await runTest(() => testMessageEncryption(client));
|
|
195
|
+
await runTest(() => testRetentionPendingMessages(client));
|
|
196
|
+
await runTest(() => testRetentionCompletedMessages(client));
|
|
197
|
+
await runTest(() => testPartitionRetention(client));
|
|
198
|
+
await runTest(() => testConsumerEncryption(client));
|
|
199
|
+
await runTest(() => testMessageEviction(client));
|
|
200
|
+
await runTest(() => testCombinedEnterpriseFeatures(client));
|
|
201
|
+
await runTest(() => testEnterpriseErrorHandling(client));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================
|
|
205
|
+
// BUS MODE FEATURE TESTS
|
|
206
|
+
// ============================================
|
|
207
|
+
if (!testFilter || testFilter === 'bus') {
|
|
208
|
+
console.log('\n🚌 BUS MODE FEATURES');
|
|
209
|
+
console.log('-'.repeat(40));
|
|
210
|
+
|
|
211
|
+
await runTest(() => testBusConsumerGroups(client));
|
|
212
|
+
await runTest(() => testMixedMode(client));
|
|
213
|
+
await runTest(() => testConsumerGroupSubscriptionModes(client));
|
|
214
|
+
await runTest(() => testConsumerGroupIsolation(client));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================
|
|
218
|
+
// EDGE CASE TESTS
|
|
219
|
+
// ============================================
|
|
220
|
+
if (!testFilter || testFilter === 'edge') {
|
|
221
|
+
console.log('\n🔍 EDGE CASES');
|
|
222
|
+
console.log('-'.repeat(40));
|
|
223
|
+
|
|
224
|
+
await runTest(() => testEmptyAndNullPayloads(client));
|
|
225
|
+
await runTest(() => testVeryLargePayloads(client));
|
|
226
|
+
await runTest(() => testConcurrentPushOperations(client));
|
|
227
|
+
await runTest(() => testConcurrentTakeOperations(client));
|
|
228
|
+
await runTest(() => testManyConsumerGroups(client));
|
|
229
|
+
await runTest(() => testRetryLimitExhaustion(client));
|
|
230
|
+
await runTest(() => testLeaseExpiration(client));
|
|
231
|
+
await runTest(() => testSQLInjectionPrevention(client));
|
|
232
|
+
await runTest(() => testXSSPrevention(client));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================
|
|
236
|
+
// ADVANCED SCENARIO TESTS
|
|
237
|
+
// ============================================
|
|
238
|
+
if (!testFilter || testFilter === 'advanced' || testFilter === 'pattern') {
|
|
239
|
+
console.log('\n🎯 ADVANCED SCENARIOS');
|
|
240
|
+
console.log('-'.repeat(40));
|
|
241
|
+
|
|
242
|
+
await runTest(() => testMultiStagePipeline(client));
|
|
243
|
+
await runTest(() => testFanOutFanIn(client));
|
|
244
|
+
await runTest(() => testComplexPriorityScenarios(client));
|
|
245
|
+
await runTest(() => testDynamicPriorityAdjustment(client));
|
|
246
|
+
await runTest(() => testDeadLetterQueuePattern(client));
|
|
247
|
+
await runTest(() => testSagaPattern(client));
|
|
248
|
+
await runTest(() => testRateLimiting(client));
|
|
249
|
+
await runTest(() => testMessageDeduplication(client));
|
|
250
|
+
await runTest(() => testTimeBatchProcessing(client));
|
|
251
|
+
await runTest(() => testEventSourcing(client));
|
|
252
|
+
await runTest(() => testQueueMetrics(client));
|
|
253
|
+
await runTest(() => testCircuitBreaker(client));
|
|
254
|
+
await runTest(() => testMultipleConsumersSinglePartitionOrdering(client));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
} catch (error) {
|
|
258
|
+
log(`Test suite setup failed: ${error.message}`, 'error');
|
|
259
|
+
console.error(error);
|
|
260
|
+
} finally {
|
|
261
|
+
// Cleanup
|
|
262
|
+
if (client) {
|
|
263
|
+
await client.close();
|
|
264
|
+
}
|
|
265
|
+
await cleanupTestData();
|
|
266
|
+
await closeDb();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Print results
|
|
270
|
+
printResults();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Run the test suite
|
|
274
|
+
runTests().catch(error => {
|
|
275
|
+
console.error('💥 Test suite crashed:', error);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
|
package/src/test/test.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 5. Advanced Scenarios & Patterns
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import { createQueenClient } from '../client/queenClient.js';
|
|
16
16
|
import pg from 'pg';
|
|
17
17
|
import crypto from 'crypto';
|
|
18
18
|
|
|
@@ -124,7 +124,10 @@ const getMessageCount = async (queueName, partitionName = null) => {
|
|
|
124
124
|
// Helper function to ensure queue exists
|
|
125
125
|
const ensureQueue = async (queueName, options = {}) => {
|
|
126
126
|
try {
|
|
127
|
-
await client.
|
|
127
|
+
await client.configure({
|
|
128
|
+
queue: queueName,
|
|
129
|
+
options: options
|
|
130
|
+
});
|
|
128
131
|
} catch (error) {
|
|
129
132
|
// Queue might already exist, that's okay
|
|
130
133
|
if (!error.message.includes('already exists')) {
|
|
@@ -4301,7 +4304,7 @@ async function runAllTests() {
|
|
|
4301
4304
|
|
|
4302
4305
|
try {
|
|
4303
4306
|
// Initialize client and database connection
|
|
4304
|
-
client =
|
|
4307
|
+
client = createQueenClient({ baseUrls: TEST_CONFIG.baseUrls });
|
|
4305
4308
|
dbPool = new pg.Pool(TEST_CONFIG.dbConfig);
|
|
4306
4309
|
|
|
4307
4310
|
// Test database connection
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test utilities for Queen test suite
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import pg from 'pg';
|
|
6
|
+
|
|
7
|
+
// Test configuration
|
|
8
|
+
export const TEST_CONFIG = {
|
|
9
|
+
baseUrls: ['http://localhost:6632'],
|
|
10
|
+
dbConfig: {
|
|
11
|
+
host: process.env.PG_HOST || 'localhost',
|
|
12
|
+
port: process.env.PG_PORT || 5432,
|
|
13
|
+
database: process.env.PG_DB || 'postgres',
|
|
14
|
+
user: process.env.PG_USER || 'postgres',
|
|
15
|
+
password: process.env.PG_PASSWORD || 'postgres'
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Global test state
|
|
20
|
+
export let dbPool;
|
|
21
|
+
export let testResults = [];
|
|
22
|
+
export let currentTest = '';
|
|
23
|
+
|
|
24
|
+
// Initialize database pool
|
|
25
|
+
export async function initDb() {
|
|
26
|
+
dbPool = new pg.Pool(TEST_CONFIG.dbConfig);
|
|
27
|
+
await dbPool.query('SELECT 1');
|
|
28
|
+
return dbPool;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Close database pool
|
|
32
|
+
export async function closeDb() {
|
|
33
|
+
if (dbPool) {
|
|
34
|
+
await dbPool.end();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Test logging utilities
|
|
39
|
+
export const log = (message, type = 'info') => {
|
|
40
|
+
const timestamp = new Date().toISOString().substring(11, 23);
|
|
41
|
+
const prefix = {
|
|
42
|
+
info: '📝',
|
|
43
|
+
success: '✅',
|
|
44
|
+
error: '❌',
|
|
45
|
+
warning: '⚠️',
|
|
46
|
+
test: '🧪',
|
|
47
|
+
enterprise: '🏢',
|
|
48
|
+
edge: '🔍',
|
|
49
|
+
pattern: '🎯',
|
|
50
|
+
workflow: '🔄',
|
|
51
|
+
priority: '⚡'
|
|
52
|
+
}[type] || '📝';
|
|
53
|
+
|
|
54
|
+
console.log(`[${timestamp}] ${prefix} ${message}`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const startTest = (testName, category = 'test') => {
|
|
58
|
+
currentTest = testName;
|
|
59
|
+
log(`Starting: ${testName}`, category);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const passTest = (message = '') => {
|
|
63
|
+
const result = { test: currentTest, status: 'PASS', message };
|
|
64
|
+
testResults.push(result);
|
|
65
|
+
log(`PASS: ${currentTest} ${message}`, 'success');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const failTest = (error) => {
|
|
69
|
+
const result = { test: currentTest, status: 'FAIL', error: error.message };
|
|
70
|
+
testResults.push(result);
|
|
71
|
+
log(`FAIL: ${currentTest} - ${error.message}`, 'error');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
75
|
+
|
|
76
|
+
// Database utilities
|
|
77
|
+
export const cleanupTestData = async () => {
|
|
78
|
+
try {
|
|
79
|
+
await dbPool.query(`
|
|
80
|
+
DELETE FROM queen.messages
|
|
81
|
+
WHERE partition_id IN (
|
|
82
|
+
SELECT p.id FROM queen.partitions p
|
|
83
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
84
|
+
WHERE q.name LIKE 'test-%' OR q.name LIKE 'edge-%' OR q.name LIKE 'pattern-%' OR q.name LIKE 'workflow-%'
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
await dbPool.query(`
|
|
89
|
+
DELETE FROM queen.partitions
|
|
90
|
+
WHERE queue_id IN (
|
|
91
|
+
SELECT id FROM queen.queues WHERE name LIKE 'test-%' OR name LIKE 'edge-%' OR name LIKE 'pattern-%' OR name LIKE 'workflow-%'
|
|
92
|
+
)
|
|
93
|
+
`);
|
|
94
|
+
|
|
95
|
+
await dbPool.query(`DELETE FROM queen.queues WHERE name LIKE 'test-%' OR name LIKE 'edge-%' OR name LIKE 'pattern-%' OR name LIKE 'workflow-%'`);
|
|
96
|
+
|
|
97
|
+
await dbPool.query(`
|
|
98
|
+
DELETE FROM queen.retention_history
|
|
99
|
+
WHERE partition_id NOT IN (SELECT id FROM queen.partitions)
|
|
100
|
+
`);
|
|
101
|
+
|
|
102
|
+
log('Test data cleaned up');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
log(`Cleanup error: ${error.message}`, 'warning');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const getMessageCount = async (queueName, partitionName = null) => {
|
|
109
|
+
const query = partitionName
|
|
110
|
+
? `SELECT COUNT(*) as count FROM queen.messages m
|
|
111
|
+
JOIN queen.partitions p ON m.partition_id = p.id
|
|
112
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
113
|
+
WHERE q.name = $1 AND p.name = $2`
|
|
114
|
+
: `SELECT COUNT(*) as count FROM queen.messages m
|
|
115
|
+
JOIN queen.partitions p ON m.partition_id = p.id
|
|
116
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
117
|
+
WHERE q.name = $1`;
|
|
118
|
+
|
|
119
|
+
const params = partitionName ? [queueName, partitionName] : [queueName];
|
|
120
|
+
const result = await dbPool.query(query, params);
|
|
121
|
+
return parseInt(result.rows[0].count);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Print test results summary
|
|
125
|
+
export function printResults() {
|
|
126
|
+
console.log('\n' + '='.repeat(80));
|
|
127
|
+
console.log('📊 TEST RESULTS SUMMARY');
|
|
128
|
+
console.log('='.repeat(80));
|
|
129
|
+
|
|
130
|
+
const passed = testResults.filter(r => r.status === 'PASS').length;
|
|
131
|
+
const failed = testResults.filter(r => r.status === 'FAIL').length;
|
|
132
|
+
const total = testResults.length;
|
|
133
|
+
|
|
134
|
+
console.log(`\n📈 Overall Results: ${passed}/${total} tests passed`);
|
|
135
|
+
|
|
136
|
+
if (failed > 0) {
|
|
137
|
+
console.log('\n❌ Failed Tests:');
|
|
138
|
+
testResults
|
|
139
|
+
.filter(r => r.status === 'FAIL')
|
|
140
|
+
.forEach(r => {
|
|
141
|
+
console.log(` • ${r.test}: ${r.error}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (passed > 0) {
|
|
146
|
+
console.log('\n✅ Passed Tests:');
|
|
147
|
+
testResults
|
|
148
|
+
.filter(r => r.status === 'PASS')
|
|
149
|
+
.forEach(r => {
|
|
150
|
+
console.log(` • ${r.test}${r.message ? ': ' + r.message : ''}`);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.log('\n' + '='.repeat(80));
|
|
155
|
+
|
|
156
|
+
if (failed === 0) {
|
|
157
|
+
console.log('🎉 ALL TESTS PASSED! Queen Message Queue System is working correctly.');
|
|
158
|
+
console.log(' All features are operational.');
|
|
159
|
+
} else {
|
|
160
|
+
console.log(`⚠️ ${failed} test(s) failed. Please review the failures above.`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reset test results (useful for running tests in batches)
|
|
166
|
+
export function resetResults() {
|
|
167
|
+
testResults = [];
|
|
168
|
+
}
|
|
169
|
+
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streaming utilities for uWebSockets.js with backpressure handling
|
|
3
|
+
*
|
|
4
|
+
* When sending large responses (>1MB), we need to handle backpressure to prevent:
|
|
5
|
+
* - Event loop blocking
|
|
6
|
+
* - Memory buildup
|
|
7
|
+
* - Slow client stalling the server
|
|
8
|
+
*
|
|
9
|
+
* uWebSockets.js provides efficient backpressure handling via:
|
|
10
|
+
* - res.write() - Returns false if buffer is full
|
|
11
|
+
* - res.onWritable() - Called when buffer has space again
|
|
12
|
+
* - res.cork() - Batches syscalls for efficiency
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { log } from './logger.js';
|
|
16
|
+
|
|
17
|
+
// Thresholds for streaming decisions
|
|
18
|
+
const SMALL_RESPONSE_THRESHOLD = 128 * 1024; // 128KB - send directly
|
|
19
|
+
const CHUNK_SIZE = 64 * 1024; // 64KB - stream in chunks
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Send JSON response with automatic backpressure handling
|
|
23
|
+
*
|
|
24
|
+
* For small responses (<128KB): Sends directly via res.end()
|
|
25
|
+
* For large responses (>=128KB): Streams with backpressure via res.write() + res.onWritable()
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} res - uWebSockets.js response object
|
|
28
|
+
* @param {Object} data - Data to serialize and send
|
|
29
|
+
* @param {Object} abortedRef - Reference object with 'aborted' flag
|
|
30
|
+
* @param {Object} options - Optional settings
|
|
31
|
+
* @returns {boolean} - True if sent successfully, false if aborted
|
|
32
|
+
*/
|
|
33
|
+
export const streamJSON = (res, data, abortedRef = null, options = {}) => {
|
|
34
|
+
// Check if already aborted
|
|
35
|
+
if (abortedRef && abortedRef.aborted) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const jsonStr = JSON.stringify(data);
|
|
40
|
+
const dataSize = jsonStr.length;
|
|
41
|
+
|
|
42
|
+
// Small response - send directly (fast path)
|
|
43
|
+
if (dataSize < SMALL_RESPONSE_THRESHOLD) {
|
|
44
|
+
try {
|
|
45
|
+
res.cork(() => {
|
|
46
|
+
res.end(jsonStr);
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
} catch (error) {
|
|
50
|
+
log(`Error sending small response: ${error.message}`);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Large response - stream with backpressure handling
|
|
56
|
+
// uWebSockets.js requires all writes to be synchronous within cork callbacks
|
|
57
|
+
log(`Streaming large response: ${(dataSize / 1024 / 1024).toFixed(2)}MB`);
|
|
58
|
+
|
|
59
|
+
let offset = 0;
|
|
60
|
+
|
|
61
|
+
// Start sending - write as much as possible in first cork
|
|
62
|
+
res.cork(() => {
|
|
63
|
+
// Write chunks until buffer is full or done
|
|
64
|
+
while (offset < dataSize) {
|
|
65
|
+
const chunkEnd = Math.min(offset + CHUNK_SIZE, dataSize);
|
|
66
|
+
const chunk = jsonStr.slice(offset, chunkEnd);
|
|
67
|
+
const isLastChunk = chunkEnd >= dataSize;
|
|
68
|
+
|
|
69
|
+
if (isLastChunk) {
|
|
70
|
+
// Last chunk - end the response
|
|
71
|
+
res.end(chunk);
|
|
72
|
+
log(`Streamed response complete: ${(dataSize / 1024).toFixed(2)}KB sent`);
|
|
73
|
+
return; // All done!
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ok = res.write(chunk);
|
|
77
|
+
offset = chunkEnd;
|
|
78
|
+
|
|
79
|
+
if (!ok) {
|
|
80
|
+
// Buffer full - stop and wait for drain
|
|
81
|
+
log(`Backpressure detected at offset ${offset}/${dataSize} (${((offset/dataSize)*100).toFixed(1)}%)`);
|
|
82
|
+
break; // Exit while loop, set up onWritable below
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Successfully wrote, continue to next chunk
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// If we exited due to backpressure (offset < dataSize), set up handler
|
|
89
|
+
if (offset < dataSize) {
|
|
90
|
+
// Set up backpressure handler for remaining chunks
|
|
91
|
+
// onWritable is called when the socket buffer has space again
|
|
92
|
+
res.onWritable((writeOffset) => {
|
|
93
|
+
if (abortedRef && abortedRef.aborted) {
|
|
94
|
+
return true; // true = done, stop calling
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// CRITICAL: All writes inside onWritable must also be corked!
|
|
98
|
+
let isDone = true; // Will be set to false if we need to continue
|
|
99
|
+
|
|
100
|
+
res.cork(() => {
|
|
101
|
+
// Write as many chunks as possible while buffer has space
|
|
102
|
+
while (offset < dataSize) {
|
|
103
|
+
const chunkEnd = Math.min(offset + CHUNK_SIZE, dataSize);
|
|
104
|
+
const chunk = jsonStr.slice(offset, chunkEnd);
|
|
105
|
+
const isLastChunk = chunkEnd >= dataSize;
|
|
106
|
+
|
|
107
|
+
if (isLastChunk) {
|
|
108
|
+
// Last chunk - end the response
|
|
109
|
+
res.end(chunk);
|
|
110
|
+
log(`Streamed response complete: ${(dataSize / 1024).toFixed(2)}KB sent`);
|
|
111
|
+
isDone = true; // true = done, stop calling onWritable
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const ok = res.write(chunk);
|
|
116
|
+
offset = chunkEnd;
|
|
117
|
+
|
|
118
|
+
if (!ok) {
|
|
119
|
+
// Buffer full again - stop and wait for next onWritable call
|
|
120
|
+
log(`Backpressure detected at offset ${offset}/${dataSize} (${((offset/dataSize)*100).toFixed(1)}%)`);
|
|
121
|
+
isDone = false; // false = not done, call me again when writable
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Successfully wrote, try next chunk in this iteration
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// If we get here, all chunks sent
|
|
129
|
+
isDone = true; // Done
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return isDone;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return true;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Send a simple JSON response (for small data)
|
|
142
|
+
* Always uses res.cork() for efficiency
|
|
143
|
+
*/
|
|
144
|
+
export const sendJSON = (res, data, statusCode = 200) => {
|
|
145
|
+
res.cork(() => {
|
|
146
|
+
res.writeStatus(statusCode.toString());
|
|
147
|
+
res.end(JSON.stringify(data));
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Send error response with proper status code
|
|
153
|
+
* Note: CORS headers should be set by caller before calling this
|
|
154
|
+
*/
|
|
155
|
+
export const sendError = (res, error, statusCode = 500) => {
|
|
156
|
+
res.cork(() => {
|
|
157
|
+
res.writeStatus(statusCode.toString());
|
|
158
|
+
res.end(JSON.stringify({
|
|
159
|
+
error: error.message || error,
|
|
160
|
+
code: error.code || 'INTERNAL_ERROR'
|
|
161
|
+
}));
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Stream large array in chunks (alternative approach for very large arrays)
|
|
167
|
+
* Serializes chunks separately to avoid large JSON.stringify() blocking
|
|
168
|
+
*/
|
|
169
|
+
export const streamJSONArray = (res, items, abortedRef = null) => {
|
|
170
|
+
if (abortedRef && abortedRef.aborted) return false;
|
|
171
|
+
|
|
172
|
+
const ITEMS_PER_CHUNK = 1000;
|
|
173
|
+
let sent = 0;
|
|
174
|
+
|
|
175
|
+
const sendNextChunk = () => {
|
|
176
|
+
if (abortedRef && abortedRef.aborted) return true;
|
|
177
|
+
|
|
178
|
+
const chunk = items.slice(sent, sent + ITEMS_PER_CHUNK);
|
|
179
|
+
if (chunk.length === 0) {
|
|
180
|
+
// All sent
|
|
181
|
+
res.end(']');
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const isFirst = sent === 0;
|
|
186
|
+
const isLast = sent + chunk.length >= items.length;
|
|
187
|
+
|
|
188
|
+
// Build chunk JSON
|
|
189
|
+
let chunkStr;
|
|
190
|
+
if (isFirst) {
|
|
191
|
+
chunkStr = '[' + chunk.map(item => JSON.stringify(item)).join(',');
|
|
192
|
+
} else {
|
|
193
|
+
chunkStr = ',' + chunk.map(item => JSON.stringify(item)).join(',');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ok = isLast
|
|
197
|
+
? res.end(chunkStr + ']')
|
|
198
|
+
: res.write(chunkStr);
|
|
199
|
+
|
|
200
|
+
sent += chunk.length;
|
|
201
|
+
|
|
202
|
+
if (!ok) {
|
|
203
|
+
// Backpressure
|
|
204
|
+
res.onWritable(() => sendNextChunk());
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (!isLast) {
|
|
209
|
+
setImmediate(sendNextChunk);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return true;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
res.cork(() => {
|
|
216
|
+
const ok = sendNextChunk();
|
|
217
|
+
if (!ok) {
|
|
218
|
+
res.onWritable(sendNextChunk);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default {
|
|
226
|
+
streamJSON,
|
|
227
|
+
sendJSON,
|
|
228
|
+
sendError,
|
|
229
|
+
streamJSONArray
|
|
230
|
+
};
|
|
231
|
+
|