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.
- package/API.md +1116 -0
- package/CACHE.md +519 -0
- package/DASHBOARD-V3.md +478 -0
- package/DASHBOARD.md +382 -0
- package/MOD_QUEUE.md +453 -0
- package/PARTITION_LOCKING_DESIGN.md +989 -0
- package/PLAN.md +707 -0
- package/QUERY_ANALSYS.md +72 -0
- package/QUEUE_BUS.md +334 -0
- package/README.md +1495 -0
- package/V2-PLAN.md +236 -0
- package/assets/dashboard.png +0 -0
- package/dashboard/.vscode/extensions.json +3 -0
- package/dashboard/README.md +5 -0
- package/dashboard/index.html +14 -0
- package/dashboard/package-lock.json +1458 -0
- package/dashboard/package.json +25 -0
- package/dashboard/public/vite.svg +1 -0
- package/dashboard/src/App.vue +29 -0
- package/dashboard/src/assets/styles/main.css +908 -0
- package/dashboard/src/assets/vue.svg +1 -0
- package/dashboard/src/components/cards/MetricCard.vue +298 -0
- package/dashboard/src/components/charts/QueueDepthChart.vue +276 -0
- package/dashboard/src/components/charts/QueueLagChart.vue +436 -0
- package/dashboard/src/components/charts/ThroughputChart.vue +302 -0
- package/dashboard/src/components/common/ActivityFeed.vue +251 -0
- package/dashboard/src/components/layout/AppHeader.vue +208 -0
- package/dashboard/src/components/layout/AppLayout.vue +88 -0
- package/dashboard/src/components/layout/AppSidebar.vue +261 -0
- package/dashboard/src/main.js +44 -0
- package/dashboard/src/router.js +54 -0
- package/dashboard/src/services/api.js +187 -0
- package/dashboard/src/services/websocket.js +167 -0
- package/dashboard/src/utils/constants.js +56 -0
- package/dashboard/src/utils/helpers.js +118 -0
- package/dashboard/src/views/Analytics.vue +912 -0
- package/dashboard/src/views/Dashboard.vue +906 -0
- package/dashboard/src/views/Messages.vue +437 -0
- package/dashboard/src/views/QueueDetail.vue +501 -0
- package/dashboard/src/views/Queues.vue +333 -0
- package/dashboard/vite.config.js +30 -0
- package/debug-namespace.js +110 -0
- package/docs/long-polling.md +159 -0
- package/docs/multi-server-cache-solutions.md +185 -0
- package/docs/performance-tuning.md +222 -0
- package/examples/bus-mode.js +239 -0
- package/examples/continuous-consumer-optimized.js +215 -0
- package/examples/continuous-consumer.js +159 -0
- package/examples/continuous-producer.js +343 -0
- package/examples/mixed-mode.js +277 -0
- package/examples/multi-server-test.js +305 -0
- package/examples/single.js +64 -0
- package/examples/smartchat-dealyed.js +42 -0
- package/examples/smartchat.js +52 -0
- package/examples/test-cache-invalidation.js +119 -0
- package/examples/test-cache-multi-server.js +245 -0
- package/examples/test-minimal-client.js +112 -0
- package/examples/test-queue-creation-policy.js +137 -0
- package/init-db.js +20 -0
- package/package.json +36 -0
- package/src/client/client.js +291 -0
- package/src/client/index.js +6 -0
- package/src/client/queenClient.js +513 -0
- package/src/client/utils/http.js +172 -0
- package/src/client/utils/loadBalancer.js +152 -0
- package/src/client/utils/retry.js +35 -0
- package/src/config.js +215 -0
- package/src/database/connection.js +103 -0
- package/src/database/poolManager.js +192 -0
- package/src/database/schema-v2.sql +214 -0
- package/src/managers/eventManager.js +59 -0
- package/src/managers/queueManagerOptimized.js +1512 -0
- package/src/managers/resourceCache.js +96 -0
- package/src/managers/systemEventManager.js +127 -0
- package/src/routes/ack.js +26 -0
- package/src/routes/analytics.js +812 -0
- package/src/routes/configure.js +46 -0
- package/src/routes/messages.js +298 -0
- package/src/routes/pop.js +85 -0
- package/src/routes/push.js +28 -0
- package/src/routes/resources.js +296 -0
- package/src/server.js +1286 -0
- package/src/services/encryptionService.js +82 -0
- package/src/services/evictionService.js +131 -0
- package/src/services/retentionService.js +129 -0
- package/src/services/startupSync.js +35 -0
- package/src/test/test.js +4521 -0
- package/src/utils/logger.js +44 -0
- package/src/utils/uuid.js +5 -0
- 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
|
+
}
|