queen-mq 0.3.0 → 0.4.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/README.md +212 -144
- package/client-js/client-v2/Queen.js +28 -0
- package/client-js/client-v2/README.md +6 -2
- package/client-js/client-v2/builders/QueueBuilder.js +7 -1
- package/client-js/client-v2/builders/TransactionBuilder.js +20 -4
- package/client-js/client-v2/stream/StreamBuilder.js +72 -0
- package/client-js/client-v2/stream/StreamConsumer.js +156 -0
- package/client-js/client-v2/stream/Window.js +140 -0
- package/client-js/test-v2/MAINTENANCE_TEST.md +148 -0
- package/client-js/test-v2/consume.js +1 -0
- package/client-js/test-v2/maintenance.js +261 -0
- package/client-js/test-v2/retention.js +69 -0
- package/client-js/test-v2/run.js +7 -1
- package/package.json +2 -2
- package/client-js/client/client.js +0 -1536
- package/client-js/client/index.js +0 -4
- package/client-js/client/utils/http.js +0 -173
- package/client-js/client/utils/loadBalancer.js +0 -152
- package/client-js/client/utils/retry.js +0 -41
- package/client-js/services/encryptionService.js +0 -82
- package/client-js/services/evictionService.js +0 -160
- package/client-js/services/retentionService.js +0 -162
- package/client-js/services/startupSync.js +0 -35
- package/client-js/test/README.md +0 -224
- package/client-js/test/advanced-client-tests.js +0 -761
- package/client-js/test/advanced-pattern-tests.js +0 -1137
- package/client-js/test/bus-mode-tests.js +0 -361
- package/client-js/test/core-tests.js +0 -457
- package/client-js/test/edge-case-tests.js +0 -562
- package/client-js/test/enterprise-tests.js +0 -637
- package/client-js/test/human.js +0 -162
- package/client-js/test/partition-locking-tests.js +0 -545
- package/client-js/test/partition-transaction-tests.js +0 -482
- package/client-js/test/qos0-tests.js +0 -334
- package/client-js/test/test-new.js +0 -370
- package/client-js/test/utils.js +0 -169
- package/client-js/test/window-buffer-test.js +0 -114
- package/client-js/utils/logger.js +0 -44
- package/client-js/utils/uuid.js +0 -5
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// Functional retention service for message cleanup
|
|
2
|
-
import { log, LogTypes } from '../utils/logger.js';
|
|
3
|
-
import config from '../config.js';
|
|
4
|
-
|
|
5
|
-
const RETENTION_INTERVAL = config.JOBS.RETENTION_INTERVAL;
|
|
6
|
-
|
|
7
|
-
// Apply retention policy to a single queue
|
|
8
|
-
const retentionQueue = async (client, queue) => {
|
|
9
|
-
const {
|
|
10
|
-
retention_seconds: retentionSeconds = 0,
|
|
11
|
-
completed_retention_seconds: completedRetentionSeconds = 0,
|
|
12
|
-
retention_enabled: retentionEnabled = false
|
|
13
|
-
} = queue;
|
|
14
|
-
|
|
15
|
-
if (!retentionEnabled) return 0;
|
|
16
|
-
|
|
17
|
-
let totalDeleted = 0;
|
|
18
|
-
|
|
19
|
-
// Delete old unconsumed messages from all partitions in this queue
|
|
20
|
-
if (retentionSeconds > 0) {
|
|
21
|
-
const result = await client.query(`
|
|
22
|
-
DELETE FROM queen.messages m
|
|
23
|
-
WHERE m.partition_id IN (
|
|
24
|
-
SELECT id FROM queen.partitions WHERE queue_id = $1
|
|
25
|
-
)
|
|
26
|
-
AND m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
27
|
-
AND NOT EXISTS (
|
|
28
|
-
-- Message has been consumed by at least one consumer
|
|
29
|
-
SELECT 1
|
|
30
|
-
FROM queen.partition_consumers pc
|
|
31
|
-
WHERE pc.partition_id = m.partition_id
|
|
32
|
-
AND (m.created_at, m.id) <= (pc.last_consumed_created_at, pc.last_consumed_id)
|
|
33
|
-
)
|
|
34
|
-
RETURNING id
|
|
35
|
-
`, [queue.id, retentionSeconds]);
|
|
36
|
-
|
|
37
|
-
totalDeleted += result.rowCount || 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Delete old consumed messages from all partitions in this queue
|
|
41
|
-
// (messages consumed by ALL consumer groups on that partition)
|
|
42
|
-
if (completedRetentionSeconds > 0) {
|
|
43
|
-
const result = await client.query(`
|
|
44
|
-
DELETE FROM queen.messages m
|
|
45
|
-
WHERE m.partition_id IN (
|
|
46
|
-
SELECT id FROM queen.partitions WHERE queue_id = $1
|
|
47
|
-
)
|
|
48
|
-
AND m.created_at < NOW() - INTERVAL '1 second' * $2
|
|
49
|
-
AND NOT EXISTS (
|
|
50
|
-
-- No consumer group that hasn't consumed this message yet
|
|
51
|
-
SELECT 1
|
|
52
|
-
FROM queen.partition_consumers pc
|
|
53
|
-
WHERE pc.partition_id = m.partition_id
|
|
54
|
-
AND ((m.created_at, m.id) > (pc.last_consumed_created_at, pc.last_consumed_id)
|
|
55
|
-
OR pc.last_consumed_id IS NULL)
|
|
56
|
-
)
|
|
57
|
-
RETURNING id
|
|
58
|
-
`, [queue.id, completedRetentionSeconds]);
|
|
59
|
-
|
|
60
|
-
totalDeleted += result.rowCount || 0;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Log retention if messages were deleted
|
|
64
|
-
if (totalDeleted > 0) {
|
|
65
|
-
log(`${LogTypes.RETENTION} | Queue: ${queue.name} | Count: ${totalDeleted} | RetentionSeconds: ${retentionSeconds} | CompletedRetentionSeconds: ${completedRetentionSeconds}`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return totalDeleted;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// Clean up empty partitions (no longer based on partition-level config)
|
|
72
|
-
const cleanupEmptyPartitions = async (client) => {
|
|
73
|
-
// For now, we keep Default partitions and only clean up truly empty non-default partitions
|
|
74
|
-
// that haven't had activity for a long time (e.g., 7 days)
|
|
75
|
-
const result = await client.query(`
|
|
76
|
-
DELETE FROM queen.partitions p
|
|
77
|
-
WHERE p.name != 'Default'
|
|
78
|
-
AND NOT EXISTS (
|
|
79
|
-
SELECT 1 FROM queen.messages m WHERE m.partition_id = p.id
|
|
80
|
-
)
|
|
81
|
-
AND p.last_activity < NOW() - INTERVAL '7 days'
|
|
82
|
-
RETURNING id, name
|
|
83
|
-
`);
|
|
84
|
-
|
|
85
|
-
if (result.rowCount > 0) {
|
|
86
|
-
log(`🗑️ Deleted ${result.rowCount} empty partitions`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return result.rowCount;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Clean up old metrics data (messages_consumed table)
|
|
93
|
-
const cleanupOldMetrics = async (client) => {
|
|
94
|
-
const retentionDays = config.JOBS.METRICS_RETENTION_DAYS;
|
|
95
|
-
|
|
96
|
-
// Delete metrics data older than the retention period
|
|
97
|
-
const result = await client.query(`
|
|
98
|
-
DELETE FROM queen.messages_consumed
|
|
99
|
-
WHERE acked_at < NOW() - INTERVAL '1 day' * $1
|
|
100
|
-
`, [retentionDays]);
|
|
101
|
-
|
|
102
|
-
if (result.rowCount > 0) {
|
|
103
|
-
log(`📊 Deleted ${result.rowCount} old metrics records (older than ${retentionDays} days)`);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return result.rowCount;
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
// Main retention function
|
|
110
|
-
const performRetention = async (pool) => {
|
|
111
|
-
const client = await pool.connect();
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
// Get queues with retention enabled
|
|
115
|
-
const queuesResult = await client.query(`
|
|
116
|
-
SELECT q.id, q.name, q.retention_enabled, q.retention_seconds,
|
|
117
|
-
q.completed_retention_seconds
|
|
118
|
-
FROM queen.queues q
|
|
119
|
-
WHERE q.retention_enabled = true
|
|
120
|
-
`);
|
|
121
|
-
|
|
122
|
-
let totalDeleted = 0;
|
|
123
|
-
for (const queue of queuesResult.rows) {
|
|
124
|
-
totalDeleted += await retentionQueue(client, queue);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Cleanup empty partitions
|
|
128
|
-
await cleanupEmptyPartitions(client);
|
|
129
|
-
|
|
130
|
-
// Cleanup old metrics data
|
|
131
|
-
await cleanupOldMetrics(client);
|
|
132
|
-
|
|
133
|
-
return totalDeleted;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
log('Retention error:', error);
|
|
136
|
-
return 0;
|
|
137
|
-
} finally {
|
|
138
|
-
client.release();
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// Start the retention job
|
|
143
|
-
export const startRetentionJob = (pool) => {
|
|
144
|
-
const intervalId = setInterval(async () => {
|
|
145
|
-
try {
|
|
146
|
-
await performRetention(pool);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
log('Retention job error:', error);
|
|
149
|
-
}
|
|
150
|
-
}, RETENTION_INTERVAL);
|
|
151
|
-
|
|
152
|
-
log(`♻️ Retention job started (interval: ${RETENTION_INTERVAL}ms)`);
|
|
153
|
-
|
|
154
|
-
// Return cleanup function
|
|
155
|
-
return () => {
|
|
156
|
-
clearInterval(intervalId);
|
|
157
|
-
log('Retention job stopped');
|
|
158
|
-
};
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// Export for manual execution
|
|
162
|
-
export const runRetention = performRetention;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { SYSTEM_QUEUE } from '../managers/systemEventManager.js';
|
|
2
|
-
|
|
3
|
-
export async function syncSystemEvents(client, eventManager, consumerGroup) {
|
|
4
|
-
console.log('Synchronizing system events...');
|
|
5
|
-
|
|
6
|
-
let processed = 0;
|
|
7
|
-
let hasMore = true;
|
|
8
|
-
|
|
9
|
-
while (hasMore) {
|
|
10
|
-
// Pop and process system events in batches using the server's consumer group
|
|
11
|
-
// This ensures we only process events we haven't seen before
|
|
12
|
-
const result = await client.pop({
|
|
13
|
-
queue: SYSTEM_QUEUE,
|
|
14
|
-
consumerGroup: consumerGroup, // Use server's unique consumer group
|
|
15
|
-
batch: 100,
|
|
16
|
-
wait: false
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (!result.messages || result.messages.length === 0) {
|
|
20
|
-
hasMore = false;
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
for (const message of result.messages) {
|
|
25
|
-
await eventManager.processSystemEvent(message.data);
|
|
26
|
-
await client.ack(message.transactionId, 'completed', null, consumerGroup);
|
|
27
|
-
processed++;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (processed > 0) {
|
|
32
|
-
console.log(`Synchronized ${processed} system events`);
|
|
33
|
-
}
|
|
34
|
-
return processed;
|
|
35
|
-
}
|
package/client-js/test/README.md
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
# Queen Message Queue Test Suite
|
|
2
|
-
|
|
3
|
-
This directory contains the comprehensive test suite for the Queen Message Queue system, using the new minimalist `Queen` client interface.
|
|
4
|
-
|
|
5
|
-
## Test Structure
|
|
6
|
-
|
|
7
|
-
The test suite is organized into focused, modular files:
|
|
8
|
-
|
|
9
|
-
### Core Files
|
|
10
|
-
|
|
11
|
-
- **`test-new.js`** - Main test runner that orchestrates all tests
|
|
12
|
-
- **`utils.js`** - Shared utilities (logging, database helpers, result tracking)
|
|
13
|
-
|
|
14
|
-
### Test Categories
|
|
15
|
-
|
|
16
|
-
#### 1. Core Features (`core-tests.js`)
|
|
17
|
-
- Queue creation policy
|
|
18
|
-
- Single/batch message push
|
|
19
|
-
- Queue configuration
|
|
20
|
-
- Take and acknowledgment
|
|
21
|
-
- Delayed processing
|
|
22
|
-
- FIFO ordering within partitions
|
|
23
|
-
|
|
24
|
-
#### 2. Partition Locking (`partition-locking-tests.js`)
|
|
25
|
-
- Partition locking in queue mode
|
|
26
|
-
- Partition locking in bus mode
|
|
27
|
-
- Specific partition requests with locking
|
|
28
|
-
- Namespace/task filtering with partition locking
|
|
29
|
-
|
|
30
|
-
#### 3. Enterprise Features (`enterprise-tests.js`)
|
|
31
|
-
- Message encryption/decryption
|
|
32
|
-
- Retention policies (pending & completed messages)
|
|
33
|
-
- Message eviction
|
|
34
|
-
- Combined enterprise features
|
|
35
|
-
- Enterprise error handling
|
|
36
|
-
|
|
37
|
-
#### 4. Bus Mode Features (`bus-mode-tests.js`)
|
|
38
|
-
- Consumer groups
|
|
39
|
-
- Mixed mode (queue + bus)
|
|
40
|
-
- Subscription modes (all vs new messages)
|
|
41
|
-
- Consumer group isolation
|
|
42
|
-
|
|
43
|
-
#### 5. Edge Cases (`edge-case-tests.js` & `partition-transaction-tests.js`)
|
|
44
|
-
- Empty and null payloads
|
|
45
|
-
- Very large payloads
|
|
46
|
-
- Concurrent push/take operations
|
|
47
|
-
- Retry limit exhaustion
|
|
48
|
-
- Lease expiration and redelivery
|
|
49
|
-
- SQL injection prevention
|
|
50
|
-
- XSS prevention
|
|
51
|
-
- **Partition-scoped transaction_id handling** (`partition-transaction-tests.js`):
|
|
52
|
-
- Duplicate transaction IDs across partitions with correct ACK targeting
|
|
53
|
-
- Batch ACK with duplicate transaction IDs
|
|
54
|
-
- DLQ operations with duplicate transaction IDs
|
|
55
|
-
- Analytics API with duplicate transaction IDs
|
|
56
|
-
|
|
57
|
-
#### 6. Advanced Patterns (`advanced-pattern-tests.js`)
|
|
58
|
-
- Multi-stage pipeline workflow
|
|
59
|
-
- Fan-out/fan-in pattern
|
|
60
|
-
- Dead letter queue pattern
|
|
61
|
-
- Circuit breaker pattern
|
|
62
|
-
- Message deduplication
|
|
63
|
-
|
|
64
|
-
## New vs Old Interface
|
|
65
|
-
|
|
66
|
-
### Old Interface (test.js)
|
|
67
|
-
```javascript
|
|
68
|
-
import { createQueenClient } from '../client/queenClient.js';
|
|
69
|
-
|
|
70
|
-
const client = createQueenClient({ baseUrls: [...] });
|
|
71
|
-
|
|
72
|
-
// Configure
|
|
73
|
-
await client.configure({ queue: 'myqueue', options: {} });
|
|
74
|
-
|
|
75
|
-
// Push
|
|
76
|
-
await client.push({ items: [{ queue: 'myqueue', partition: 'Default', payload: {...} }] });
|
|
77
|
-
|
|
78
|
-
// Pop
|
|
79
|
-
const result = await client.pop({ queue: 'myqueue', batch: 10 });
|
|
80
|
-
|
|
81
|
-
// Ack
|
|
82
|
-
await client.ack(transactionId, 'completed', null, consumerGroup);
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### New Interface (test-new.js)
|
|
86
|
-
```javascript
|
|
87
|
-
import { Queen } from '../client/client.js';
|
|
88
|
-
|
|
89
|
-
const client = new Queen({ baseUrls: [...] });
|
|
90
|
-
|
|
91
|
-
// Configure
|
|
92
|
-
await client.queue('myqueue', {}, { namespace, task });
|
|
93
|
-
|
|
94
|
-
// Push
|
|
95
|
-
await client.push('myqueue/partition', payload);
|
|
96
|
-
|
|
97
|
-
// Take (async iterator)
|
|
98
|
-
for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
|
|
99
|
-
// Process message
|
|
100
|
-
await client.ack(msg, true, { group: 'mygroup' });
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
## Key Differences
|
|
105
|
-
|
|
106
|
-
1. **Address Format**: The new interface uses a unified address format:
|
|
107
|
-
- `"queue"` - Simple queue
|
|
108
|
-
- `"queue/partition"` - Specific partition
|
|
109
|
-
- `"queue@group"` - Consumer group
|
|
110
|
-
- `"queue/partition@group"` - Partition + group
|
|
111
|
-
- `"namespace:name"` - Namespace filter
|
|
112
|
-
- `"task:name"` - Task filter
|
|
113
|
-
- `"namespace:billing/task:process"` - Combined filters
|
|
114
|
-
|
|
115
|
-
2. **Async Iterator**: `pop()` → `take()` using async iteration
|
|
116
|
-
```javascript
|
|
117
|
-
for await (const msg of client.take(address, options)) {
|
|
118
|
-
// Handle message
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
3. **Simplified Methods**: 4 core methods instead of many:
|
|
123
|
-
- `client.queue(name, options, metadata)` - Configure
|
|
124
|
-
- `client.push(address, payload, options)` - Send
|
|
125
|
-
- `client.take(address, options)` - Receive (async iterator)
|
|
126
|
-
- `client.ack(message, status, context)` - Acknowledge
|
|
127
|
-
|
|
128
|
-
4. **Message Properties**: Can be in payload or options:
|
|
129
|
-
```javascript
|
|
130
|
-
// In options
|
|
131
|
-
await client.push('queue', { data }, { transactionId: 'txn-1' });
|
|
132
|
-
|
|
133
|
-
// In payload
|
|
134
|
-
await client.push('queue', { data, transactionId: 'txn-1' });
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## Running Tests
|
|
138
|
-
|
|
139
|
-
```bash
|
|
140
|
-
# Make sure Queen servers are running first
|
|
141
|
-
# And database is accessible
|
|
142
|
-
|
|
143
|
-
cd /Users/alice/Work/queen
|
|
144
|
-
|
|
145
|
-
# Run all tests
|
|
146
|
-
nvm use 22 && node client-js/test/test-new.js
|
|
147
|
-
|
|
148
|
-
# Run specific test category
|
|
149
|
-
node client-js/test/test-new.js core # Core features only
|
|
150
|
-
node client-js/test/test-new.js partition # Partition locking tests only
|
|
151
|
-
node client-js/test/test-new.js enterprise # Enterprise features only
|
|
152
|
-
node client-js/test/test-new.js bus # Bus mode tests only
|
|
153
|
-
node client-js/test/test-new.js edge # Edge cases (includes partition-scoped txn_id tests)
|
|
154
|
-
node client-js/test/test-new.js advanced # Advanced patterns only
|
|
155
|
-
|
|
156
|
-
# Show help
|
|
157
|
-
node client-js/test/test-new.js help
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
### Testing Partition-Scoped Transaction ID Fix
|
|
161
|
-
|
|
162
|
-
The `edge` test category includes critical tests for partition-scoped transaction_id handling:
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
# Run all edge case tests (includes partition transaction_id tests)
|
|
166
|
-
node client-js/test/test-new.js edge
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
These tests verify that:
|
|
170
|
-
- ✅ The same `transaction_id` can exist in multiple partitions
|
|
171
|
-
- ✅ ACK operations correctly target messages using BOTH `partition_id` AND `transaction_id`
|
|
172
|
-
- ✅ Batch ACK operations scope correctly by partition
|
|
173
|
-
- ✅ DLQ operations only affect the intended partition
|
|
174
|
-
- ✅ Analytics API (`GET /api/v1/messages/:partitionId/:transactionId`) returns the correct message
|
|
175
|
-
|
|
176
|
-
### Available Test Categories
|
|
177
|
-
|
|
178
|
-
- **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
|
|
179
|
-
- **`partition`** (or `locking`) - Partition locking in queue and bus modes
|
|
180
|
-
- **`enterprise`** - Enterprise features (encryption, retention, eviction)
|
|
181
|
-
- **`bus`** - Bus mode features (consumer groups, mixed mode, subscription modes, isolation)
|
|
182
|
-
- **`edge`** - Edge cases (null payloads, large payloads, concurrency, SQL injection, XSS)
|
|
183
|
-
- **`advanced`** (or `pattern`) - Advanced patterns (pipeline, fan-out/fan-in, DLQ, circuit breaker, deduplication)
|
|
184
|
-
|
|
185
|
-
## Test Configuration
|
|
186
|
-
|
|
187
|
-
Tests use environment variables for configuration:
|
|
188
|
-
- `PG_HOST` - PostgreSQL host (default: localhost)
|
|
189
|
-
- `PG_PORT` - PostgreSQL port (default: 5432)
|
|
190
|
-
- `PG_DB` - Database name (default: postgres)
|
|
191
|
-
- `PG_USER` - Database user (default: postgres)
|
|
192
|
-
- `PG_PASSWORD` - Database password (default: postgres)
|
|
193
|
-
- `QUEEN_ENCRYPTION_KEY` - Optional encryption key for encryption tests
|
|
194
|
-
|
|
195
|
-
## Adding New Tests
|
|
196
|
-
|
|
197
|
-
1. Create a test function in the appropriate category file:
|
|
198
|
-
```javascript
|
|
199
|
-
export async function testMyFeature(client) {
|
|
200
|
-
startTest('My Feature Test', 'category');
|
|
201
|
-
try {
|
|
202
|
-
// Test code here
|
|
203
|
-
passTest('Feature works correctly');
|
|
204
|
-
} catch (error) {
|
|
205
|
-
failTest(error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
2. Import and add to `test-new.js`:
|
|
211
|
-
```javascript
|
|
212
|
-
import { testMyFeature } from './category-tests.js';
|
|
213
|
-
|
|
214
|
-
// In runTests():
|
|
215
|
-
await runTest(() => testMyFeature(client));
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
## Notes
|
|
219
|
-
|
|
220
|
-
- All tests automatically clean up test data before and after running
|
|
221
|
-
- Tests use the `test-*`, `edge-*`, `pattern-*`, and `workflow-*` queue name prefixes
|
|
222
|
-
- The test suite supports both single-server and multi-server configurations
|
|
223
|
-
- Some enterprise tests (encryption, retention) are skipped if not configured
|
|
224
|
-
|