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.
Files changed (160) hide show
  1. package/API.md +862 -752
  2. package/AUTH.md +2044 -0
  3. package/LICENSE.md +202 -0
  4. package/README.md +1705 -1051
  5. package/WEBAPP.md +1889 -0
  6. package/assets/dashboard-01.png +0 -0
  7. package/assets/queen-logo-blue.svg +210 -0
  8. package/assets/queen-logo-cyan.svg +210 -0
  9. package/assets/queen-logo-indigo.svg +210 -0
  10. package/assets/queen-logo-orange.svg +210 -0
  11. package/assets/queen-logo-pink.svg +210 -0
  12. package/assets/queen-logo-purple.svg +210 -0
  13. package/assets/queen-logo-rose.svg +239 -0
  14. package/assets/queen-logo.svg +263 -0
  15. package/examples/batch-processing.js +58 -0
  16. package/examples/test-complete-client.js +260 -0
  17. package/examples/test-dashboard-api.js +200 -0
  18. package/examples/test-traceid.js +147 -0
  19. package/package.json +17 -4
  20. package/server.log +1 -0
  21. package/src/benchmark/consumer.js +207 -0
  22. package/src/benchmark/consumer_multi.js +216 -0
  23. package/src/benchmark/producer.js +75 -0
  24. package/src/benchmark/producer_multi.js +115 -0
  25. package/src/client/client.js +300 -31
  26. package/src/client/queenClient.js +5 -0
  27. package/src/cluster-server.js +242 -0
  28. package/src/config.js +19 -5
  29. package/src/database/connection.js +42 -16
  30. package/src/database/poolManager.js +7 -0
  31. package/src/database/schema-v2.sql +194 -130
  32. package/src/managers/queueManagerOptimized.js +823 -933
  33. package/src/managers/systemEventManager.js +8 -3
  34. package/src/routes/messages.js +127 -57
  35. package/src/routes/pop.js +27 -43
  36. package/src/routes/resources.js +61 -27
  37. package/src/routes/status.js +1037 -0
  38. package/src/server.js +308 -272
  39. package/src/services/evictionService.js +57 -28
  40. package/src/services/retentionService.js +44 -11
  41. package/src/test/MIGRATION_ISSUES.md +174 -0
  42. package/src/test/README.md +203 -0
  43. package/src/test/advanced-pattern-tests.js +1137 -0
  44. package/src/test/bus-mode-tests.js +361 -0
  45. package/src/test/core-tests.js +342 -0
  46. package/src/test/edge-case-tests.js +561 -0
  47. package/src/test/enterprise-tests.js +637 -0
  48. package/src/test/partition-locking-tests.js +545 -0
  49. package/src/test/test-new.js +278 -0
  50. package/src/test/test.js +6 -3
  51. package/src/test/utils.js +169 -0
  52. package/src/utils/streaming.js +231 -0
  53. package/src/utils/uuid.js +2 -2
  54. package/src/websocket/wsServer.js +10 -3
  55. package/test-keepalive-v2.sh +22 -0
  56. package/webapp/COLOR_GUIDE.md +118 -0
  57. package/webapp/README.md +143 -0
  58. package/webapp/index.html +14 -0
  59. package/webapp/package-lock.json +3184 -0
  60. package/webapp/package.json +25 -0
  61. package/webapp/postcss.config.js +7 -0
  62. package/webapp/public/assets/queen-logo-blue.svg +210 -0
  63. package/webapp/public/assets/queen-logo-cyan.svg +210 -0
  64. package/webapp/public/assets/queen-logo-indigo.svg +210 -0
  65. package/webapp/public/assets/queen-logo-orange.svg +210 -0
  66. package/webapp/public/assets/queen-logo-pink.svg +210 -0
  67. package/webapp/public/assets/queen-logo-purple.svg +210 -0
  68. package/webapp/public/assets/queen-logo-rose.svg +239 -0
  69. package/webapp/public/assets/queen-logo.svg +263 -0
  70. package/webapp/src/App.vue +19 -0
  71. package/webapp/src/api/analytics.js +10 -0
  72. package/webapp/src/api/client.js +29 -0
  73. package/webapp/src/api/consumers.js +52 -0
  74. package/webapp/src/api/health.js +7 -0
  75. package/webapp/src/api/messages.js +26 -0
  76. package/webapp/src/api/queues.js +14 -0
  77. package/webapp/src/api/resources.js +8 -0
  78. package/webapp/src/assets/styles/main.css +357 -0
  79. package/webapp/src/components/analytics/AnalyticsFilters.vue +87 -0
  80. package/webapp/src/components/analytics/AnalyticsMetrics.vue +57 -0
  81. package/webapp/src/components/analytics/MessageDistributionChart.vue +111 -0
  82. package/webapp/src/components/analytics/MessageFlowChart.vue +173 -0
  83. package/webapp/src/components/analytics/TimeRangeSelector.vue +27 -0
  84. package/webapp/src/components/analytics/TopQueuesChart.vue +132 -0
  85. package/webapp/src/components/common/ConfirmDialog.vue +56 -0
  86. package/webapp/src/components/common/LoadingSpinner.vue +6 -0
  87. package/webapp/src/components/common/MetricCard.vue +43 -0
  88. package/webapp/src/components/common/StatusBadge.vue +45 -0
  89. package/webapp/src/components/dashboard/MessageStatusCard.vue +50 -0
  90. package/webapp/src/components/dashboard/PerformanceCard.vue +38 -0
  91. package/webapp/src/components/dashboard/ThroughputChart.vue +182 -0
  92. package/webapp/src/components/dashboard/TopQueuesTable.vue +53 -0
  93. package/webapp/src/components/layout/AppLayout.vue +110 -0
  94. package/webapp/src/components/layout/AppSidebar.vue +304 -0
  95. package/webapp/src/components/messages/MessageDetailPanel.vue +242 -0
  96. package/webapp/src/components/messages/MessageFilters.vue +114 -0
  97. package/webapp/src/components/queue-detail/PartitionList.vue +79 -0
  98. package/webapp/src/components/queue-detail/PushMessageModal.vue +175 -0
  99. package/webapp/src/components/queue-detail/QueueConfig.vue +63 -0
  100. package/webapp/src/components/queue-detail/QueueDetailHeader.vue +53 -0
  101. package/webapp/src/components/queue-detail/RecentMessages.vue +76 -0
  102. package/webapp/src/components/queues/CreateQueueModal.vue +193 -0
  103. package/webapp/src/components/queues/QueueFilters.vue +90 -0
  104. package/webapp/src/composables/useApi.js +34 -0
  105. package/webapp/src/composables/useTheme.js +36 -0
  106. package/webapp/src/main.js +11 -0
  107. package/webapp/src/router/index.js +42 -0
  108. package/webapp/src/utils/colors.js +96 -0
  109. package/webapp/src/utils/formatters.js +49 -0
  110. package/webapp/src/views/Analytics.vue +377 -0
  111. package/webapp/src/views/ConsumerGroups.vue +433 -0
  112. package/webapp/src/views/Dashboard.vue +418 -0
  113. package/webapp/src/views/Messages.vue +361 -0
  114. package/webapp/src/views/QueueDetail.vue +582 -0
  115. package/webapp/src/views/Queues.vue +496 -0
  116. package/webapp/tailwind.config.js +25 -0
  117. package/webapp/vite.config.js +10 -0
  118. package/CACHE.md +0 -519
  119. package/DASHBOARD-V3.md +0 -478
  120. package/DASHBOARD.md +0 -382
  121. package/MOD_QUEUE.md +0 -453
  122. package/PARTITION_LOCKING_DESIGN.md +0 -989
  123. package/PLAN.md +0 -707
  124. package/QUERY_ANALSYS.md +0 -72
  125. package/QUEUE_BUS.md +0 -334
  126. package/V2-PLAN.md +0 -236
  127. package/dashboard/.vscode/extensions.json +0 -3
  128. package/dashboard/README.md +0 -5
  129. package/dashboard/index.html +0 -14
  130. package/dashboard/package-lock.json +0 -1458
  131. package/dashboard/package.json +0 -25
  132. package/dashboard/public/vite.svg +0 -1
  133. package/dashboard/src/App.vue +0 -29
  134. package/dashboard/src/assets/styles/main.css +0 -908
  135. package/dashboard/src/assets/vue.svg +0 -1
  136. package/dashboard/src/components/cards/MetricCard.vue +0 -298
  137. package/dashboard/src/components/charts/QueueDepthChart.vue +0 -276
  138. package/dashboard/src/components/charts/QueueLagChart.vue +0 -436
  139. package/dashboard/src/components/charts/ThroughputChart.vue +0 -302
  140. package/dashboard/src/components/common/ActivityFeed.vue +0 -251
  141. package/dashboard/src/components/layout/AppHeader.vue +0 -208
  142. package/dashboard/src/components/layout/AppLayout.vue +0 -88
  143. package/dashboard/src/components/layout/AppSidebar.vue +0 -261
  144. package/dashboard/src/main.js +0 -44
  145. package/dashboard/src/router.js +0 -54
  146. package/dashboard/src/services/api.js +0 -187
  147. package/dashboard/src/services/websocket.js +0 -167
  148. package/dashboard/src/utils/constants.js +0 -56
  149. package/dashboard/src/utils/helpers.js +0 -118
  150. package/dashboard/src/views/Analytics.vue +0 -912
  151. package/dashboard/src/views/Dashboard.vue +0 -906
  152. package/dashboard/src/views/Messages.vue +0 -437
  153. package/dashboard/src/views/QueueDetail.vue +0 -501
  154. package/dashboard/src/views/Queues.vue +0 -333
  155. package/dashboard/vite.config.js +0 -30
  156. package/debug-namespace.js +0 -110
  157. package/docs/long-polling.md +0 -159
  158. package/docs/multi-server-cache-solutions.md +0 -185
  159. package/docs/performance-tuning.md +0 -222
  160. 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) || 20,
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) || 1000, // 1 second
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' || 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) || 10 * 1024 * 1024, // 10MB 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
- // Create connection pool
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
- return new Pool({
12
- user: process.env.PG_USER || 'postgres',
13
- host: process.env.PG_HOST || 'localhost',
14
- database: process.env.PG_DB || 'postgres',
15
- password: process.env.PG_PASSWORD || 'postgres',
16
- port: process.env.PG_PORT || 5432,
17
- max: process.env.DB_POOL_SIZE || 20,
18
- idleTimeoutMillis: 30000,
19
- connectionTimeoutMillis: 2000,
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('SET LOCAL statement_timeout = 30000'); // 30 seconds
78
- await client.query('SET LOCAL lock_timeout = 5000'); // 5 seconds
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 serialization failure or deadlock
87
- if ((error.code === '40001' || error.code === '40P01') && attempt < maxAttempts) {
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
- -- Queen Message Queue Schema V2 with Bus/Consumer Group Support
2
- -- Structure: Queues → Partitions → Messages → Status (per consumer group)
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
- -- Queues table (top level, with all configuration options)
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), -- 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)
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, -- 0 = unlimited queue size
35
+ max_queue_size INTEGER DEFAULT 0,
36
+
30
37
  created_at TIMESTAMPTZ DEFAULT NOW()
31
38
  );
