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,214 @@
|
|
|
1
|
+
-- Queen Message Queue Schema V2 with Bus/Consumer Group Support
|
|
2
|
+
-- Structure: Queues → Partitions → Messages → Status (per consumer group)
|
|
3
|
+
|
|
4
|
+
-- Create schema only if it doesn't exist (preserves existing data)
|
|
5
|
+
CREATE SCHEMA IF NOT EXISTS queen;
|
|
6
|
+
|
|
7
|
+
-- Queues table (top level, with all configuration options)
|
|
8
|
+
CREATE TABLE IF NOT EXISTS queen.queues (
|
|
9
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
+
name VARCHAR(255) UNIQUE NOT NULL,
|
|
11
|
+
namespace VARCHAR(255), -- optional grouping
|
|
12
|
+
task VARCHAR(255), -- optional grouping
|
|
13
|
+
priority INTEGER DEFAULT 0, -- Queue priority (higher = processed first)
|
|
14
|
+
-- Configuration options (all at queue level now)
|
|
15
|
+
lease_time INTEGER DEFAULT 300,
|
|
16
|
+
retry_limit INTEGER DEFAULT 3,
|
|
17
|
+
retry_delay INTEGER DEFAULT 1000,
|
|
18
|
+
max_size INTEGER DEFAULT 10000,
|
|
19
|
+
ttl INTEGER DEFAULT 3600,
|
|
20
|
+
dead_letter_queue BOOLEAN DEFAULT FALSE,
|
|
21
|
+
dlq_after_max_retries BOOLEAN DEFAULT FALSE,
|
|
22
|
+
delayed_processing INTEGER DEFAULT 0,
|
|
23
|
+
window_buffer INTEGER DEFAULT 0,
|
|
24
|
+
retention_seconds INTEGER DEFAULT 0,
|
|
25
|
+
completed_retention_seconds INTEGER DEFAULT 0,
|
|
26
|
+
retention_enabled BOOLEAN DEFAULT FALSE,
|
|
27
|
+
encryption_enabled BOOLEAN DEFAULT FALSE,
|
|
28
|
+
max_wait_time_seconds INTEGER DEFAULT 0,
|
|
29
|
+
max_queue_size INTEGER DEFAULT 0, -- 0 = unlimited queue size
|
|
30
|
+
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- Partitions table (subdivisions of queues, where FIFO happens)
|
|
34
|
+
CREATE TABLE IF NOT EXISTS queen.partitions (
|
|
35
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
36
|
+
queue_id UUID REFERENCES queen.queues(id) ON DELETE CASCADE,
|
|
37
|
+
name VARCHAR(255) NOT NULL DEFAULT 'Default',
|
|
38
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
39
|
+
last_activity TIMESTAMPTZ DEFAULT NOW(),
|
|
40
|
+
UNIQUE(queue_id, name)
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
-- Messages table (immutable message data only)
|
|
44
|
+
CREATE TABLE IF NOT EXISTS queen.messages (
|
|
45
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
46
|
+
transaction_id VARCHAR(255) UNIQUE NOT NULL, -- Changed from UUID to string for flexibility
|
|
47
|
+
trace_id UUID DEFAULT gen_random_uuid(), -- For tracking multi-step pipelines
|
|
48
|
+
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
49
|
+
payload JSONB NOT NULL,
|
|
50
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
51
|
+
is_encrypted BOOLEAN DEFAULT FALSE
|
|
52
|
+
-- Removed all status fields - moved to messages_status table
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
-- Messages status table (tracks consumption per consumer group)
|
|
56
|
+
CREATE TABLE IF NOT EXISTS queen.messages_status (
|
|
57
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
58
|
+
message_id UUID REFERENCES queen.messages(id) ON DELETE CASCADE,
|
|
59
|
+
consumer_group VARCHAR(255), -- NULL for queue mode (competing consumers)
|
|
60
|
+
status VARCHAR(20) DEFAULT 'pending',
|
|
61
|
+
worker_id VARCHAR(255),
|
|
62
|
+
locked_at TIMESTAMPTZ,
|
|
63
|
+
completed_at TIMESTAMPTZ,
|
|
64
|
+
failed_at TIMESTAMPTZ,
|
|
65
|
+
error_message TEXT,
|
|
66
|
+
retry_count INTEGER DEFAULT 0,
|
|
67
|
+
lease_expires_at TIMESTAMPTZ,
|
|
68
|
+
processing_at TIMESTAMPTZ,
|
|
69
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
70
|
+
UNIQUE(message_id, consumer_group) -- One status per message per consumer group
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
-- Consumer groups registry (tracks subscription preferences)
|
|
74
|
+
CREATE TABLE IF NOT EXISTS queen.consumer_groups (
|
|
75
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
76
|
+
queue_id UUID REFERENCES queen.queues(id) ON DELETE CASCADE,
|
|
77
|
+
name VARCHAR(255) NOT NULL,
|
|
78
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
79
|
+
subscription_start_from TIMESTAMPTZ, -- NULL = consume all, timestamp = consume from this point
|
|
80
|
+
active BOOLEAN DEFAULT true,
|
|
81
|
+
last_seen_at TIMESTAMPTZ DEFAULT NOW(),
|
|
82
|
+
UNIQUE(queue_id, name)
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
-- Partition leases table (tracks which partition is locked for which consumer group)
|
|
86
|
+
CREATE TABLE IF NOT EXISTS queen.partition_leases (
|
|
87
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
88
|
+
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
89
|
+
consumer_group VARCHAR(255) DEFAULT '__QUEUE_MODE__', -- '__QUEUE_MODE__' for queue mode
|
|
90
|
+
lease_expires_at TIMESTAMPTZ NOT NULL,
|
|
91
|
+
message_batch JSONB, -- Store IDs of messages in this lease
|
|
92
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
93
|
+
released_at TIMESTAMPTZ, -- When lease was released (NULL if active)
|
|
94
|
+
UNIQUE(partition_id, consumer_group) -- One active lease per partition per consumer group
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
-- Indexes for queues
|
|
98
|
+
CREATE INDEX IF NOT EXISTS idx_queues_name ON queen.queues(name);
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_queues_priority ON queen.queues(priority DESC);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_queues_namespace ON queen.queues(namespace) WHERE namespace IS NOT NULL;
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_queues_task ON queen.queues(task) WHERE task IS NOT NULL;
|
|
102
|
+
CREATE INDEX IF NOT EXISTS idx_queues_namespace_task ON queen.queues(namespace, task) WHERE namespace IS NOT NULL AND task IS NOT NULL;
|
|
103
|
+
CREATE INDEX IF NOT EXISTS idx_queues_namespace_priority ON queen.queues(namespace, priority DESC) WHERE namespace IS NOT NULL;
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_queues_task_priority ON queen.queues(task, priority DESC) WHERE task IS NOT NULL;
|
|
105
|
+
CREATE INDEX IF NOT EXISTS idx_queues_retention_enabled ON queen.queues(retention_enabled) WHERE retention_enabled = true;
|
|
106
|
+
CREATE INDEX IF NOT EXISTS idx_queues_lease_time ON queen.queues(lease_time);
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_queues_ttl ON queen.queues(ttl);
|
|
108
|
+
|
|
109
|
+
-- Indexes for partitions
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_partitions_queue_name ON queen.partitions(queue_id, name);
|
|
111
|
+
|
|
112
|
+
-- Indexes for messages (simplified - no status fields)
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_messages_partition_created ON queen.messages(partition_id, created_at);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_messages_transaction_id ON queen.messages(transaction_id);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_messages_trace_id ON queen.messages(trace_id);
|
|
116
|
+
CREATE INDEX IF NOT EXISTS idx_messages_created_at ON queen.messages(created_at);
|
|
117
|
+
|
|
118
|
+
-- Indexes for messages_status (critical for performance)
|
|
119
|
+
-- Note: Removed the partial unique index idx_status_queue_mode as it conflicts with
|
|
120
|
+
-- the table-level UNIQUE(message_id, consumer_group) constraint
|
|
121
|
+
|
|
122
|
+
-- Bus mode index (consumer_group IS NOT NULL)
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_status_bus_mode
|
|
124
|
+
ON queen.messages_status(message_id, consumer_group, status)
|
|
125
|
+
WHERE consumer_group IS NOT NULL;
|
|
126
|
+
|
|
127
|
+
-- Index for finding pending messages by consumer group
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_status_pending
|
|
129
|
+
ON queen.messages_status(consumer_group, status, message_id)
|
|
130
|
+
WHERE status = 'pending';
|
|
131
|
+
|
|
132
|
+
-- Index for lease expiration checks
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_status_lease_expires
|
|
134
|
+
ON queen.messages_status(lease_expires_at, status, consumer_group)
|
|
135
|
+
WHERE status = 'processing';
|
|
136
|
+
|
|
137
|
+
-- Index for fast queue depth checks
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_messages_status_pending_processing
|
|
139
|
+
ON queen.messages_status(message_id, status)
|
|
140
|
+
WHERE status IN ('pending', 'processing') OR status IS NULL;
|
|
141
|
+
|
|
142
|
+
-- Index for efficient pop operations (find messages without status for a consumer group)
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_status_message_consumer
|
|
144
|
+
ON queen.messages_status(message_id, consumer_group);
|
|
145
|
+
|
|
146
|
+
-- Indexes for consumer groups
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_consumer_groups_queue_name
|
|
148
|
+
ON queen.consumer_groups(queue_id, name);
|
|
149
|
+
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_consumer_groups_active
|
|
151
|
+
ON queen.consumer_groups(queue_id, active)
|
|
152
|
+
WHERE active = true;
|
|
153
|
+
|
|
154
|
+
-- Indexes for partition leases
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_partition_leases_expires
|
|
156
|
+
ON queen.partition_leases(lease_expires_at)
|
|
157
|
+
WHERE released_at IS NULL;
|
|
158
|
+
|
|
159
|
+
CREATE INDEX IF NOT EXISTS idx_partition_leases_active
|
|
160
|
+
ON queen.partition_leases(partition_id, consumer_group)
|
|
161
|
+
WHERE released_at IS NULL;
|
|
162
|
+
|
|
163
|
+
CREATE INDEX IF NOT EXISTS idx_partition_leases_active_lookup
|
|
164
|
+
ON queen.partition_leases(partition_id, consumer_group, lease_expires_at)
|
|
165
|
+
WHERE released_at IS NULL;
|
|
166
|
+
|
|
167
|
+
-- Retention history table for tracking deletions
|
|
168
|
+
CREATE TABLE IF NOT EXISTS queen.retention_history (
|
|
169
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
170
|
+
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
171
|
+
messages_deleted INTEGER DEFAULT 0,
|
|
172
|
+
retention_type VARCHAR(50), -- 'retention', 'completed', 'evicted'
|
|
173
|
+
executed_at TIMESTAMPTZ DEFAULT NOW()
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_retention_history_partition
|
|
177
|
+
ON queen.retention_history(partition_id);
|
|
178
|
+
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_retention_history_executed
|
|
180
|
+
ON queen.retention_history(executed_at);
|
|
181
|
+
|
|
182
|
+
-- Trigger to update partition last_activity
|
|
183
|
+
CREATE OR REPLACE FUNCTION update_partition_last_activity()
|
|
184
|
+
RETURNS TRIGGER AS $$
|
|
185
|
+
BEGIN
|
|
186
|
+
UPDATE queen.partitions
|
|
187
|
+
SET last_activity = NOW()
|
|
188
|
+
WHERE id = NEW.partition_id;
|
|
189
|
+
RETURN NEW;
|
|
190
|
+
END;
|
|
191
|
+
$$ LANGUAGE plpgsql;
|
|
192
|
+
|
|
193
|
+
DROP TRIGGER IF EXISTS trigger_update_partition_activity ON queen.messages;
|
|
194
|
+
CREATE TRIGGER trigger_update_partition_activity
|
|
195
|
+
AFTER INSERT ON queen.messages
|
|
196
|
+
FOR EACH ROW
|
|
197
|
+
EXECUTE FUNCTION update_partition_last_activity();
|
|
198
|
+
|
|
199
|
+
-- Function to clean up old completed statuses in bus mode
|
|
200
|
+
CREATE OR REPLACE FUNCTION cleanup_completed_statuses()
|
|
201
|
+
RETURNS void AS $$
|
|
202
|
+
BEGIN
|
|
203
|
+
-- Delete completed statuses older than retention period for bus mode
|
|
204
|
+
DELETE FROM queen.messages_status ms
|
|
205
|
+
USING queen.messages m, queen.partitions p, queen.queues q
|
|
206
|
+
WHERE ms.message_id = m.id
|
|
207
|
+
AND m.partition_id = p.id
|
|
208
|
+
AND p.queue_id = q.id
|
|
209
|
+
AND ms.status IN ('completed', 'failed')
|
|
210
|
+
AND ms.consumer_group IS NOT NULL -- Only for bus mode
|
|
211
|
+
AND q.completed_retention_seconds > 0
|
|
212
|
+
AND ms.completed_at < NOW() - INTERVAL '1 second' * q.completed_retention_seconds;
|
|
213
|
+
END;
|
|
214
|
+
$$ LANGUAGE plpgsql;
|
|
@@ -0,0 +1,59 @@
|
|
|
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
|
+
};
|