queen-mq 0.1.0 → 0.1.2
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 +862 -752
- package/AUTH.md +2044 -0
- package/LICENSE.md +202 -0
- package/README.md +1705 -1051
- package/WEBAPP.md +1889 -0
- package/assets/dashboard-01.png +0 -0
- package/assets/queen-logo-blue.svg +210 -0
- package/assets/queen-logo-cyan.svg +210 -0
- package/assets/queen-logo-indigo.svg +210 -0
- package/assets/queen-logo-orange.svg +210 -0
- package/assets/queen-logo-pink.svg +210 -0
- package/assets/queen-logo-purple.svg +210 -0
- package/assets/queen-logo-rose.svg +239 -0
- package/assets/queen-logo.svg +263 -0
- package/examples/batch-processing.js +58 -0
- package/examples/test-complete-client.js +260 -0
- package/examples/test-dashboard-api.js +200 -0
- package/examples/test-traceid.js +147 -0
- package/package.json +17 -4
- package/server.log +1 -0
- package/src/benchmark/consumer.js +207 -0
- package/src/benchmark/consumer_multi.js +216 -0
- package/src/benchmark/producer.js +75 -0
- package/src/benchmark/producer_multi.js +115 -0
- package/src/client/client.js +300 -31
- package/src/client/queenClient.js +5 -0
- package/src/cluster-server.js +242 -0
- package/src/config.js +19 -5
- package/src/database/connection.js +42 -16
- package/src/database/poolManager.js +7 -0
- package/src/database/schema-v2.sql +194 -130
- package/src/managers/queueManagerOptimized.js +823 -933
- package/src/managers/systemEventManager.js +8 -3
- package/src/routes/messages.js +127 -57
- package/src/routes/pop.js +27 -43
- package/src/routes/resources.js +61 -27
- package/src/routes/status.js +1037 -0
- package/src/server.js +308 -272
- package/src/services/evictionService.js +57 -28
- package/src/services/retentionService.js +44 -11
- package/src/test/MIGRATION_ISSUES.md +174 -0
- package/src/test/README.md +203 -0
- package/src/test/advanced-pattern-tests.js +1137 -0
- package/src/test/bus-mode-tests.js +361 -0
- package/src/test/core-tests.js +342 -0
- package/src/test/edge-case-tests.js +561 -0
- package/src/test/enterprise-tests.js +637 -0
- package/src/test/partition-locking-tests.js +545 -0
- package/src/test/test-new.js +278 -0
- package/src/test/test.js +6 -3
- package/src/test/utils.js +169 -0
- package/src/utils/streaming.js +231 -0
- package/src/utils/uuid.js +2 -2
- package/src/websocket/wsServer.js +10 -3
- package/test-keepalive-v2.sh +22 -0
- package/webapp/COLOR_GUIDE.md +118 -0
- package/webapp/README.md +143 -0
- package/webapp/index.html +14 -0
- package/webapp/package-lock.json +3184 -0
- package/webapp/package.json +25 -0
- package/webapp/postcss.config.js +7 -0
- package/webapp/public/assets/queen-logo-blue.svg +210 -0
- package/webapp/public/assets/queen-logo-cyan.svg +210 -0
- package/webapp/public/assets/queen-logo-indigo.svg +210 -0
- package/webapp/public/assets/queen-logo-orange.svg +210 -0
- package/webapp/public/assets/queen-logo-pink.svg +210 -0
- package/webapp/public/assets/queen-logo-purple.svg +210 -0
- package/webapp/public/assets/queen-logo-rose.svg +239 -0
- package/webapp/public/assets/queen-logo.svg +263 -0
- package/webapp/src/App.vue +19 -0
- package/webapp/src/api/analytics.js +10 -0
- package/webapp/src/api/client.js +29 -0
- package/webapp/src/api/consumers.js +52 -0
- package/webapp/src/api/health.js +7 -0
- package/webapp/src/api/messages.js +26 -0
- package/webapp/src/api/queues.js +14 -0
- package/webapp/src/api/resources.js +8 -0
- package/webapp/src/assets/styles/main.css +357 -0
- package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
- package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
- package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
- package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
- package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
- package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
- package/webapp/src/components/common/ConfirmDialog.vue +56 -0
- package/webapp/src/components/common/LoadingSpinner.vue +6 -0
- package/webapp/src/components/common/MetricCard.vue +43 -0
- package/webapp/src/components/common/StatusBadge.vue +45 -0
- package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
- package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
- package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
- package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
- package/webapp/src/components/layout/AppLayout.vue +110 -0
- package/webapp/src/components/layout/AppSidebar.vue +304 -0
- package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
- package/webapp/src/components/messages/MessageFilters.vue +114 -0
- package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
- package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
- package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
- package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
- package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
- package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
- package/webapp/src/components/queues/QueueFilters.vue +90 -0
- package/webapp/src/composables/useApi.js +34 -0
- package/webapp/src/composables/useTheme.js +36 -0
- package/webapp/src/main.js +11 -0
- package/webapp/src/router/index.js +42 -0
- package/webapp/src/utils/colors.js +96 -0
- package/webapp/src/utils/formatters.js +49 -0
- package/webapp/src/views/Analytics.vue +377 -0
- package/webapp/src/views/ConsumerGroups.vue +433 -0
- package/webapp/src/views/Dashboard.vue +418 -0
- package/webapp/src/views/Messages.vue +361 -0
- package/webapp/src/views/QueueDetail.vue +582 -0
- package/webapp/src/views/Queues.vue +496 -0
- package/webapp/tailwind.config.js +25 -0
- package/webapp/vite.config.js +10 -0
- package/CACHE.md +0 -519
- package/DASHBOARD-V3.md +0 -478
- package/DASHBOARD.md +0 -382
- package/MOD_QUEUE.md +0 -453
- package/PARTITION_LOCKING_DESIGN.md +0 -989
- package/PLAN.md +0 -707
- package/QUERY_ANALSYS.md +0 -72
- package/QUEUE_BUS.md +0 -334
- package/V2-PLAN.md +0 -236
- package/dashboard/.vscode/extensions.json +0 -3
- package/dashboard/README.md +0 -5
- package/dashboard/index.html +0 -14
- package/dashboard/package-lock.json +0 -1458
- package/dashboard/package.json +0 -25
- package/dashboard/public/vite.svg +0 -1
- package/dashboard/src/App.vue +0 -29
- package/dashboard/src/assets/styles/main.css +0 -908
- package/dashboard/src/assets/vue.svg +0 -1
- package/dashboard/src/components/cards/MetricCard.vue +0 -298
- package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
- package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
- package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
- package/dashboard/src/components/common/ActivityFeed.vue +0 -251
- package/dashboard/src/components/layout/AppHeader.vue +0 -208
- package/dashboard/src/components/layout/AppLayout.vue +0 -88
- package/dashboard/src/components/layout/AppSidebar.vue +0 -261
- package/dashboard/src/main.js +0 -44
- package/dashboard/src/router.js +0 -54
- package/dashboard/src/services/api.js +0 -187
- package/dashboard/src/services/websocket.js +0 -167
- package/dashboard/src/utils/constants.js +0 -56
- package/dashboard/src/utils/helpers.js +0 -118
- package/dashboard/src/views/Analytics.vue +0 -912
- package/dashboard/src/views/Dashboard.vue +0 -906
- package/dashboard/src/views/Messages.vue +0 -437
- package/dashboard/src/views/QueueDetail.vue +0 -501
- package/dashboard/src/views/Queues.vue +0 -333
- package/dashboard/vite.config.js +0 -30
- package/debug-namespace.js +0 -110
- package/docs/long-polling.md +0 -159
- package/docs/multi-server-cache-solutions.md +0 -185
- package/docs/performance-tuning.md +0 -222
- package/src/routes/analytics.js +0 -812
package/src/config.js
CHANGED
|
@@ -23,12 +23,17 @@ export const DATABASE = {
|
|
|
23
23
|
PASSWORD: process.env.PG_PASSWORD || 'postgres',
|
|
24
24
|
PORT: parseInt(process.env.PG_PORT) || 5432,
|
|
25
25
|
|
|
26
|
+
// SSL configuration
|
|
27
|
+
USE_SSL: process.env.PG_USE_SSL === 'true',
|
|
28
|
+
SSL_REJECT_UNAUTHORIZED: process.env.PG_SSL_REJECT_UNAUTHORIZED !== 'false',
|
|
29
|
+
|
|
26
30
|
// Pool configuration
|
|
27
|
-
POOL_SIZE: parseInt(process.env.DB_POOL_SIZE) ||
|
|
31
|
+
POOL_SIZE: parseInt(process.env.DB_POOL_SIZE) || 150,
|
|
28
32
|
IDLE_TIMEOUT: parseInt(process.env.DB_IDLE_TIMEOUT) || 30000, // 30 seconds
|
|
29
33
|
CONNECTION_TIMEOUT: parseInt(process.env.DB_CONNECTION_TIMEOUT) || 2000, // 2 seconds
|
|
30
34
|
STATEMENT_TIMEOUT: parseInt(process.env.DB_STATEMENT_TIMEOUT) || 30000, // 30 seconds
|
|
31
35
|
QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT) || 30000, // 30 seconds
|
|
36
|
+
LOCK_TIMEOUT: parseInt(process.env.DB_LOCK_TIMEOUT) || 10000, // 10 seconds
|
|
32
37
|
|
|
33
38
|
// Pool manager settings
|
|
34
39
|
MAX_RETRIES: parseInt(process.env.DB_MAX_RETRIES) || 3
|
|
@@ -43,8 +48,14 @@ export const QUEUE = {
|
|
|
43
48
|
BATCH_INSERT_SIZE: parseInt(process.env.BATCH_INSERT_SIZE) || 1000,
|
|
44
49
|
|
|
45
50
|
// Long polling
|
|
46
|
-
POLL_INTERVAL: parseInt(process.env.QUEUE_POLL_INTERVAL) || 100, // 100ms
|
|
47
|
-
POLL_INTERVAL_FILTERED: parseInt(process.env.QUEUE_POLL_INTERVAL_FILTERED) ||
|
|
51
|
+
POLL_INTERVAL: parseInt(process.env.QUEUE_POLL_INTERVAL) || 100, // 100ms - initial poll interval
|
|
52
|
+
POLL_INTERVAL_FILTERED: parseInt(process.env.QUEUE_POLL_INTERVAL_FILTERED) || 50, // 50ms for better distribution across consumers
|
|
53
|
+
MAX_POLL_INTERVAL: parseInt(process.env.QUEUE_MAX_POLL_INTERVAL) || 2000, // 2000ms - max poll interval after backoff
|
|
54
|
+
BACKOFF_THRESHOLD: parseInt(process.env.QUEUE_BACKOFF_THRESHOLD) || 5, // Number of empty polls before backoff starts
|
|
55
|
+
BACKOFF_MULTIPLIER: parseFloat(process.env.QUEUE_BACKOFF_MULTIPLIER) || 2, // Exponential backoff multiplier
|
|
56
|
+
|
|
57
|
+
// Partition selection for filtered pops (namespace/task)
|
|
58
|
+
MAX_PARTITION_CANDIDATES: parseInt(process.env.MAX_PARTITION_CANDIDATES) || 100, // Number of candidate partitions to fetch for lease acquisition
|
|
48
59
|
|
|
49
60
|
// Queue defaults
|
|
50
61
|
DEFAULT_LEASE_TIME: parseInt(process.env.DEFAULT_LEASE_TIME) || 300, // 5 minutes
|
|
@@ -72,7 +83,7 @@ export const QUEUE = {
|
|
|
72
83
|
// System Events Configuration
|
|
73
84
|
export const SYSTEM_EVENTS = {
|
|
74
85
|
// Enable/disable system event propagation
|
|
75
|
-
ENABLED: process.env.QUEEN_SYSTEM_EVENTS_ENABLED === 'true' ||
|
|
86
|
+
ENABLED: process.env.QUEEN_SYSTEM_EVENTS_ENABLED === 'true' || false,
|
|
76
87
|
|
|
77
88
|
// Batching window for event publishing (milliseconds)
|
|
78
89
|
BATCH_MS: parseInt(process.env.QUEEN_SYSTEM_EVENTS_BATCH_MS) || 10,
|
|
@@ -91,6 +102,9 @@ export const JOBS = {
|
|
|
91
102
|
RETENTION_BATCH_SIZE: parseInt(process.env.RETENTION_BATCH_SIZE) || 1000,
|
|
92
103
|
PARTITION_CLEANUP_DAYS: parseInt(process.env.PARTITION_CLEANUP_DAYS) || 7,
|
|
93
104
|
|
|
105
|
+
// Metrics retention (messages_consumed table)
|
|
106
|
+
METRICS_RETENTION_DAYS: parseInt(process.env.METRICS_RETENTION_DAYS) || 90, // Keep 90 days of metrics by default
|
|
107
|
+
|
|
94
108
|
// Eviction service
|
|
95
109
|
EVICTION_INTERVAL: parseInt(process.env.EVICTION_INTERVAL) || 60000, // 1 minute
|
|
96
110
|
EVICTION_BATCH_SIZE: parseInt(process.env.EVICTION_BATCH_SIZE) || 1000,
|
|
@@ -129,7 +143,7 @@ export const CLIENT = {
|
|
|
129
143
|
|
|
130
144
|
// API Configuration
|
|
131
145
|
export const API = {
|
|
132
|
-
MAX_BODY_SIZE: parseInt(process.env.MAX_BODY_SIZE) ||
|
|
146
|
+
MAX_BODY_SIZE: parseInt(process.env.MAX_BODY_SIZE) || 100 * 1024 * 1024, // 100MB max body size
|
|
133
147
|
// Pagination
|
|
134
148
|
DEFAULT_LIMIT: parseInt(process.env.API_DEFAULT_LIMIT) || 100,
|
|
135
149
|
MAX_LIMIT: parseInt(process.env.API_MAX_LIMIT) || 1000,
|
|
@@ -2,22 +2,44 @@ import pg from 'pg';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
|
+
import config from '../config.js';
|
|
5
6
|
|
|
6
7
|
const { Pool } = pg;
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Create a PostgreSQL connection pool with proper configuration
|
|
12
|
+
* This is the single source of truth for pool creation across the application
|
|
13
|
+
*/
|
|
10
14
|
export const createPool = () => {
|
|
11
|
-
|
|
12
|
-
user:
|
|
13
|
-
host:
|
|
14
|
-
database:
|
|
15
|
-
password:
|
|
16
|
-
port:
|
|
17
|
-
max:
|
|
18
|
-
idleTimeoutMillis:
|
|
19
|
-
connectionTimeoutMillis:
|
|
20
|
-
|
|
15
|
+
const poolConfig = {
|
|
16
|
+
user: config.DATABASE.USER,
|
|
17
|
+
host: config.DATABASE.HOST,
|
|
18
|
+
database: config.DATABASE.DATABASE,
|
|
19
|
+
password: config.DATABASE.PASSWORD,
|
|
20
|
+
port: config.DATABASE.PORT,
|
|
21
|
+
max: config.DATABASE.POOL_SIZE,
|
|
22
|
+
idleTimeoutMillis: config.DATABASE.IDLE_TIMEOUT,
|
|
23
|
+
connectionTimeoutMillis: config.DATABASE.CONNECTION_TIMEOUT,
|
|
24
|
+
// Note: statement_timeout and query_timeout cannot be set as startup parameters
|
|
25
|
+
// They should be set per-query or per-transaction using SET commands
|
|
26
|
+
application_name: config.SERVER.APPLICATION_NAME,
|
|
27
|
+
// PERFORMANCE NOTE: TCP_NODELAY for latency reduction
|
|
28
|
+
// The node-postgres driver enables TCP_NODELAY by default on sockets
|
|
29
|
+
// For server-side: Edit postgresql.conf and add: tcp_nodelay = on
|
|
30
|
+
// Reference: https://stackoverflow.com/questions/60634455/how-does-one-configure-tcp-nodelay-for-libpq-and-postgres-server
|
|
31
|
+
keepAlive: true,
|
|
32
|
+
keepAliveInitialDelayMillis: 10000
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Add SSL configuration if enabled
|
|
36
|
+
if (config.DATABASE.USE_SSL) {
|
|
37
|
+
poolConfig.ssl = {
|
|
38
|
+
rejectUnauthorized: config.DATABASE.SSL_REJECT_UNAUTHORIZED
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return new Pool(poolConfig);
|
|
21
43
|
};
|
|
22
44
|
|
|
23
45
|
// Initialize database schema
|
|
@@ -74,8 +96,8 @@ export const withTransaction = async (pool, callback, isolationLevel = 'READ COM
|
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
// Set timeouts to prevent long blocks
|
|
77
|
-
await client.query(
|
|
78
|
-
await client.query(
|
|
99
|
+
await client.query(`SET LOCAL statement_timeout = ${config.DATABASE.STATEMENT_TIMEOUT}`);
|
|
100
|
+
await client.query(`SET LOCAL lock_timeout = ${config.DATABASE.LOCK_TIMEOUT}`);
|
|
79
101
|
|
|
80
102
|
const result = await callback(client);
|
|
81
103
|
await client.query('COMMIT');
|
|
@@ -83,13 +105,17 @@ export const withTransaction = async (pool, callback, isolationLevel = 'READ COM
|
|
|
83
105
|
} catch (error) {
|
|
84
106
|
await client.query('ROLLBACK');
|
|
85
107
|
|
|
86
|
-
// Check for
|
|
87
|
-
|
|
108
|
+
// Check for retriable errors:
|
|
109
|
+
// 40001: serialization_failure
|
|
110
|
+
// 40P01: deadlock_detected
|
|
111
|
+
// 55P03: lock_not_available (lock timeout)
|
|
112
|
+
// 25P02: in_failed_sql_transaction (transaction already aborted - retry whole transaction)
|
|
113
|
+
if ((error.code === '40001' || error.code === '40P01' || error.code === '55P03' || error.code === '25P02') && attempt < maxAttempts) {
|
|
88
114
|
// Exponential backoff: 100ms, 200ms, 400ms
|
|
89
115
|
const delay = Math.min(100 * Math.pow(2, attempt - 1), 1000);
|
|
90
116
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
91
117
|
attempt++;
|
|
92
|
-
console.log(`Transaction retry attempt ${attempt}/${maxAttempts} after ${error.code}`);
|
|
118
|
+
console.log(`Transaction retry attempt ${attempt}/${maxAttempts} after ${error.code} (${error.message})`);
|
|
93
119
|
continue;
|
|
94
120
|
}
|
|
95
121
|
|
|
@@ -22,6 +22,13 @@ export class PoolManager {
|
|
|
22
22
|
connectionTimeoutMillis: appConfig.DATABASE.CONNECTION_TIMEOUT,
|
|
23
23
|
...config
|
|
24
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
|
+
}
|
|
25
32
|
|
|
26
33
|
this.pool = new Pool(this.config);
|
|
27
34
|
this.waitingQueue = [];
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
--
|
|
2
|
-
--
|
|
1
|
+
-- ════════════════════════════════════════════════════════════════
|
|
2
|
+
-- Queen Message Queue Schema V3
|
|
3
|
+
-- Simplified: Merged cursor+lease, removed messages_status
|
|
4
|
+
-- ════════════════════════════════════════════════════════════════
|
|
3
5
|
|
|
4
|
-
-- Create schema only if it doesn't exist (preserves existing data)
|
|
5
6
|
CREATE SCHEMA IF NOT EXISTS queen;
|
|
6
7
|
|
|
7
|
-
--
|
|
8
|
+
-- ────────────────────────────────────────────────────────────────
|
|
9
|
+
-- 1. QUEUES - Top-level configuration
|
|
10
|
+
-- ────────────────────────────────────────────────────────────────
|
|
8
11
|
CREATE TABLE IF NOT EXISTS queen.queues (
|
|
9
12
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
13
|
name VARCHAR(255) UNIQUE NOT NULL,
|
|
11
|
-
namespace VARCHAR(255),
|
|
12
|
-
task VARCHAR(255),
|
|
13
|
-
priority INTEGER DEFAULT 0,
|
|
14
|
-
|
|
14
|
+
namespace VARCHAR(255),
|
|
15
|
+
task VARCHAR(255),
|
|
16
|
+
priority INTEGER DEFAULT 0,
|
|
17
|
+
|
|
18
|
+
-- Processing configuration
|
|
15
19
|
lease_time INTEGER DEFAULT 300,
|
|
16
20
|
retry_limit INTEGER DEFAULT 3,
|
|
17
21
|
retry_delay INTEGER DEFAULT 1000,
|
|
@@ -21,16 +25,30 @@ CREATE TABLE IF NOT EXISTS queen.queues (
|
|
|
21
25
|
dlq_after_max_retries BOOLEAN DEFAULT FALSE,
|
|
22
26
|
delayed_processing INTEGER DEFAULT 0,
|
|
23
27
|
window_buffer INTEGER DEFAULT 0,
|
|
28
|
+
|
|
29
|
+
-- Enterprise features
|
|
24
30
|
retention_seconds INTEGER DEFAULT 0,
|
|
25
31
|
completed_retention_seconds INTEGER DEFAULT 0,
|
|
26
32
|
retention_enabled BOOLEAN DEFAULT FALSE,
|
|
27
33
|
encryption_enabled BOOLEAN DEFAULT FALSE,
|
|
28
34
|
max_wait_time_seconds INTEGER DEFAULT 0,
|
|
29
|
-
max_queue_size INTEGER DEFAULT 0,
|
|
35
|
+
max_queue_size INTEGER DEFAULT 0,
|
|
36
|
+
|
|
30
37
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
31
38
|
);
|
|
32
39
|
|
|
33
|
-
--
|
|
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
|
+
-- ────────────────────────────────────────────────────────────────
|
|
34
52
|
CREATE TABLE IF NOT EXISTS queen.partitions (
|
|
35
53
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
36
54
|
queue_id UUID REFERENCES queen.queues(id) ON DELETE CASCADE,
|
|
@@ -40,131 +58,169 @@ CREATE TABLE IF NOT EXISTS queen.partitions (
|
|
|
40
58
|
UNIQUE(queue_id, name)
|
|
41
59
|
);
|
|
42
60
|
|
|
43
|
-
|
|
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
|
+
-- ────────────────────────────────────────────────────────────────
|
|
44
70
|
CREATE TABLE IF NOT EXISTS queen.messages (
|
|
45
71
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
46
|
-
transaction_id VARCHAR(255) UNIQUE NOT NULL,
|
|
47
|
-
trace_id UUID DEFAULT gen_random_uuid(),
|
|
72
|
+
transaction_id VARCHAR(255) UNIQUE NOT NULL,
|
|
73
|
+
trace_id UUID DEFAULT gen_random_uuid(),
|
|
48
74
|
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
49
75
|
payload JSONB NOT NULL,
|
|
50
76
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
51
77
|
is_encrypted BOOLEAN DEFAULT FALSE
|
|
52
|
-
-- Removed all status fields - moved to messages_status table
|
|
53
78
|
);
|
|
54
79
|
|
|
55
|
-
--
|
|
56
|
-
CREATE
|
|
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 (
|
|
57
97
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
58
|
-
|
|
59
|
-
consumer_group VARCHAR(255)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
-- ═══════════════════════════════════════════════════════════
|
|
67
113
|
lease_expires_at TIMESTAMPTZ,
|
|
68
|
-
|
|
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
|
+
-- ═══════════════════════════════════════════════════════════
|
|
69
129
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
70
|
-
|
|
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
|
+
)
|
|
71
141
|
);
|
|
72
142
|
|
|
73
|
-
--
|
|
74
|
-
CREATE
|
|
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 (
|
|
75
169
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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()
|
|
83
179
|
);
|
|
84
180
|
|
|
85
|
-
--
|
|
86
|
-
CREATE
|
|
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 (
|
|
87
199
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
200
|
+
message_id UUID REFERENCES queen.messages(id) ON DELETE CASCADE,
|
|
88
201
|
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
89
|
-
consumer_group VARCHAR(255)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
UNIQUE(partition_id, consumer_group) -- One active lease per partition per consumer group
|
|
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()
|
|
95
207
|
);
|
|
96
208
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
CREATE INDEX IF NOT EXISTS
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
CREATE INDEX IF NOT EXISTS
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
CREATE INDEX IF NOT EXISTS
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
--
|
|
110
|
-
|
|
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
|
|
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
|
+
-- ────────────────────────────────────────────────────────────────
|
|
168
224
|
CREATE TABLE IF NOT EXISTS queen.retention_history (
|
|
169
225
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
170
226
|
partition_id UUID REFERENCES queen.partitions(id) ON DELETE CASCADE,
|
|
@@ -179,7 +235,11 @@ ON queen.retention_history(partition_id);
|
|
|
179
235
|
CREATE INDEX IF NOT EXISTS idx_retention_history_executed
|
|
180
236
|
ON queen.retention_history(executed_at);
|
|
181
237
|
|
|
182
|
-
--
|
|
238
|
+
-- ────────────────────────────────────────────────────────────────
|
|
239
|
+
-- TRIGGERS
|
|
240
|
+
-- ────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
-- Update partition last_activity on message insert
|
|
183
243
|
CREATE OR REPLACE FUNCTION update_partition_last_activity()
|
|
184
244
|
RETURNS TRIGGER AS $$
|
|
185
245
|
BEGIN
|
|
@@ -196,19 +256,23 @@ AFTER INSERT ON queen.messages
|
|
|
196
256
|
FOR EACH ROW
|
|
197
257
|
EXECUTE FUNCTION update_partition_last_activity();
|
|
198
258
|
|
|
199
|
-
--
|
|
200
|
-
CREATE OR REPLACE FUNCTION
|
|
201
|
-
RETURNS
|
|
259
|
+
-- Update pending_estimate on message insert
|
|
260
|
+
CREATE OR REPLACE FUNCTION update_pending_on_push()
|
|
261
|
+
RETURNS TRIGGER AS $$
|
|
202
262
|
BEGIN
|
|
203
|
-
--
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
AND q.completed_retention_seconds > 0
|
|
212
|
-
AND ms.completed_at < NOW() - INTERVAL '1 second' * q.completed_retention_seconds;
|
|
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;
|
|
213
271
|
END;
|
|
214
|
-
$$ LANGUAGE plpgsql;
|
|
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();
|