queen-mq 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/API.md +1116 -0
  2. package/CACHE.md +519 -0
  3. package/DASHBOARD-V3.md +478 -0
  4. package/DASHBOARD.md +382 -0
  5. package/MOD_QUEUE.md +453 -0
  6. package/PARTITION_LOCKING_DESIGN.md +989 -0
  7. package/PLAN.md +707 -0
  8. package/QUERY_ANALSYS.md +72 -0
  9. package/QUEUE_BUS.md +334 -0
  10. package/README.md +1495 -0
  11. package/V2-PLAN.md +236 -0
  12. package/assets/dashboard.png +0 -0
  13. package/dashboard/.vscode/extensions.json +3 -0
  14. package/dashboard/README.md +5 -0
  15. package/dashboard/index.html +14 -0
  16. package/dashboard/package-lock.json +1458 -0
  17. package/dashboard/package.json +25 -0
  18. package/dashboard/public/vite.svg +1 -0
  19. package/dashboard/src/App.vue +29 -0
  20. package/dashboard/src/assets/styles/main.css +908 -0
  21. package/dashboard/src/assets/vue.svg +1 -0
  22. package/dashboard/src/components/cards/MetricCard.vue +298 -0
  23. package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
  24. package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
  25. package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
  26. package/dashboard/src/components/common/ActivityFeed.vue +251 -0
  27. package/dashboard/src/components/layout/AppHeader.vue +208 -0
  28. package/dashboard/src/components/layout/AppLayout.vue +88 -0
  29. package/dashboard/src/components/layout/AppSidebar.vue +261 -0
  30. package/dashboard/src/main.js +44 -0
  31. package/dashboard/src/router.js +54 -0
  32. package/dashboard/src/services/api.js +187 -0
  33. package/dashboard/src/services/websocket.js +167 -0
  34. package/dashboard/src/utils/constants.js +56 -0
  35. package/dashboard/src/utils/helpers.js +118 -0
  36. package/dashboard/src/views/Analytics.vue +912 -0
  37. package/dashboard/src/views/Dashboard.vue +906 -0
  38. package/dashboard/src/views/Messages.vue +437 -0
  39. package/dashboard/src/views/QueueDetail.vue +501 -0
  40. package/dashboard/src/views/Queues.vue +333 -0
  41. package/dashboard/vite.config.js +30 -0
  42. package/debug-namespace.js +110 -0
  43. package/docs/long-polling.md +159 -0
  44. package/docs/multi-server-cache-solutions.md +185 -0
  45. package/docs/performance-tuning.md +222 -0
  46. package/examples/bus-mode.js +239 -0
  47. package/examples/continuous-consumer-optimized.js +215 -0
  48. package/examples/continuous-consumer.js +159 -0
  49. package/examples/continuous-producer.js +343 -0
  50. package/examples/mixed-mode.js +277 -0
  51. package/examples/multi-server-test.js +305 -0
  52. package/examples/single.js +64 -0
  53. package/examples/smartchat-dealyed.js +42 -0
  54. package/examples/smartchat.js +52 -0
  55. package/examples/test-cache-invalidation.js +119 -0
  56. package/examples/test-cache-multi-server.js +245 -0
  57. package/examples/test-minimal-client.js +112 -0
  58. package/examples/test-queue-creation-policy.js +137 -0
  59. package/init-db.js +20 -0
  60. package/package.json +36 -0
  61. package/src/client/client.js +291 -0
  62. package/src/client/index.js +6 -0
  63. package/src/client/queenClient.js +513 -0
  64. package/src/client/utils/http.js +172 -0
  65. package/src/client/utils/loadBalancer.js +152 -0
  66. package/src/client/utils/retry.js +35 -0
  67. package/src/config.js +215 -0
  68. package/src/database/connection.js +103 -0
  69. package/src/database/poolManager.js +192 -0
  70. package/src/database/schema-v2.sql +214 -0
  71. package/src/managers/eventManager.js +59 -0
  72. package/src/managers/queueManagerOptimized.js +1512 -0
  73. package/src/managers/resourceCache.js +96 -0
  74. package/src/managers/systemEventManager.js +127 -0
  75. package/src/routes/ack.js +26 -0
  76. package/src/routes/analytics.js +812 -0
  77. package/src/routes/configure.js +46 -0
  78. package/src/routes/messages.js +298 -0
  79. package/src/routes/pop.js +85 -0
  80. package/src/routes/push.js +28 -0
  81. package/src/routes/resources.js +296 -0
  82. package/src/server.js +1286 -0
  83. package/src/services/encryptionService.js +82 -0
  84. package/src/services/evictionService.js +131 -0
  85. package/src/services/retentionService.js +129 -0
  86. package/src/services/startupSync.js +35 -0
  87. package/src/test/test.js +4521 -0
  88. package/src/utils/logger.js +44 -0
  89. package/src/utils/uuid.js +5 -0
  90. package/src/websocket/wsServer.js +221 -0
