queen-mq 0.2.0 → 0.2.22
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/README.md +186 -763
- package/{src → client-js}/benchmark/consumer.js +6 -4
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +277 -7
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/{src → client-js}/test/README.md +30 -9
- package/{src → client-js}/test/core-tests.js +7 -2
- package/{src → client-js}/test/edge-case-tests.js +2 -1
- package/client-js/test/human.js +162 -0
- package/client-js/test/partition-transaction-tests.js +482 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +55 -0
- package/package.json +8 -17
- package/init-db.js +0 -20
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1634
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1633
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/advanced-client-tests.js +0 -0
- /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
- /package/{src → client-js}/test/bus-mode-tests.js +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Connection Pool Manager for High Concurrency
|
|
3
|
-
*
|
|
4
|
-
* This module provides optimized connection pooling for high-throughput scenarios
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import pg from 'pg';
|
|
8
|
-
import appConfig from '../config.js';
|
|
9
|
-
|
|
10
|
-
const { Pool } = pg;
|
|
11
|
-
|
|
12
|
-
export class PoolManager {
|
|
13
|
-
constructor(config) {
|
|
14
|
-
this.config = {
|
|
15
|
-
user: appConfig.DATABASE.USER,
|
|
16
|
-
host: appConfig.DATABASE.HOST,
|
|
17
|
-
database: appConfig.DATABASE.DATABASE,
|
|
18
|
-
password: appConfig.DATABASE.PASSWORD,
|
|
19
|
-
port: appConfig.DATABASE.PORT,
|
|
20
|
-
max: appConfig.DATABASE.POOL_SIZE,
|
|
21
|
-
idleTimeoutMillis: appConfig.DATABASE.IDLE_TIMEOUT,
|
|
22
|
-
connectionTimeoutMillis: appConfig.DATABASE.CONNECTION_TIMEOUT,
|
|
23
|
-
...config
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// Add SSL configuration if enabled
|
|
27
|
-
if (appConfig.DATABASE.USE_SSL) {
|
|
28
|
-
this.config.ssl = {
|
|
29
|
-
rejectUnauthorized: appConfig.DATABASE.SSL_REJECT_UNAUTHORIZED
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
this.pool = new Pool(this.config);
|
|
34
|
-
this.waitingQueue = [];
|
|
35
|
-
this.activeConnections = 0;
|
|
36
|
-
this.maxRetries = appConfig.DATABASE.MAX_RETRIES;
|
|
37
|
-
|
|
38
|
-
// Monitor pool events
|
|
39
|
-
this.pool.on('connect', () => {
|
|
40
|
-
this.activeConnections++;
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
this.pool.on('remove', () => {
|
|
44
|
-
this.activeConnections--;
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
this.pool.on('error', (err) => {
|
|
48
|
-
console.error('Unexpected pool error:', err);
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Execute a query with automatic retry and connection management
|
|
54
|
-
*/
|
|
55
|
-
async query(text, params, retries = this.maxRetries) {
|
|
56
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
57
|
-
try {
|
|
58
|
-
return await this.pool.query(text, params);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (error.message.includes('timeout exceeded when trying to connect')) {
|
|
61
|
-
// Connection timeout - retry with exponential backoff
|
|
62
|
-
if (attempt < retries - 1) {
|
|
63
|
-
const delay = Math.min(100 * Math.pow(2, attempt), 1000);
|
|
64
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Get a client with retry logic
|
|
75
|
-
*/
|
|
76
|
-
async getClient(retries = this.maxRetries) {
|
|
77
|
-
for (let attempt = 0; attempt < retries; attempt++) {
|
|
78
|
-
try {
|
|
79
|
-
const client = await this.pool.connect();
|
|
80
|
-
|
|
81
|
-
// Wrap release to ensure proper cleanup
|
|
82
|
-
const originalRelease = client.release.bind(client);
|
|
83
|
-
client.release = (err) => {
|
|
84
|
-
originalRelease(err);
|
|
85
|
-
this.processWaitingQueue();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return client;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
if (error.message.includes('timeout exceeded when trying to connect')) {
|
|
91
|
-
if (attempt < retries - 1) {
|
|
92
|
-
const delay = Math.min(100 * Math.pow(2, attempt), 1000);
|
|
93
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
throw error;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Execute a function with a client, ensuring proper cleanup
|
|
104
|
-
*/
|
|
105
|
-
async withClient(fn) {
|
|
106
|
-
const client = await this.getClient();
|
|
107
|
-
try {
|
|
108
|
-
return await fn(client);
|
|
109
|
-
} finally {
|
|
110
|
-
client.release();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Execute multiple queries in parallel with connection management
|
|
116
|
-
*/
|
|
117
|
-
async parallelQueries(queries) {
|
|
118
|
-
const promises = queries.map(({ text, params }) =>
|
|
119
|
-
this.query(text, params)
|
|
120
|
-
);
|
|
121
|
-
return Promise.all(promises);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Batch execute with optimal connection usage
|
|
126
|
-
*/
|
|
127
|
-
async batchExecute(items, batchSize, executeFn) {
|
|
128
|
-
const results = [];
|
|
129
|
-
const batches = [];
|
|
130
|
-
|
|
131
|
-
// Split items into batches
|
|
132
|
-
for (let i = 0; i < items.length; i += batchSize) {
|
|
133
|
-
batches.push(items.slice(i, i + batchSize));
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Process batches with controlled concurrency
|
|
137
|
-
const concurrency = Math.min(Math.floor(this.config.max / 2), 10);
|
|
138
|
-
|
|
139
|
-
for (let i = 0; i < batches.length; i += concurrency) {
|
|
140
|
-
const batchPromises = batches
|
|
141
|
-
.slice(i, i + concurrency)
|
|
142
|
-
.map(batch => this.withClient(client => executeFn(client, batch)));
|
|
143
|
-
|
|
144
|
-
const batchResults = await Promise.all(batchPromises);
|
|
145
|
-
results.push(...batchResults.flat());
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return results;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Process any waiting queue operations
|
|
153
|
-
*/
|
|
154
|
-
processWaitingQueue() {
|
|
155
|
-
if (this.waitingQueue.length > 0 && this.pool.idleCount > 0) {
|
|
156
|
-
const { resolve } = this.waitingQueue.shift();
|
|
157
|
-
this.getClient().then(resolve).catch(() => {
|
|
158
|
-
// Re-queue if failed
|
|
159
|
-
this.waitingQueue.push({ resolve });
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Get pool statistics
|
|
166
|
-
*/
|
|
167
|
-
getStats() {
|
|
168
|
-
return {
|
|
169
|
-
total: this.pool.totalCount,
|
|
170
|
-
idle: this.pool.idleCount,
|
|
171
|
-
waiting: this.pool.waitingCount,
|
|
172
|
-
active: this.activeConnections,
|
|
173
|
-
queueLength: this.waitingQueue.length,
|
|
174
|
-
config: {
|
|
175
|
-
max: this.config.max,
|
|
176
|
-
connectionTimeout: this.config.connectionTimeoutMillis
|
|
177
|
-
}
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Gracefully close the pool
|
|
183
|
-
*/
|
|
184
|
-
async close() {
|
|
185
|
-
await this.pool.end();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Create a singleton instance
|
|
190
|
-
let poolManager;
|
|
191
|
-
|
|
192
|
-
export const getPoolManager = (config) => {
|
|
193
|
-
if (!poolManager) {
|
|
194
|
-
poolManager = new PoolManager(config);
|
|
195
|
-
}
|
|
196
|
-
return poolManager;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
export default PoolManager;
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
-- ════════════════════════════════════════════════════════════════
|
|
2
|
-
-- Queen Message Queue Schema V3
|
|
3
|
-
-- Simplified: Merged cursor+lease, removed messages_status
|
|
4
|
-
-- ════════════════════════════════════════════════════════════════
|
|
5
|
-
|
|
6
|
-
CREATE SCHEMA IF NOT EXISTS queen;
|
|
7
|
-
|
|
8
|
-
-- ────────────────────────────────────────────────────────────────
|
|
9
|
-
-- 1. QUEUES - Top-level configuration
|
|
10
|
-
-- ────────────────────────────────────────────────────────────────
|
|
11
|
-
CREATE TABLE IF NOT EXISTS queen.queues (
|
|
12
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
13
|
-
name VARCHAR(255) UNIQUE NOT NULL,
|
|
14
|
-
namespace VARCHAR(255),
|
|
15
|
-
task VARCHAR(255),
|
|
16
|
-
priority INTEGER DEFAULT 0,
|
|
17
|
-
|
|
18
|
-
-- Processing configuration
|
|
19
|
-
lease_time INTEGER DEFAULT 300,
|
|
20
|
-
retry_limit INTEGER DEFAULT 3,
|
|
21
|
-
retry_delay INTEGER DEFAULT 1000,
|
|
22
|
-
max_size INTEGER DEFAULT 10000,
|
|
23
|
-
ttl INTEGER DEFAULT 3600,
|
|
24
|
-
dead_letter_queue BOOLEAN DEFAULT FALSE,
|
|
25
|
-
dlq_after_max_retries BOOLEAN DEFAULT FALSE,
|
|
26
|
-
delayed_processing INTEGER DEFAULT 0,
|
|
27
|
-
window_buffer INTEGER DEFAULT 0,
|
|
28
|
-
|
|
29
|
-
-- Enterprise features
|
|
30
|
-
retention_seconds INTEGER DEFAULT 0,
|
|
31
|
-
completed_retention_seconds INTEGER DEFAULT 0,
|
|
32
|
-
retention_enabled BOOLEAN DEFAULT FALSE,
|
|
33
|
-
encryption_enabled BOOLEAN DEFAULT FALSE,
|
|
34
|
-
max_wait_time_seconds INTEGER DEFAULT 0,
|
|
35
|
-
max_queue_size INTEGER DEFAULT 0,
|
|
36
|
-
|
|
37
|
-
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
-- Indexes for queues
|
|
41
|
-
CREATE INDEX IF NOT EXISTS idx_queues_name ON queen.queues(name);
|
|
42
|
-
CREATE INDEX IF NOT EXISTS idx_queues_priority ON queen.queues(priority DESC);
|
|
43
|
-
CREATE INDEX IF NOT EXISTS idx_queues_namespace ON queen.queues(namespace) WHERE namespace IS NOT NULL;
|
|
44
|
-
CREATE INDEX IF NOT EXISTS idx_queues_task ON queen.queues(task) WHERE task IS NOT NULL;
|
|
45
|
-
CREATE INDEX IF NOT EXISTS idx_queues_namespace_task ON queen.queues(namespace, task)
|
|
46
|
-
WHERE namespace IS NOT NULL AND task IS NOT NULL;
|
|
47
|
-
CREATE INDEX IF NOT EXISTS idx_queues_retention_enabled ON queen.queues(retention_enabled) WHERE retention_enabled = true;
|
|
48
|
-
|
|
49
|
-
-- ────────────────────────────────────────────────────────────────
|
|
50
|
-
-- 2. PARTITIONS - FIFO subdivisions within queues
|
|
51
|
-
-- ────────────────────────────────────────────────────────────────
|
|
52
|
-
CREATE TABLE IF NOT EXISTS queen.partitions (
|
|
53
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
54
|
-
queue_id UUID REFERENCES queen.queues(id) ON DELETE CASCADE,
|
|
55
|
-
name VARCHAR(255) NOT NULL DEFAULT 'Default',
|
|
56
|
-
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
57
|
-
last_activity TIMESTAMPTZ DEFAULT NOW(),
|
|
58
|
-
UNIQUE(queue_id, name)
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
CREATE INDEX IF NOT EXISTS idx_partitions_queue_name
|
|
62
|
-
ON queen.partitions(queue_id, name);
|
|
63
|
-
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_partitions_last_activity
|
|
65
|
-
ON queen.partitions(last_activity);
|
|
66
|
-
|
|
67
|
-
-- ────────────────────────────────────────────────────────────────
|
|
68
|
-
-- 3. MESSAGES - Immutable message data
|
|
69
|
-
-- ────────────────────────────────────────────────────────────────
|
|
70
|
-
CREATE TABLE IF NOT EXISTS queen.messages (
|
|
71
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
|
-
transaction_id VARCHAR(255) UNIQUE NOT NULL,
|
|
73
|
-
trace_id UUID DEFAULT gen_random_uuid(),
|
|
74
|
-
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
75
|
-
payload JSONB NOT NULL,
|
|
76
|
-
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
77
|
-
is_encrypted BOOLEAN DEFAULT FALSE
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
-- Indexes for messages (critical for cursor-based queries!)
|
|
81
|
-
CREATE INDEX IF NOT EXISTS idx_messages_partition_created_id
|
|
82
|
-
ON queen.messages(partition_id, created_at, id);
|
|
83
|
-
|
|
84
|
-
CREATE INDEX IF NOT EXISTS idx_messages_transaction_id
|
|
85
|
-
ON queen.messages(transaction_id);
|
|
86
|
-
|
|
87
|
-
CREATE INDEX IF NOT EXISTS idx_messages_trace_id
|
|
88
|
-
ON queen.messages(trace_id);
|
|
89
|
-
|
|
90
|
-
CREATE INDEX IF NOT EXISTS idx_messages_created_at
|
|
91
|
-
ON queen.messages(created_at);
|
|
92
|
-
|
|
93
|
-
-- ────────────────────────────────────────────────────────────────
|
|
94
|
-
-- 4. PARTITION_CONSUMERS - Unified cursor + lease tracking
|
|
95
|
-
-- ────────────────────────────────────────────────────────────────
|
|
96
|
-
CREATE TABLE IF NOT EXISTS queen.partition_consumers (
|
|
97
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
98
|
-
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
99
|
-
consumer_group VARCHAR(255) DEFAULT '__QUEUE_MODE__',
|
|
100
|
-
|
|
101
|
-
-- ═══════════════════════════════════════════════════════════
|
|
102
|
-
-- CURSOR STATE (Persistent - consumption progress)
|
|
103
|
-
-- ═══════════════════════════════════════════════════════════
|
|
104
|
-
last_consumed_id UUID DEFAULT '00000000-0000-0000-0000-000000000000',
|
|
105
|
-
last_consumed_created_at TIMESTAMPTZ,
|
|
106
|
-
total_messages_consumed BIGINT DEFAULT 0,
|
|
107
|
-
total_batches_consumed BIGINT DEFAULT 0,
|
|
108
|
-
last_consumed_at TIMESTAMPTZ,
|
|
109
|
-
|
|
110
|
-
-- ═══════════════════════════════════════════════════════════
|
|
111
|
-
-- LEASE STATE (Ephemeral - active processing lock)
|
|
112
|
-
-- ═══════════════════════════════════════════════════════════
|
|
113
|
-
lease_expires_at TIMESTAMPTZ,
|
|
114
|
-
lease_acquired_at TIMESTAMPTZ,
|
|
115
|
-
message_batch JSONB,
|
|
116
|
-
batch_size INTEGER DEFAULT 0,
|
|
117
|
-
acked_count INTEGER DEFAULT 0,
|
|
118
|
-
worker_id VARCHAR(255),
|
|
119
|
-
|
|
120
|
-
-- ═══════════════════════════════════════════════════════════
|
|
121
|
-
-- STATISTICS (for dashboard - avoids expensive COUNT queries)
|
|
122
|
-
-- ═══════════════════════════════════════════════════════════
|
|
123
|
-
pending_estimate BIGINT DEFAULT 0,
|
|
124
|
-
last_stats_update TIMESTAMPTZ,
|
|
125
|
-
|
|
126
|
-
-- ═══════════════════════════════════════════════════════════
|
|
127
|
-
-- METADATA
|
|
128
|
-
-- ═══════════════════════════════════════════════════════════
|
|
129
|
-
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
130
|
-
|
|
131
|
-
UNIQUE(partition_id, consumer_group),
|
|
132
|
-
|
|
133
|
-
-- Constraint: cursor consistency
|
|
134
|
-
CHECK (
|
|
135
|
-
(last_consumed_id = '00000000-0000-0000-0000-000000000000'
|
|
136
|
-
AND last_consumed_created_at IS NULL)
|
|
137
|
-
OR
|
|
138
|
-
(last_consumed_id != '00000000-0000-0000-0000-000000000000'
|
|
139
|
-
AND last_consumed_created_at IS NOT NULL)
|
|
140
|
-
)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
-- Indexes for partition_consumers (critical for performance!)
|
|
144
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_lookup
|
|
145
|
-
ON queen.partition_consumers(partition_id, consumer_group);
|
|
146
|
-
|
|
147
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_active_leases
|
|
148
|
-
ON queen.partition_consumers(partition_id, consumer_group, lease_expires_at)
|
|
149
|
-
WHERE lease_expires_at IS NOT NULL;
|
|
150
|
-
|
|
151
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_expired_leases
|
|
152
|
-
ON queen.partition_consumers(lease_expires_at)
|
|
153
|
-
WHERE lease_expires_at IS NOT NULL;
|
|
154
|
-
|
|
155
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_progress
|
|
156
|
-
ON queen.partition_consumers(last_consumed_at DESC);
|
|
157
|
-
|
|
158
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_idle
|
|
159
|
-
ON queen.partition_consumers(partition_id, consumer_group)
|
|
160
|
-
WHERE lease_expires_at IS NULL;
|
|
161
|
-
|
|
162
|
-
CREATE INDEX IF NOT EXISTS idx_partition_consumers_consumer_group
|
|
163
|
-
ON queen.partition_consumers(consumer_group);
|
|
164
|
-
|
|
165
|
-
-- ────────────────────────────────────────────────────────────────
|
|
166
|
-
-- 5. MESSAGES_CONSUMED - ACK tracking for metrics (time-series)
|
|
167
|
-
-- ────────────────────────────────────────────────────────────────
|
|
168
|
-
CREATE TABLE IF NOT EXISTS queen.messages_consumed (
|
|
169
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
170
|
-
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
171
|
-
consumer_group VARCHAR(255) NOT NULL,
|
|
172
|
-
|
|
173
|
-
-- Metrics for this ACK batch
|
|
174
|
-
messages_completed INTEGER DEFAULT 0,
|
|
175
|
-
messages_failed INTEGER DEFAULT 0,
|
|
176
|
-
|
|
177
|
-
-- Timestamp for time-series aggregation
|
|
178
|
-
acked_at TIMESTAMPTZ DEFAULT NOW()
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
-- Indexes for time-series queries
|
|
182
|
-
CREATE INDEX IF NOT EXISTS idx_messages_consumed_acked_at
|
|
183
|
-
ON queen.messages_consumed(acked_at DESC);
|
|
184
|
-
|
|
185
|
-
CREATE INDEX IF NOT EXISTS idx_messages_consumed_partition_acked
|
|
186
|
-
ON queen.messages_consumed(partition_id, acked_at DESC);
|
|
187
|
-
|
|
188
|
-
CREATE INDEX IF NOT EXISTS idx_messages_consumed_consumer_acked
|
|
189
|
-
ON queen.messages_consumed(consumer_group, acked_at DESC);
|
|
190
|
-
|
|
191
|
-
-- Composite index for filtering by queue (via partition join)
|
|
192
|
-
CREATE INDEX IF NOT EXISTS idx_messages_consumed_partition_id
|
|
193
|
-
ON queen.messages_consumed(partition_id);
|
|
194
|
-
|
|
195
|
-
-- ────────────────────────────────────────────────────────────────
|
|
196
|
-
-- 6. DEAD_LETTER_QUEUE - Failed messages
|
|
197
|
-
-- ────────────────────────────────────────────────────────────────
|
|
198
|
-
CREATE TABLE IF NOT EXISTS queen.dead_letter_queue (
|
|
199
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
200
|
-
message_id UUID REFERENCES queen.messages(id) ON DELETE CASCADE,
|
|
201
|
-
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
202
|
-
consumer_group VARCHAR(255),
|
|
203
|
-
error_message TEXT,
|
|
204
|
-
retry_count INTEGER DEFAULT 0,
|
|
205
|
-
original_created_at TIMESTAMPTZ,
|
|
206
|
-
failed_at TIMESTAMPTZ DEFAULT NOW()
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
CREATE INDEX IF NOT EXISTS idx_dlq_partition
|
|
210
|
-
ON queen.dead_letter_queue(partition_id);
|
|
211
|
-
|
|
212
|
-
CREATE INDEX IF NOT EXISTS idx_dlq_consumer_group
|
|
213
|
-
ON queen.dead_letter_queue(consumer_group);
|
|
214
|
-
|
|
215
|
-
CREATE INDEX IF NOT EXISTS idx_dlq_failed_at
|
|
216
|
-
ON queen.dead_letter_queue(failed_at DESC);
|
|
217
|
-
|
|
218
|
-
CREATE INDEX IF NOT EXISTS idx_dlq_message_consumer
|
|
219
|
-
ON queen.dead_letter_queue(message_id, consumer_group);
|
|
220
|
-
|
|
221
|
-
-- ────────────────────────────────────────────────────────────────
|
|
222
|
-
-- 7. RETENTION_HISTORY - Optional audit of cleanup operations
|
|
223
|
-
-- ────────────────────────────────────────────────────────────────
|
|
224
|
-
CREATE TABLE IF NOT EXISTS queen.retention_history (
|
|
225
|
-
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
226
|
-
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
227
|
-
messages_deleted INTEGER DEFAULT 0,
|
|
228
|
-
retention_type VARCHAR(50), -- 'retention', 'completed', 'evicted'
|
|
229
|
-
executed_at TIMESTAMPTZ DEFAULT NOW()
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
CREATE INDEX IF NOT EXISTS idx_retention_history_partition
|
|
233
|
-
ON queen.retention_history(partition_id);
|
|
234
|
-
|
|
235
|
-
CREATE INDEX IF NOT EXISTS idx_retention_history_executed
|
|
236
|
-
ON queen.retention_history(executed_at);
|
|
237
|
-
|
|
238
|
-
-- ────────────────────────────────────────────────────────────────
|
|
239
|
-
-- TRIGGERS
|
|
240
|
-
-- ────────────────────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
-- Update partition last_activity on message insert
|
|
243
|
-
CREATE OR REPLACE FUNCTION update_partition_last_activity()
|
|
244
|
-
RETURNS TRIGGER AS $$
|
|
245
|
-
BEGIN
|
|
246
|
-
UPDATE queen.partitions
|
|
247
|
-
SET last_activity = NOW()
|
|
248
|
-
WHERE id = NEW.partition_id;
|
|
249
|
-
RETURN NEW;
|
|
250
|
-
END;
|
|
251
|
-
$$ LANGUAGE plpgsql;
|
|
252
|
-
|
|
253
|
-
DROP TRIGGER IF EXISTS trigger_update_partition_activity ON queen.messages;
|
|
254
|
-
CREATE TRIGGER trigger_update_partition_activity
|
|
255
|
-
AFTER INSERT ON queen.messages
|
|
256
|
-
FOR EACH ROW
|
|
257
|
-
EXECUTE FUNCTION update_partition_last_activity();
|
|
258
|
-
|
|
259
|
-
-- Update pending_estimate on message insert
|
|
260
|
-
CREATE OR REPLACE FUNCTION update_pending_on_push()
|
|
261
|
-
RETURNS TRIGGER AS $$
|
|
262
|
-
BEGIN
|
|
263
|
-
-- Increment pending estimate for all consumers on this partition
|
|
264
|
-
UPDATE queen.partition_consumers
|
|
265
|
-
SET pending_estimate = pending_estimate + 1,
|
|
266
|
-
last_stats_update = NOW()
|
|
267
|
-
WHERE partition_id = NEW.partition_id;
|
|
268
|
-
|
|
269
|
-
-- If no consumers exist yet, that's OK - they'll be created on first POP
|
|
270
|
-
RETURN NEW;
|
|
271
|
-
END;
|
|
272
|
-
$$ LANGUAGE plpgsql;
|
|
273
|
-
|
|
274
|
-
DROP TRIGGER IF EXISTS trigger_update_pending_on_push ON queen.messages;
|
|
275
|
-
CREATE TRIGGER trigger_update_pending_on_push
|
|
276
|
-
AFTER INSERT ON queen.messages
|
|
277
|
-
FOR EACH ROW
|
|
278
|
-
EXECUTE FUNCTION update_pending_on_push();
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { EventEmitter } from 'events';
|
|
2
|
-
|
|
3
|
-
export const createEventManager = () => {
|
|
4
|
-
const emitter = new EventEmitter();
|
|
5
|
-
emitter.setMaxListeners(1000); // Support many concurrent long polls
|
|
6
|
-
|
|
7
|
-
const emit = (event, data) => {
|
|
8
|
-
emitter.emit(event, data);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
const on = (event, handler) => {
|
|
12
|
-
emitter.on(event, handler);
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const once = (event, handler) => {
|
|
16
|
-
emitter.once(event, handler);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const removeListener = (event, handler) => {
|
|
20
|
-
emitter.removeListener(event, handler);
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// Wait for a message with timeout
|
|
24
|
-
const waitForMessage = (queuePath, timeout = 30000) => {
|
|
25
|
-
return new Promise((resolve) => {
|
|
26
|
-
const timer = setTimeout(() => {
|
|
27
|
-
removeListener(`message:${queuePath}`, handler);
|
|
28
|
-
resolve(null);
|
|
29
|
-
}, timeout);
|
|
30
|
-
|
|
31
|
-
const handler = (data) => {
|
|
32
|
-
clearTimeout(timer);
|
|
33
|
-
resolve(data);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
once(`message:${queuePath}`, handler);
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
// Notify when message is available
|
|
41
|
-
const notifyMessageAvailable = (queuePath) => {
|
|
42
|
-
emit(`message:${queuePath}`, { available: true });
|
|
43
|
-
// Also emit for parent queue if this is a partition path
|
|
44
|
-
const parts = queuePath.split('/');
|
|
45
|
-
if (parts.length === 2) {
|
|
46
|
-
// This is queue/partition, also notify queue level
|
|
47
|
-
emit(`message:${parts[0]}`, { available: true });
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
emit,
|
|
53
|
-
on,
|
|
54
|
-
once,
|
|
55
|
-
removeListener,
|
|
56
|
-
waitForMessage,
|
|
57
|
-
notifyMessageAvailable
|
|
58
|
-
};
|
|
59
|
-
};
|