queen-mq 0.3.1 โ 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 +1 -1
- 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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Window } from './Window.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* StreamConsumer - Manages consuming windows from a stream
|
|
5
|
+
*/
|
|
6
|
+
export class StreamConsumer {
|
|
7
|
+
constructor(httpClient, queen, streamName, consumerGroup) {
|
|
8
|
+
this.httpClient = httpClient;
|
|
9
|
+
this.queen = queen;
|
|
10
|
+
this.streamName = streamName;
|
|
11
|
+
this.consumerGroup = consumerGroup;
|
|
12
|
+
this.pollTimeout = 30000; // 30s long poll
|
|
13
|
+
this.leaseRenewInterval = 20000; // 20s renewal (before 60s timeout)
|
|
14
|
+
this.running = false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Start processing windows with the provided callback
|
|
19
|
+
* Runs in an infinite loop until stopped
|
|
20
|
+
* @param {Function} callback - async function(window) to process each window
|
|
21
|
+
*/
|
|
22
|
+
async process(callback) {
|
|
23
|
+
this.running = true;
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
while (this.running) {
|
|
27
|
+
let window = null;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
window = await this.pollWindow();
|
|
31
|
+
|
|
32
|
+
if (!window) {
|
|
33
|
+
// 204 No Content - no window available
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
await this.executeCallback(window, callback);
|
|
38
|
+
|
|
39
|
+
} catch (err) {
|
|
40
|
+
// Backoff on error
|
|
41
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Stop the processing loop
|
|
48
|
+
*/
|
|
49
|
+
stop() {
|
|
50
|
+
this.running = false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Poll for a window (blocking call)
|
|
55
|
+
* @returns {Promise<Window|null>} Window or null if no content
|
|
56
|
+
*/
|
|
57
|
+
async pollWindow() {
|
|
58
|
+
try {
|
|
59
|
+
const response = await this.httpClient.post('/api/v1/stream/poll', {
|
|
60
|
+
streamName: this.streamName,
|
|
61
|
+
consumerGroup: this.consumerGroup,
|
|
62
|
+
timeout: this.pollTimeout
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle 204 No Content (HttpClient returns null for 204)
|
|
66
|
+
if (!response) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// HttpClient returns the JSON body directly (e.g., {"window": {...}})
|
|
71
|
+
if (!response.window) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Window(response.window);
|
|
76
|
+
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// HttpClient throws on errors, returns null on 204
|
|
79
|
+
// So any error here is a real network/server error
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Execute the callback with lease renewal
|
|
86
|
+
* @param {Window} window - The window to process
|
|
87
|
+
* @param {Function} callback - User callback
|
|
88
|
+
*/
|
|
89
|
+
async executeCallback(window, callback) {
|
|
90
|
+
let leaseTimer = null;
|
|
91
|
+
let leaseExpired = false;
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
// Start lease renewal timer
|
|
95
|
+
leaseTimer = setInterval(async () => {
|
|
96
|
+
try {
|
|
97
|
+
await this.httpClient.post('/api/v1/stream/renew-lease', {
|
|
98
|
+
leaseId: window.leaseId,
|
|
99
|
+
extend_ms: this.leaseRenewInterval + 10000 // Extend by renewal interval + buffer
|
|
100
|
+
});
|
|
101
|
+
} catch (e) {
|
|
102
|
+
leaseExpired = true;
|
|
103
|
+
}
|
|
104
|
+
}, this.leaseRenewInterval);
|
|
105
|
+
|
|
106
|
+
// Execute user callback
|
|
107
|
+
await callback(window);
|
|
108
|
+
|
|
109
|
+
// ACK if lease hasn't expired
|
|
110
|
+
if (!leaseExpired) {
|
|
111
|
+
await this.httpClient.post('/api/v1/stream/ack', {
|
|
112
|
+
windowId: window.id,
|
|
113
|
+
leaseId: window.leaseId,
|
|
114
|
+
success: true
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (err) {
|
|
119
|
+
|
|
120
|
+
// NACK if lease hasn't expired
|
|
121
|
+
if (!leaseExpired) {
|
|
122
|
+
try {
|
|
123
|
+
await this.httpClient.post('/api/v1/stream/ack', {
|
|
124
|
+
windowId: window.id,
|
|
125
|
+
leaseId: window.leaseId,
|
|
126
|
+
success: false // NACK
|
|
127
|
+
});
|
|
128
|
+
} catch (nackErr) {
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Re-throw to trigger backoff
|
|
134
|
+
throw err;
|
|
135
|
+
|
|
136
|
+
} finally {
|
|
137
|
+
// Stop lease renewal
|
|
138
|
+
if (leaseTimer) {
|
|
139
|
+
clearInterval(leaseTimer);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Seek to a specific timestamp
|
|
146
|
+
* @param {string} timestamp - ISO timestamp to seek to
|
|
147
|
+
*/
|
|
148
|
+
async seek(timestamp) {
|
|
149
|
+
return this.httpClient.post('/api/v1/stream/seek', {
|
|
150
|
+
streamName: this.streamName,
|
|
151
|
+
consumerGroup: this.consumerGroup,
|
|
152
|
+
timestamp
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility function to get nested property value from object using dot notation
|
|
3
|
+
*/
|
|
4
|
+
const getPath = (obj, path) =>
|
|
5
|
+
path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : null), obj);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Window - Represents a time window of messages with utility methods
|
|
9
|
+
*/
|
|
10
|
+
export class Window {
|
|
11
|
+
constructor(rawWindow) {
|
|
12
|
+
// Copy all properties from raw window
|
|
13
|
+
Object.assign(this, rawWindow);
|
|
14
|
+
|
|
15
|
+
// Store immutable original messages
|
|
16
|
+
this.allMessages = Object.freeze(rawWindow.messages || []);
|
|
17
|
+
|
|
18
|
+
// Working copy for transformations
|
|
19
|
+
this.messages = [...this.allMessages];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Filter messages based on a predicate function
|
|
24
|
+
* @param {Function} filterFn - Predicate function (msg) => boolean
|
|
25
|
+
* @returns {Window} this for chaining
|
|
26
|
+
*/
|
|
27
|
+
filter(filterFn) {
|
|
28
|
+
this.messages = this.messages.filter(filterFn);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Group messages by a key path (dot notation supported)
|
|
34
|
+
* @param {string} keyPath - Path to key in message data (e.g., 'data.userId')
|
|
35
|
+
* @returns {Object} Object with keys as group names and values as arrays of messages
|
|
36
|
+
*/
|
|
37
|
+
groupBy(keyPath) {
|
|
38
|
+
const groups = {};
|
|
39
|
+
for (const msg of this.messages) {
|
|
40
|
+
const key = getPath(msg, keyPath) || 'null_key';
|
|
41
|
+
if (!groups[key]) {
|
|
42
|
+
groups[key] = [];
|
|
43
|
+
}
|
|
44
|
+
groups[key].push(msg);
|
|
45
|
+
}
|
|
46
|
+
return groups;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Aggregate messages using various aggregation functions
|
|
51
|
+
* @param {Object} config - Aggregation configuration
|
|
52
|
+
* @param {boolean} config.count - Count messages
|
|
53
|
+
* @param {string[]} config.sum - Array of paths to sum
|
|
54
|
+
* @param {string[]} config.avg - Array of paths to average
|
|
55
|
+
* @param {string[]} config.min - Array of paths to find minimum
|
|
56
|
+
* @param {string[]} config.max - Array of paths to find maximum
|
|
57
|
+
* @returns {Object} Aggregation results
|
|
58
|
+
*/
|
|
59
|
+
aggregate(config = {}) {
|
|
60
|
+
const results = {};
|
|
61
|
+
|
|
62
|
+
// Count
|
|
63
|
+
if (config.count) {
|
|
64
|
+
results.count = this.messages.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Sum
|
|
68
|
+
if (config.sum) {
|
|
69
|
+
results.sum = {};
|
|
70
|
+
for (const path of config.sum) {
|
|
71
|
+
results.sum[path] = this.messages.reduce((total, msg) => {
|
|
72
|
+
const val = getPath(msg, path);
|
|
73
|
+
return total + (typeof val === 'number' ? val : 0);
|
|
74
|
+
}, 0);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Average
|
|
79
|
+
if (config.avg) {
|
|
80
|
+
results.avg = {};
|
|
81
|
+
for (const path of config.avg) {
|
|
82
|
+
const sum = this.messages.reduce((total, msg) => {
|
|
83
|
+
const val = getPath(msg, path);
|
|
84
|
+
return total + (typeof val === 'number' ? val : 0);
|
|
85
|
+
}, 0);
|
|
86
|
+
results.avg[path] = this.messages.length > 0 ? sum / this.messages.length : 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Min
|
|
91
|
+
if (config.min) {
|
|
92
|
+
results.min = {};
|
|
93
|
+
for (const path of config.min) {
|
|
94
|
+
const values = this.messages
|
|
95
|
+
.map(msg => getPath(msg, path))
|
|
96
|
+
.filter(val => typeof val === 'number');
|
|
97
|
+
results.min[path] = values.length > 0 ? Math.min(...values) : null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Max
|
|
102
|
+
if (config.max) {
|
|
103
|
+
results.max = {};
|
|
104
|
+
for (const path of config.max) {
|
|
105
|
+
const values = this.messages
|
|
106
|
+
.map(msg => getPath(msg, path))
|
|
107
|
+
.filter(val => typeof val === 'number');
|
|
108
|
+
results.max[path] = values.length > 0 ? Math.max(...values) : null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Reset working messages to the original frozen set
|
|
117
|
+
* @returns {Window} this for chaining
|
|
118
|
+
*/
|
|
119
|
+
reset() {
|
|
120
|
+
this.messages = [...this.allMessages];
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get count of messages in working set
|
|
126
|
+
* @returns {number}
|
|
127
|
+
*/
|
|
128
|
+
size() {
|
|
129
|
+
return this.messages.length;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get count of original messages
|
|
134
|
+
* @returns {number}
|
|
135
|
+
*/
|
|
136
|
+
originalSize() {
|
|
137
|
+
return this.allMessages.length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Maintenance Mode Test
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Comprehensive test for the maintenance mode feature with file buffer.
|
|
6
|
+
|
|
7
|
+
## Test Scenario
|
|
8
|
+
|
|
9
|
+
1. **Start consumer** - Begins consuming messages from queue
|
|
10
|
+
2. **Start producer** - Produces 10 messages/second
|
|
11
|
+
3. **Enable maintenance** - Triggers maintenance mode via API
|
|
12
|
+
4. **Verify buffering** - Consumer stops receiving (messages go to file buffer)
|
|
13
|
+
5. **Continue producing** - Producer keeps running for 10 seconds during maintenance
|
|
14
|
+
6. **Disable maintenance** - Deactivates maintenance mode via API
|
|
15
|
+
7. **Verify drain** - Consumer resumes receiving messages
|
|
16
|
+
8. **Stop producer** - Halts message production
|
|
17
|
+
9. **Wait for completion** - Waits 10 seconds for file buffer to drain
|
|
18
|
+
10. **Verify counts** - Ensures all produced messages were received
|
|
19
|
+
|
|
20
|
+
## Running the Test
|
|
21
|
+
|
|
22
|
+
### Standalone
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd /Users/alice/Work/queen/client-js/test-v2
|
|
26
|
+
node maintenance.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Via Test Runner
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Run just this test
|
|
33
|
+
node run.js test_maintenance_mode
|
|
34
|
+
|
|
35
|
+
# Run all human tests (includes this one)
|
|
36
|
+
node run.js human
|
|
37
|
+
|
|
38
|
+
# Run all tests
|
|
39
|
+
node run.js
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Expected Output
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
๐งช Testing Maintenance Mode with File Buffer
|
|
46
|
+
|
|
47
|
+
๐ Step 1: Configuring queue "test-maintenance-queue"...
|
|
48
|
+
โ
Queue configured
|
|
49
|
+
|
|
50
|
+
๐ฅ Step 2: Starting consumer...
|
|
51
|
+
โ
Consumer started
|
|
52
|
+
|
|
53
|
+
๐ค Step 3: Starting producer (10 msgs/sec)...
|
|
54
|
+
โ
Producer started
|
|
55
|
+
|
|
56
|
+
โฑ๏ธ Step 4: Waiting 3 seconds for normal message flow...
|
|
57
|
+
๐จ Received 10 messages (total: 10)
|
|
58
|
+
๐จ Received 10 messages (total: 20)
|
|
59
|
+
๐จ Received 10 messages (total: 30)
|
|
60
|
+
Produced: 30, Received: 30
|
|
61
|
+
|
|
62
|
+
๐ง Step 5: Enabling MAINTENANCE MODE...
|
|
63
|
+
Response: { maintenanceMode: true, bufferedMessages: 0, ... }
|
|
64
|
+
โ
Maintenance mode enabled
|
|
65
|
+
|
|
66
|
+
โฑ๏ธ Step 6: Waiting 2 seconds - consumer should stop receiving...
|
|
67
|
+
Messages received during maintenance: 0
|
|
68
|
+
Total: Produced=50, Received=30
|
|
69
|
+
|
|
70
|
+
โฑ๏ธ Step 7: Producing during maintenance for 10 seconds...
|
|
71
|
+
(Messages should go to file buffer)
|
|
72
|
+
|
|
73
|
+
Maintenance period complete:
|
|
74
|
+
- Total produced: 150
|
|
75
|
+
- Total received: 30
|
|
76
|
+
- Buffered (should be ~100): 120
|
|
77
|
+
|
|
78
|
+
๐ Step 8: Checking maintenance status...
|
|
79
|
+
Status: { maintenanceMode: true, bufferedMessages: 120, ... }
|
|
80
|
+
|
|
81
|
+
โ
Step 9: Disabling MAINTENANCE MODE...
|
|
82
|
+
Response: { maintenanceMode: false, bufferedMessages: 120, ... }
|
|
83
|
+
(File buffer should start draining to database)
|
|
84
|
+
|
|
85
|
+
โฑ๏ธ Step 10: Waiting for messages to resume...
|
|
86
|
+
๐จ Received 10 messages (total: 40)
|
|
87
|
+
Messages received after resuming: 10
|
|
88
|
+
โ
Messages are flowing again!
|
|
89
|
+
|
|
90
|
+
๐ Step 11: Stopping producer...
|
|
91
|
+
Final produced count: 150
|
|
92
|
+
|
|
93
|
+
โฑ๏ธ Step 12: Waiting 10 seconds for file buffer to drain...
|
|
94
|
+
1s - Received: 50/150 (33.3%)
|
|
95
|
+
๐จ Received 10 messages (total: 60)
|
|
96
|
+
2s - Received: 70/150 (46.7%)
|
|
97
|
+
๐จ Received 10 messages (total: 80)
|
|
98
|
+
3s - Received: 90/150 (60.0%)
|
|
99
|
+
๐จ Received 10 messages (total: 100)
|
|
100
|
+
4s - Received: 110/150 (73.3%)
|
|
101
|
+
๐จ Received 10 messages (total: 120)
|
|
102
|
+
5s - Received: 130/150 (86.7%)
|
|
103
|
+
๐จ Received 10 messages (total: 140)
|
|
104
|
+
6s - Received: 150/150 (100.0%)
|
|
105
|
+
โ
All messages received!
|
|
106
|
+
|
|
107
|
+
๐ Step 13: Stopping consumer...
|
|
108
|
+
โ
Consumer stopped
|
|
109
|
+
|
|
110
|
+
๐ Final Verification:
|
|
111
|
+
|
|
112
|
+
Total Produced: 150
|
|
113
|
+
Total Received: 150
|
|
114
|
+
Difference: 0
|
|
115
|
+
|
|
116
|
+
โ
SUCCESS: All messages accounted for!
|
|
117
|
+
Maintenance mode works correctly with file buffer.
|
|
118
|
+
|
|
119
|
+
๐ Queen client closed
|
|
120
|
+
|
|
121
|
+
โ
All tests passed!
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## What It Tests
|
|
125
|
+
|
|
126
|
+
โ
**Maintenance mode activation** - API responds correctly
|
|
127
|
+
โ
**Message buffering** - Messages go to file buffer (consumer stops receiving)
|
|
128
|
+
โ
**Continued production** - Can push messages during maintenance
|
|
129
|
+
โ
**Maintenance mode deactivation** - API responds correctly
|
|
130
|
+
โ
**Automatic drain** - File buffer drains to database
|
|
131
|
+
โ
**Message ordering** - All messages received in order (FIFO)
|
|
132
|
+
โ
**No message loss** - Produced count == Received count
|
|
133
|
+
โ
**Multi-instance support** - Uses database for state (works across restarts)
|
|
134
|
+
|
|
135
|
+
## Success Criteria
|
|
136
|
+
|
|
137
|
+
- All produced messages are eventually received
|
|
138
|
+
- Consumer stops receiving during maintenance
|
|
139
|
+
- Consumer resumes receiving after maintenance is disabled
|
|
140
|
+
- No duplicates or lost messages
|
|
141
|
+
- Drain completes within 10 seconds
|
|
142
|
+
|
|
143
|
+
## Notes
|
|
144
|
+
|
|
145
|
+
- Uses `axios` for direct API calls (maintenance endpoints)
|
|
146
|
+
- Uses Queen client for queue operations (push/pop/ack)
|
|
147
|
+
- Test takes ~25 seconds total (3s warmup + 10s maintenance + 10s drain)
|
|
148
|
+
|