@@ -0,0 +1,82 @@
1
+ import crypto from 'crypto';
2
+ import { log } from '../utils/logger.js';
3
+
4
+ const ALGORITHM = 'aes-256-gcm';
5
+ const KEY_ENV = 'QUEEN_ENCRYPTION_KEY';
6
+
7
+ // Get and validate the encryption key
8
+ const getKey = () => {
9
+ const keyHex = process.env[KEY_ENV];
10
+ if (!keyHex) return null;
11
+
12
+ try {
13
+ const key = Buffer.from(keyHex, 'hex');
14
+ if (key.length !== 32) {
15
+ log(`ERROR: ${KEY_ENV} must be 32 bytes (64 hex characters)`);
16
+ return null;
17
+ }
18
+ return key;
19
+ } catch (error) {
20
+ log(`ERROR: Invalid ${KEY_ENV} format:`, error.message);
21
+ return null;
22
+ }
23
+ };
24
+
25
+ // Encrypt a payload
26
+ export const encryptPayload = async (payload) => {
27
+ const key = getKey();
28
+ if (!key) throw new Error('Encryption key not configured');
29
+
30
+ const iv = crypto.randomBytes(16);
31
+ const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
32
+
33
+ const payloadStr = JSON.stringify(payload);
34
+ const encrypted = Buffer.concat([
35
+ cipher.update(payloadStr, 'utf8'),
36
+ cipher.final()
37
+ ]);
38
+
39
+ return {
40
+ encrypted: encrypted.toString('base64'),
41
+ iv: iv.toString('base64'),
42
+ authTag: cipher.getAuthTag().toString('base64')
43
+ };
44
+ };
45
+
46
+ // Decrypt a payload
47
+ export const decryptPayload = async (encryptedData) => {
48
+ const key = getKey();
49
+ if (!key) throw new Error('Encryption key not configured');
50
+
51
+ const decipher = crypto.createDecipheriv(
52
+ ALGORITHM,
53
+ key,
54
+ Buffer.from(encryptedData.iv, 'base64')
55
+ );
56
+
57
+ decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
58
+
59
+ const decrypted = Buffer.concat([
60
+ decipher.update(Buffer.from(encryptedData.encrypted, 'base64')),
61
+ decipher.final()
62
+ ]);
63
+
64
+ return JSON.parse(decrypted.toString('utf8'));
65
+ };
66
+
67
+ // Check if encryption is available
68
+ export const isEncryptionEnabled = () => {
69
+ const key = getKey();
70
+ return key !== null;
71
+ };
72
+
73
+ // Initialize and log status
74
+ export const initEncryption = () => {
75
+ const enabled = isEncryptionEnabled();
76
+ if (enabled) {
77
+ log('✅ Encryption service initialized');
78
+ } else {
79
+ log('⚠️ Encryption service disabled (QUEEN_ENCRYPTION_KEY not set)');
80
+ }
81
+ return enabled;
82
+ };
@@ -0,0 +1,131 @@
1
+ // Functional eviction service for stale messages
2
+ import { log, LogTypes } from '../utils/logger.js';
3
+ import config from '../config.js';
4
+
5
+ const EVICTION_INTERVAL = config.JOBS.EVICTION_INTERVAL;
6
+
7
+ // Evict messages that exceeded max wait time for a queue
8
+ const evictMessages = async (client, queueName, maxWaitTimeSeconds) => {
9
+ if (!maxWaitTimeSeconds || maxWaitTimeSeconds <= 0) return 0;
10
+
11
+ // For the new schema, we need to update or create status entries for evicted messages
12
+ const result = await client.query(`
13
+ WITH queue_partitions AS (
14
+ SELECT p.id
15
+ FROM queen.partitions p
16
+ JOIN queen.queues q ON p.queue_id = q.id
17
+ WHERE q.name = $1
18
+ ),
19
+ eligible_messages AS (
20
+ SELECT m.id
21
+ FROM queen.messages m
22
+ JOIN queue_partitions qp ON m.partition_id = qp.id
23
+ LEFT JOIN queen.messages_status ms ON m.id = ms.message_id AND ms.consumer_group IS NULL
24
+ WHERE m.created_at < NOW() - INTERVAL '1 second' * $2
25
+ AND (ms.status = 'pending' OR ms.status IS NULL)
26
+ )
27
+ INSERT INTO queen.messages_status (message_id, consumer_group, status, completed_at, error_message)
28
+ SELECT id, NULL, 'evicted', NOW(), 'Message exceeded maximum wait time'
29
+ FROM eligible_messages
30
+ ON CONFLICT (message_id, consumer_group)
31
+ DO UPDATE SET
32
+ status = 'evicted',
33
+ completed_at = NOW(),
34
+ error_message = 'Message exceeded maximum wait time'
35
+ RETURNING message_id
36
+ `, [queueName, maxWaitTimeSeconds]);
37
+
38
+ return result.rowCount || 0;
39
+ };
40
+
41
+ // Evict messages during pop operation (inline eviction)
42
+ export const evictOnPop = async (client, queueName) => {
43
+ const queueResult = await client.query(
44
+ 'SELECT max_wait_time_seconds FROM queen.queues WHERE name = $1',
45
+ [queueName]
46
+ );
47
+
48
+ if (!queueResult.rows[0]) return 0;
49
+
50
+ const maxWaitTimeSeconds = queueResult.rows[0].max_wait_time_seconds;
51
+ const evictedCount = await evictMessages(client, queueName, maxWaitTimeSeconds);
52
+
53
+ if (evictedCount > 0) {
54
+ log(`${LogTypes.EVICTION} | Queue: ${queueName} | Count: ${evictedCount} | MaxWaitTime: ${maxWaitTimeSeconds}s`);
55
+
56
+ // Log to retention history (reusing the table)
57
+ await client.query(`
58
+ INSERT INTO queen.retention_history (messages_deleted, retention_type)
59
+ VALUES ($1, 'evicted')
60
+ `, [evictedCount]);
61
+ }
62
+
63
+ return evictedCount;
64
+ };
65
+
66
+ // Background eviction for all queues
67
+ const performEviction = async (pool) => {
68
+ const client = await pool.connect();
69
+
70
+ try {
71
+ // Get all queues with eviction configured
72
+ const queuesResult = await client.query(`
73
+ SELECT name, max_wait_time_seconds
74
+ FROM queen.queues
75
+ WHERE max_wait_time_seconds > 0
76
+ `);
77
+
78
+ let totalEvicted = 0;
79
+ for (const queue of queuesResult.rows) {
80
+ const evicted = await evictMessages(
81
+ client,
82
+ queue.name,
83
+ queue.max_wait_time_seconds
84
+ );
85
+
86
+ if (evicted > 0) {
87
+ log(`${LogTypes.EVICTION} | Queue: ${queue.name} | Count: ${evicted} | MaxWaitTime: ${queue.max_wait_time_seconds}s`);
88
+ totalEvicted += evicted;
89
+ }
90
+ }
91
+
92
+ if (totalEvicted > 0) {
93
+ await client.query(`
94
+ INSERT INTO queen.retention_history (messages_deleted, retention_type)
95
+ VALUES ($1, 'evicted')
96
+ `, [totalEvicted]);
97
+ }
98
+
99
+ return totalEvicted;
100
+ } catch (error) {
101
+ log('Eviction error:', error);
102
+ return 0;
103
+ } finally {
104
+ client.release();
105
+ }
106
+ };
107
+
108
+ // Start the eviction job (optional background job)
109
+ export const startEvictionJob = (pool, eventManager) => {
110
+ const intervalId = setInterval(async () => {
111
+ try {
112
+ const evicted = await performEviction(pool);
113
+ if (evicted > 0 && eventManager) {
114
+ eventManager.emit('messages:evicted', { count: evicted });
115
+ }
116
+ } catch (error) {
117
+ log('Eviction job error:', error);
118
+ }
119
+ }, EVICTION_INTERVAL);
120
+
121
+ log(`⏰ Eviction job started (interval: ${EVICTION_INTERVAL}ms)`);
122
+
123
+ // Return cleanup function
124
+ return () => {
125
+ clearInterval(intervalId);
126
+ log('Eviction job stopped');
127
+ };
128
+ };
129
+
130
+ // Export for manual execution
131
+ export const runEviction = performEviction;
@@ -0,0 +1,129 @@
1
+ // Functional retention service for message cleanup
2
+ import { log, LogTypes } from '../utils/logger.js';
3
+ import config from '../config.js';
4
+
5
+ const RETENTION_INTERVAL = config.JOBS.RETENTION_INTERVAL;
6
+
7
+ // Apply retention policy to a single queue
8
+ const retentionQueue = async (client, queue) => {
9
+ const {
10
+ retention_seconds: retentionSeconds = 0,
11
+ completed_retention_seconds: completedRetentionSeconds = 0,
12
+ retention_enabled: retentionEnabled = false
13
+ } = queue;
14
+
15
+ if (!retentionEnabled) return 0;
16
+
17
+ let totalDeleted = 0;
18
+
19
+ // Delete old pending messages from all partitions in this queue
20
+ if (retentionSeconds > 0) {
21
+ const result = await client.query(`
22
+ DELETE FROM queen.messages
23
+ WHERE partition_id IN (
24
+ SELECT id FROM queen.partitions WHERE queue_id = $1
25
+ )
26
+ AND status = 'pending'
27
+ AND created_at < NOW() - INTERVAL '1 second' * $2
28
+ RETURNING id
29
+ `, [queue.id, retentionSeconds]);
30
+
31
+ totalDeleted += result.rowCount || 0;
32
+ }
33
+
34
+ // Delete completed/failed/evicted messages from all partitions in this queue
35
+ if (completedRetentionSeconds > 0) {
36
+ const result = await client.query(`
37
+ DELETE FROM queen.messages
38
+ WHERE partition_id IN (
39
+ SELECT id FROM queen.partitions WHERE queue_id = $1
40
+ )
41
+ AND status IN ('completed', 'failed', 'evicted')
42
+ AND COALESCE(completed_at, failed_at, created_at) < NOW() - INTERVAL '1 second' * $2
43
+ RETURNING id
44
+ `, [queue.id, completedRetentionSeconds]);
45
+
46
+ totalDeleted += result.rowCount || 0;
47
+ }
48
+
49
+ // Log retention if messages were deleted
50
+ if (totalDeleted > 0) {
51
+ // Note: We might need to update retention_history table to use queue_id instead
52
+ log(`${LogTypes.RETENTION} | Queue: ${queue.name} | Count: ${totalDeleted} | RetentionSeconds: ${retentionSeconds} | CompletedRetentionSeconds: ${completedRetentionSeconds}`);
53
+ }
54
+
55
+ return totalDeleted;
56
+ };
57
+
58
+ // Clean up empty partitions (no longer based on partition-level config)
59
+ const cleanupEmptyPartitions = async (client) => {
60
+ // For now, we keep Default partitions and only clean up truly empty non-default partitions
61
+ // that haven't had activity for a long time (e.g., 7 days)
62
+ const result = await client.query(`
63
+ DELETE FROM queen.partitions p
64
+ WHERE p.name != 'Default'
65
+ AND NOT EXISTS (
66
+ SELECT 1 FROM queen.messages m WHERE m.partition_id = p.id
67
+ )
68
+ AND p.last_activity < NOW() - INTERVAL '7 days'
69
+ RETURNING id, name
70
+ `);
71
+
72
+ if (result.rowCount > 0) {
73
+ log(`🗑️ Deleted ${result.rowCount} empty partitions`);
74
+ }
75
+
76
+ return result.rowCount;
77
+ };
78
+
79
+ // Main retention function
80
+ const performRetention = async (pool) => {
81
+ const client = await pool.connect();
82
+
83
+ try {
84
+ // Get queues with retention enabled
85
+ const queuesResult = await client.query(`
86
+ SELECT q.id, q.name, q.retention_enabled, q.retention_seconds,
87
+ q.completed_retention_seconds
88
+ FROM queen.queues q
89
+ WHERE q.retention_enabled = true
90
+ `);
91
+
92
+ let totalDeleted = 0;
93
+ for (const queue of queuesResult.rows) {
94
+ totalDeleted += await retentionQueue(client, queue);
95
+ }
96
+
97
+ // Cleanup empty partitions
98
+ await cleanupEmptyPartitions(client);
99
+
100
+ return totalDeleted;
101
+ } catch (error) {
102
+ log('Retention error:', error);
103
+ return 0;
104
+ } finally {
105
+ client.release();
106
+ }
107
+ };
108
+
109
+ // Start the retention job
110
+ export const startRetentionJob = (pool) => {
111
+ const intervalId = setInterval(async () => {
112
+ try {
113
+ await performRetention(pool);
114
+ } catch (error) {
115
+ log('Retention job error:', error);
116
+ }
117
+ }, RETENTION_INTERVAL);
118
+
119
+ log(`♻️ Retention job started (interval: ${RETENTION_INTERVAL}ms)`);
120
+
121
+ // Return cleanup function
122
+ return () => {
123
+ clearInterval(intervalId);
124
+ log('Retention job stopped');
125
+ };
126
+ };
127
+
128
+ // Export for manual execution
129
+ export const runRetention = performRetention;
@@ -0,0 +1,35 @@
1
+ import { SYSTEM_QUEUE } from '../managers/systemEventManager.js';
2
+
3
+ export async function syncSystemEvents(client, eventManager, consumerGroup) {
4
+ console.log('Synchronizing system events...');
5
+
6
+ let processed = 0;
7
+ let hasMore = true;
8
+
9
+ while (hasMore) {
10
+ // Pop and process system events in batches using the server's consumer group
11
+ // This ensures we only process events we haven't seen before
12
+ const result = await client.pop({
13
+ queue: SYSTEM_QUEUE,
14
+ consumerGroup: consumerGroup, // Use server's unique consumer group
15
+ batch: 100,
16
+ wait: false
17
+ });
18
+
19
+ if (!result.messages || result.messages.length === 0) {
20
+ hasMore = false;
21
+ break;
22
+ }
23
+
24
+ for (const message of result.messages) {
25
+ await eventManager.processSystemEvent(message.payload);
26
+ await client.ack(message.transactionId, 'completed', null, consumerGroup);
27
+ processed++;
28
+ }
29
+ }
30
+
31
+ if (processed > 0) {
32
+ console.log(`Synchronized ${processed} system events`);
33
+ }
34
+ return processed;
35
+ }