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,96 @@
|
|
|
1
|
+
import { EventTypes } from './systemEventManager.js';
|
|
2
|
+
|
|
3
|
+
// In-memory cache for resource existence to avoid repeated DB lookups
|
|
4
|
+
export const createResourceCache = () => {
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
// Make TTL configurable - set to 0 to disable caching in multi-server setups
|
|
7
|
+
const TTL = process.env.QUEEN_CACHE_TTL ? parseInt(process.env.QUEEN_CACHE_TTL) : 60000; // Default 1 minute
|
|
8
|
+
|
|
9
|
+
const getCacheKey = (queue, partition) => `${queue}:${partition || 'Default'}`;
|
|
10
|
+
|
|
11
|
+
const checkResource = (queue, partition) => {
|
|
12
|
+
// If TTL is 0, caching is disabled
|
|
13
|
+
if (TTL === 0) return null;
|
|
14
|
+
|
|
15
|
+
const key = getCacheKey(queue, partition);
|
|
16
|
+
const cached = cache.get(key);
|
|
17
|
+
|
|
18
|
+
if (cached && Date.now() - cached.timestamp < TTL) {
|
|
19
|
+
return cached.data;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return null; // Not in cache
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const cacheResource = (queue, partition, data) => {
|
|
26
|
+
// If TTL is 0, caching is disabled
|
|
27
|
+
if (TTL === 0) return;
|
|
28
|
+
|
|
29
|
+
const key = getCacheKey(queue, partition);
|
|
30
|
+
cache.set(key, { data, timestamp: Date.now() });
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const invalidate = (queue, partition) => {
|
|
34
|
+
if (queue && partition) {
|
|
35
|
+
cache.delete(getCacheKey(queue, partition));
|
|
36
|
+
} else if (queue) {
|
|
37
|
+
// Clear all partitions for this queue
|
|
38
|
+
for (const key of cache.keys()) {
|
|
39
|
+
if (key.startsWith(`${queue}:`)) {
|
|
40
|
+
cache.delete(key);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
cache.clear(); // Clear all if no specific resource
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const invalidateQueue = (queue) => {
|
|
49
|
+
// Clear all partitions for this queue when queue config changes
|
|
50
|
+
for (const key of cache.keys()) {
|
|
51
|
+
if (key.startsWith(`${queue}:`)) {
|
|
52
|
+
cache.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Cleanup old entries periodically (only if caching is enabled)
|
|
58
|
+
if (TTL > 0) {
|
|
59
|
+
setInterval(() => {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
for (const [key, value] of cache.entries()) {
|
|
62
|
+
if (now - value.timestamp > TTL) {
|
|
63
|
+
cache.delete(key);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, Math.min(TTL, 60000)); // Check at most once per minute
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Register event handlers for cache invalidation
|
|
70
|
+
const registerEventHandlers = (eventManager) => {
|
|
71
|
+
// Invalidate cache on queue changes
|
|
72
|
+
eventManager.on(EventTypes.QUEUE_UPDATED, (event) => {
|
|
73
|
+
invalidateQueue(event.entityId);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
eventManager.on(EventTypes.QUEUE_DELETED, (event) => {
|
|
77
|
+
invalidateQueue(event.entityId);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
eventManager.on(EventTypes.PARTITION_CREATED, (event) => {
|
|
81
|
+
invalidate(event.queueName, event.entityId);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
eventManager.on(EventTypes.PARTITION_DELETED, (event) => {
|
|
85
|
+
invalidate(event.queueName, event.entityId);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
checkResource,
|
|
91
|
+
cacheResource,
|
|
92
|
+
invalidate,
|
|
93
|
+
invalidateQueue,
|
|
94
|
+
registerEventHandlers
|
|
95
|
+
};
|
|
96
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { generateUUID } from '../utils/uuid.js';
|
|
2
|
+
|
|
3
|
+
export const SYSTEM_QUEUE = '__system_events__';
|
|
4
|
+
|
|
5
|
+
export const EventTypes = {
|
|
6
|
+
// Queue events
|
|
7
|
+
QUEUE_CREATED: 'queue.created',
|
|
8
|
+
QUEUE_UPDATED: 'queue.updated',
|
|
9
|
+
QUEUE_DELETED: 'queue.deleted',
|
|
10
|
+
|
|
11
|
+
// Partition events
|
|
12
|
+
PARTITION_CREATED: 'partition.created',
|
|
13
|
+
PARTITION_DELETED: 'partition.deleted',
|
|
14
|
+
|
|
15
|
+
// Future events
|
|
16
|
+
CONSUMER_GROUP_CREATED: 'consumer_group.created',
|
|
17
|
+
CONSUMER_GROUP_UPDATED: 'consumer_group.updated'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class SystemEventManager {
|
|
21
|
+
constructor(pool, serverInstanceId) {
|
|
22
|
+
this.pool = pool;
|
|
23
|
+
this.serverInstanceId = serverInstanceId;
|
|
24
|
+
this.handlers = new Map();
|
|
25
|
+
this.eventQueue = [];
|
|
26
|
+
this.batchTimer = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Register event handlers
|
|
30
|
+
on(eventType, handler) {
|
|
31
|
+
if (!this.handlers.has(eventType)) {
|
|
32
|
+
this.handlers.set(eventType, []);
|
|
33
|
+
}
|
|
34
|
+
this.handlers.get(eventType).push(handler);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Emit event locally and to system queue
|
|
38
|
+
async emit(eventType, data) {
|
|
39
|
+
const event = {
|
|
40
|
+
id: generateUUID(),
|
|
41
|
+
eventType,
|
|
42
|
+
...data,
|
|
43
|
+
timestamp: Date.now(),
|
|
44
|
+
sourceServer: this.serverInstanceId,
|
|
45
|
+
version: 1
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Handle locally first (immediate consistency for originating server)
|
|
49
|
+
await this.handleEventLocally(event);
|
|
50
|
+
|
|
51
|
+
// Queue for batch publishing
|
|
52
|
+
this.eventQueue.push(event);
|
|
53
|
+
this.scheduleBatch();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Process incoming event from system queue
|
|
57
|
+
async processSystemEvent(event) {
|
|
58
|
+
// Skip own events (already processed locally)
|
|
59
|
+
if (event.sourceServer === this.serverInstanceId) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await this.handleEventLocally(event);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async handleEventLocally(event) {
|
|
67
|
+
const handlers = this.handlers.get(event.eventType) || [];
|
|
68
|
+
for (const handler of handlers) {
|
|
69
|
+
try {
|
|
70
|
+
await handler(event);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(`Handler failed for ${event.eventType}:`, error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
scheduleBatch() {
|
|
78
|
+
if (this.batchTimer) return;
|
|
79
|
+
|
|
80
|
+
this.batchTimer = setTimeout(() => {
|
|
81
|
+
this.publishBatch();
|
|
82
|
+
}, 10); // 10ms batching
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async publishBatch() {
|
|
86
|
+
if (this.eventQueue.length === 0) return;
|
|
87
|
+
|
|
88
|
+
const events = [...this.eventQueue];
|
|
89
|
+
this.eventQueue = [];
|
|
90
|
+
this.batchTimer = null;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Direct database insert to avoid circular dependency
|
|
94
|
+
const client = await this.pool.connect();
|
|
95
|
+
try {
|
|
96
|
+
for (const event of events) {
|
|
97
|
+
await this.insertSystemEvent(client, event);
|
|
98
|
+
}
|
|
99
|
+
} finally {
|
|
100
|
+
client.release();
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('Failed to publish system events:', error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async insertSystemEvent(client, event) {
|
|
108
|
+
// Direct insert bypassing queue manager to avoid circular dependency
|
|
109
|
+
await client.query(`
|
|
110
|
+
INSERT INTO queen.messages (
|
|
111
|
+
id, transaction_id, partition_id, payload,
|
|
112
|
+
created_at, trace_id
|
|
113
|
+
)
|
|
114
|
+
SELECT
|
|
115
|
+
$1, $2, p.id, $3, NOW(), $4
|
|
116
|
+
FROM queen.partitions p
|
|
117
|
+
JOIN queen.queues q ON p.queue_id = q.id
|
|
118
|
+
WHERE q.name = $5 AND p.name = 'Default'
|
|
119
|
+
`, [
|
|
120
|
+
generateUUID(),
|
|
121
|
+
generateUUID(),
|
|
122
|
+
JSON.stringify(event),
|
|
123
|
+
event.id,
|
|
124
|
+
SYSTEM_QUEUE
|
|
125
|
+
]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const createAckRoute = (queueManager) => {
|
|
2
|
+
return async (body) => {
|
|
3
|
+
const { transactionId, status, error, consumerGroup } = body;
|
|
4
|
+
|
|
5
|
+
if (!transactionId) {
|
|
6
|
+
throw new Error('transactionId is required');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!status || !['completed', 'failed'].includes(status)) {
|
|
10
|
+
throw new Error('status must be "completed" or "failed"');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await queueManager.acknowledgeMessage(transactionId, status, error, consumerGroup);
|
|
14
|
+
|
|
15
|
+
if (!result) {
|
|
16
|
+
throw new Error('Message not found or not in processing state');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
transactionId: result.transaction_id,
|
|
21
|
+
status: result.status,
|
|
22
|
+
consumerGroup: consumerGroup || null,
|
|
23
|
+
acknowledgedAt: new Date().toISOString()
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
};
|