32
39
 
33
- -- Partitions table (subdivisions of queues, where FIFO happens)
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
- -- Messages table (immutable message data only)
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, -- Changed from UUID to string for flexibility
47
- trace_id UUID DEFAULT gen_random_uuid(), -- For tracking multi-step pipelines
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
- -- Messages status table (tracks consumption per consumer group)
56
- CREATE TABLE IF NOT EXISTS queen.messages_status (
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
- 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,
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
- processing_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
+ -- ═══════════════════════════════════════════════════════════
69
129
  created_at TIMESTAMPTZ DEFAULT NOW(),
70
- UNIQUE(message_id, consumer_group) -- One status per message per consumer group
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
- -- Consumer groups registry (tracks subscription preferences)
74
- CREATE TABLE IF NOT EXISTS queen.consumer_groups (
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
- 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)
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
- -- Partition leases table (tracks which partition is locked for which consumer group)
86
- CREATE TABLE IF NOT EXISTS queen.partition_leases (
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) 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
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
- -- 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
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
- -- Trigger to update partition last_activity
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
- -- Function to clean up old completed statuses in bus mode
200
- CREATE OR REPLACE FUNCTION cleanup_completed_statuses()
201
- RETURNS void AS $$
259
+ -- Update pending_estimate on message insert
260
+ CREATE OR REPLACE FUNCTION update_pending_on_push()
261
+ RETURNS TRIGGER AS $$
202
262
  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;
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();