queen-mq 0.2.0 → 0.2.22
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 +186 -763
- package/{src → client-js}/benchmark/consumer.js +6 -4
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +277 -7
- package/{src → client-js}/client/index.js +1 -4
- package/{src → client-js}/client/utils/http.js +3 -2
- package/{src → client-js}/client/utils/retry.js +7 -1
- package/{src → client-js}/test/README.md +30 -9
- package/{src → client-js}/test/core-tests.js +7 -2
- package/{src → client-js}/test/edge-case-tests.js +2 -1
- package/client-js/test/human.js +162 -0
- package/client-js/test/partition-transaction-tests.js +482 -0
- package/client-js/test/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +55 -0
- package/package.json +8 -17
- package/init-db.js +0 -20
- package/src/cluster-server.js +0 -242
- package/src/config.js +0 -229
- package/src/database/connection.js +0 -129
- package/src/database/poolManager.js +0 -199
- package/src/database/schema-v2.sql +0 -278
- package/src/managers/eventManager.js +0 -59
- package/src/managers/queueManagerOptimized.js +0 -1634
- package/src/managers/resourceCache.js +0 -96
- package/src/managers/systemEventManager.js +0 -132
- package/src/routes/ack.js +0 -26
- package/src/routes/configure.js +0 -46
- package/src/routes/messages.js +0 -368
- package/src/routes/pop.js +0 -69
- package/src/routes/push.js +0 -28
- package/src/routes/resources.js +0 -330
- package/src/routes/status.js +0 -1037
- package/src/server.js +0 -1633
- package/src/test/MIGRATION_ISSUES.md +0 -174
- package/src/test/test.js +0 -4524
- package/src/utils/streaming.js +0 -231
- package/src/webapp-dist/assets/Analytics-DJrE4Rz2.css +0 -1
- package/src/webapp-dist/assets/Analytics-HEMXKmfJ.js +0 -1
- package/src/webapp-dist/assets/ConfirmDialog-YEoCdE7x.js +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-CC5VJywp.css +0 -1
- package/src/webapp-dist/assets/ConsumerGroups-D8Z20HhZ.js +0 -1
- package/src/webapp-dist/assets/Dashboard-BRFOz6it.css +0 -1
- package/src/webapp-dist/assets/Dashboard-Bpx9I32h.js +0 -1
- package/src/webapp-dist/assets/LoadingSpinner-BPttXcbs.js +0 -1
- package/src/webapp-dist/assets/Messages-BOfVHKma.css +0 -1
- package/src/webapp-dist/assets/Messages-EKURCbLl.js +0 -1
- package/src/webapp-dist/assets/QueueDetail-BXXA7xoZ.css +0 -1
- package/src/webapp-dist/assets/QueueDetail-Bmxg0KiA.js +0 -1
- package/src/webapp-dist/assets/Queues-CvrJkrZT.css +0 -1
- package/src/webapp-dist/assets/Queues-yUx9_3-9.js +0 -1
- package/src/webapp-dist/assets/StatusBadge-C1bBtXHh.css +0 -1
- package/src/webapp-dist/assets/StatusBadge-C6N84XmO.js +0 -1
- package/src/webapp-dist/assets/analytics-BapTekLM.js +0 -1
- package/src/webapp-dist/assets/colors-5z6Q_kUr.js +0 -18
- package/src/webapp-dist/assets/index-B7w2bKT6.css +0 -1
- package/src/webapp-dist/assets/index-CUSnwYTH.js +0 -31
- package/src/webapp-dist/assets/messages-CRV6E-xU.js +0 -1
- package/src/webapp-dist/assets/queen-logo-blue.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-cyan.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-indigo.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-orange.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-pink.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-purple.svg +0 -210
- package/src/webapp-dist/assets/queen-logo-rose.svg +0 -239
- package/src/webapp-dist/assets/queen-logo.svg +0 -263
- package/src/webapp-dist/assets/queues-BdUbxfBC.js +0 -1
- package/src/webapp-dist/assets/resources-Bs9U-ceF.js +0 -1
- package/src/webapp-dist/index.html +0 -15
- package/src/websocket/wsServer.js +0 -228
- /package/{src → client-js}/benchmark/consumer_multi.js +0 -0
- /package/{src → client-js}/benchmark/producer_multi.js +0 -0
- /package/{src → client-js}/client/utils/loadBalancer.js +0 -0
- /package/{src → client-js}/services/encryptionService.js +0 -0
- /package/{src → client-js}/services/evictionService.js +0 -0
- /package/{src → client-js}/services/retentionService.js +0 -0
- /package/{src → client-js}/services/startupSync.js +0 -0
- /package/{src → client-js}/test/advanced-client-tests.js +0 -0
- /package/{src → client-js}/test/advanced-pattern-tests.js +0 -0
- /package/{src → client-js}/test/bus-mode-tests.js +0 -0
- /package/{src → client-js}/test/enterprise-tests.js +0 -0
- /package/{src → client-js}/test/partition-locking-tests.js +0 -0
- /package/{src → client-js}/test/utils.js +0 -0
- /package/{src → client-js}/test/window-buffer-test.js +0 -0
- /package/{src → client-js}/utils/logger.js +0 -0
- /package/{src → client-js}/utils/uuid.js +0 -0
|
@@ -3,7 +3,8 @@ import fs from 'fs';
|
|
|
3
3
|
|
|
4
4
|
const QUEUE_NAME = 'benchmark-queue-001';
|
|
5
5
|
const NUMBER_OF_CONSUMERS = 10;
|
|
6
|
-
const
|
|
6
|
+
const STARTING_PARTITION = parseInt(process.argv[2]) || 0;
|
|
7
|
+
const BATCH_SIZE = 10000;
|
|
7
8
|
const CONSUME_MODE = 'partition';
|
|
8
9
|
|
|
9
10
|
// Global metrics tracking
|
|
@@ -21,7 +22,7 @@ const metrics = {
|
|
|
21
22
|
|
|
22
23
|
async function consumer(consumerId, partition) {
|
|
23
24
|
const q = new Queen({
|
|
24
|
-
baseUrls: ['http://localhost:6632'],
|
|
25
|
+
baseUrls: [ 'http://localhost:6632'],
|
|
25
26
|
timeout: 30000
|
|
26
27
|
});
|
|
27
28
|
|
|
@@ -49,7 +50,7 @@ async function consumer(consumerId, partition) {
|
|
|
49
50
|
try {
|
|
50
51
|
for await (const messages of q.takeBatch(target, { //`namespace:benchmark`,
|
|
51
52
|
wait: true,
|
|
52
|
-
timeout:
|
|
53
|
+
timeout: 30000,
|
|
53
54
|
batch: BATCH_SIZE,
|
|
54
55
|
idleTimeout: 5000
|
|
55
56
|
})) {
|
|
@@ -126,8 +127,9 @@ metrics.startTime = Date.now();
|
|
|
126
127
|
|
|
127
128
|
// Launch all consumers
|
|
128
129
|
const consumerPromises = [];
|
|
130
|
+
//
|
|
129
131
|
for (let i = 0; i < NUMBER_OF_CONSUMERS; i++) {
|
|
130
|
-
consumerPromises.push(consumer(i, i));
|
|
132
|
+
consumerPromises.push(consumer(i, i ));
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
// Wait for all consumers to complete
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Queen } from '../client/client.js';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
|
|
4
|
-
const TOTAL_MESSAGES =
|
|
4
|
+
const TOTAL_MESSAGES = 1000000;
|
|
5
5
|
const PARTITIONS = 10;
|
|
6
|
-
const MAX_BATCH_SIZE =
|
|
6
|
+
const MAX_BATCH_SIZE = 100; // Reduced from 10000
|
|
7
7
|
const PUSH_PARALLEL = 10; // Reduced from 10 to avoid lock contention
|
|
8
8
|
const QUEUE_NAME = 'benchmark-queue-001';
|
|
9
9
|
|
|
@@ -12,11 +12,16 @@ const q = new Queen({
|
|
|
12
12
|
timeout: 120000
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
await q.queueDelete(QUEUE_NAME);
|
|
18
|
+
await q.queue(QUEUE_NAME, {
|
|
17
19
|
leaseTime: 10,
|
|
18
20
|
retryLimit: 3
|
|
19
21
|
}, { namespace: 'benchmark' });
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error(e);
|
|
24
|
+
}
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHttpClient, createLoadBalancedHttpClient } from './utils/http.js';
|
|
2
2
|
import { withRetry } from './utils/retry.js';
|
|
3
3
|
import { createLoadBalancer, LoadBalancingStrategy } from './utils/loadBalancer.js';
|
|
4
|
+
import { generateUUID } from '../utils/uuid.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Minimalist Queen Message Queue Client
|
|
@@ -16,6 +17,7 @@ export class Queen {
|
|
|
16
17
|
#http;
|
|
17
18
|
#loadBalancer;
|
|
18
19
|
#connected = false;
|
|
20
|
+
#bufferManager;
|
|
19
21
|
|
|
20
22
|
constructor(config = {}) {
|
|
21
23
|
this.#config = {
|
|
@@ -27,6 +29,7 @@ export class Queen {
|
|
|
27
29
|
retryDelay: 1000,
|
|
28
30
|
...config
|
|
29
31
|
};
|
|
32
|
+
this.#bufferManager = new BufferManager(this);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -146,7 +149,7 @@ export class Queen {
|
|
|
146
149
|
* Push messages to a queue
|
|
147
150
|
* @param {string} address - Queue address (e.g., "myqueue" or "myqueue/partition")
|
|
148
151
|
* @param {Object|Array} payload - Single message or array of messages
|
|
149
|
-
* @param {Object} options - Optional message properties { transactionId, traceId }
|
|
152
|
+
* @param {Object} options - Optional message properties { transactionId, traceId, buffer }
|
|
150
153
|
*/
|
|
151
154
|
async push(address, payload, options = {}) {
|
|
152
155
|
await this.#ensureConnected();
|
|
@@ -167,7 +170,7 @@ export class Queen {
|
|
|
167
170
|
queue,
|
|
168
171
|
partition,
|
|
169
172
|
payload: item._payload || item,
|
|
170
|
-
transactionId: item._transactionId || item.transactionId
|
|
173
|
+
transactionId: item._transactionId || item.transactionId || generateUUID() // Always generate if missing
|
|
171
174
|
};
|
|
172
175
|
|
|
173
176
|
// Include traceId if provided and valid UUID
|
|
@@ -184,7 +187,9 @@ export class Queen {
|
|
|
184
187
|
queue,
|
|
185
188
|
partition,
|
|
186
189
|
payload: item,
|
|
187
|
-
transactionId: options.transactionId ||
|
|
190
|
+
transactionId: options.transactionId ||
|
|
191
|
+
(item && typeof item === 'object' ? item.transactionId : undefined) ||
|
|
192
|
+
generateUUID() // Always generate if missing
|
|
188
193
|
};
|
|
189
194
|
|
|
190
195
|
// Include traceId if provided and valid UUID
|
|
@@ -196,10 +201,26 @@ export class Queen {
|
|
|
196
201
|
return result;
|
|
197
202
|
});
|
|
198
203
|
|
|
204
|
+
// Check if client-side buffering is enabled
|
|
205
|
+
if (options.buffer && typeof options.buffer === 'object') {
|
|
206
|
+
return this.#pushBuffered(address, formattedItems, options.buffer);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Build request body for immediate push
|
|
210
|
+
const requestBody = { items: formattedItems };
|
|
211
|
+
|
|
212
|
+
// Add server-side buffering flag if specified
|
|
213
|
+
if (options.buffer === true || options.qos0 === true) {
|
|
214
|
+
requestBody.qos0 = true; // Server still uses 'qos0' internally
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Legacy support: bufferMs/bufferMax are aliases for buffer
|
|
218
|
+
if (options.bufferMs !== undefined || options.bufferMax !== undefined) {
|
|
219
|
+
requestBody.qos0 = true;
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
const result = await withRetry(
|
|
200
|
-
() => this.#http.post('/api/v1/push',
|
|
201
|
-
items: formattedItems
|
|
202
|
-
}),
|
|
223
|
+
() => this.#http.post('/api/v1/push', requestBody),
|
|
203
224
|
this.#config.retryAttempts,
|
|
204
225
|
this.#config.retryDelay
|
|
205
226
|
);
|
|
@@ -211,6 +232,25 @@ export class Queen {
|
|
|
211
232
|
return result;
|
|
212
233
|
}
|
|
213
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Push messages using client-side buffering
|
|
237
|
+
* @private
|
|
238
|
+
*/
|
|
239
|
+
#pushBuffered(address, formattedItems, bufferOptions) {
|
|
240
|
+
// Validate buffer options
|
|
241
|
+
if (!bufferOptions.size || !bufferOptions.time) {
|
|
242
|
+
throw new Error('Buffer options must include both size and time');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Add each formatted message to the buffer
|
|
246
|
+
for (const item of formattedItems) {
|
|
247
|
+
this.#bufferManager.addMessage(address, item, bufferOptions);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Return immediately (non-blocking)
|
|
251
|
+
return { buffered: true, count: formattedItems.length };
|
|
252
|
+
}
|
|
253
|
+
|
|
214
254
|
/**
|
|
215
255
|
* Internal method that yields batches of messages (arrays)
|
|
216
256
|
* @private
|
|
@@ -266,6 +306,9 @@ export class Queen {
|
|
|
266
306
|
if (subscriptionMode) params.append('subscriptionMode', subscriptionMode);
|
|
267
307
|
if (subscriptionFrom) params.append('subscriptionFrom', subscriptionFrom);
|
|
268
308
|
|
|
309
|
+
// Add auto-ack option if specified
|
|
310
|
+
if (options.autoAck) params.append('autoAck', 'true');
|
|
311
|
+
|
|
269
312
|
// Determine the endpoint based on parameters
|
|
270
313
|
if (queue) {
|
|
271
314
|
if (partition && partition !== 'Default') {
|
|
@@ -668,10 +711,45 @@ export class Queen {
|
|
|
668
711
|
return result;
|
|
669
712
|
}
|
|
670
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Manually flush a specific buffer
|
|
716
|
+
* @param {string} address - Queue address to flush
|
|
717
|
+
* @returns {Promise<void>}
|
|
718
|
+
*/
|
|
719
|
+
async flushBuffer(address) {
|
|
720
|
+
return this.#bufferManager.flushBuffer(address);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Manually flush all buffers
|
|
725
|
+
* @returns {Promise<void>}
|
|
726
|
+
*/
|
|
727
|
+
async flushAllBuffers() {
|
|
728
|
+
return this.#bufferManager.flushAllBuffers();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Get buffer statistics
|
|
733
|
+
* @returns {Object} Buffer statistics
|
|
734
|
+
*/
|
|
735
|
+
getBufferStats() {
|
|
736
|
+
return this.#bufferManager.getStats();
|
|
737
|
+
}
|
|
738
|
+
|
|
671
739
|
/**
|
|
672
740
|
* Close the client connection
|
|
673
741
|
*/
|
|
674
742
|
async close() {
|
|
743
|
+
// Flush all buffers before closing
|
|
744
|
+
try {
|
|
745
|
+
await this.#bufferManager.flushAllBuffers();
|
|
746
|
+
} catch (error) {
|
|
747
|
+
console.warn('Error flushing buffers during close:', error);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Cleanup buffer manager
|
|
751
|
+
this.#bufferManager.cleanup();
|
|
752
|
+
|
|
675
753
|
this.#connected = false;
|
|
676
754
|
this.#http = null;
|
|
677
755
|
this.#loadBalancer = null;
|
|
@@ -702,6 +780,194 @@ export class Queen {
|
|
|
702
780
|
get _http() {
|
|
703
781
|
return this.#http;
|
|
704
782
|
}
|
|
783
|
+
|
|
784
|
+
get _config() {
|
|
785
|
+
return this.#config;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Message Buffer for client-side buffering
|
|
791
|
+
*/
|
|
792
|
+
class MessageBuffer {
|
|
793
|
+
constructor(address, options, flushCallback) {
|
|
794
|
+
this.address = address;
|
|
795
|
+
this.messages = [];
|
|
796
|
+
this.options = options;
|
|
797
|
+
this.flushCallback = flushCallback;
|
|
798
|
+
this.timer = null;
|
|
799
|
+
this.firstMessageTime = null;
|
|
800
|
+
this.flushing = false;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
add(formattedMessage) {
|
|
804
|
+
// Set first message time if this is the first message
|
|
805
|
+
if (this.messages.length === 0) {
|
|
806
|
+
this.firstMessageTime = Date.now();
|
|
807
|
+
this.#startTimer();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
this.messages.push(formattedMessage);
|
|
811
|
+
|
|
812
|
+
// Check if we should flush based on size
|
|
813
|
+
if (this.messages.length >= this.options.size) {
|
|
814
|
+
this.#triggerFlush();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
#startTimer() {
|
|
819
|
+
if (this.timer) return; // Timer already running
|
|
820
|
+
|
|
821
|
+
this.timer = setTimeout(() => {
|
|
822
|
+
this.#triggerFlush();
|
|
823
|
+
}, this.options.time);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
#triggerFlush() {
|
|
827
|
+
if (this.flushing || this.messages.length === 0) return;
|
|
828
|
+
|
|
829
|
+
// Clear timer
|
|
830
|
+
if (this.timer) {
|
|
831
|
+
clearTimeout(this.timer);
|
|
832
|
+
this.timer = null;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Trigger flush via callback
|
|
836
|
+
this.flushCallback(this.address);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
extractMessages() {
|
|
840
|
+
const messages = [...this.messages];
|
|
841
|
+
this.messages = [];
|
|
842
|
+
this.firstMessageTime = null;
|
|
843
|
+
this.flushing = false;
|
|
844
|
+
|
|
845
|
+
if (this.timer) {
|
|
846
|
+
clearTimeout(this.timer);
|
|
847
|
+
this.timer = null;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return messages;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
cleanup() {
|
|
854
|
+
if (this.timer) {
|
|
855
|
+
clearTimeout(this.timer);
|
|
856
|
+
this.timer = null;
|
|
857
|
+
}
|
|
858
|
+
this.messages = [];
|
|
859
|
+
this.firstMessageTime = null;
|
|
860
|
+
this.flushing = false;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Buffer Manager for client-side message buffering
|
|
866
|
+
*/
|
|
867
|
+
class BufferManager {
|
|
868
|
+
constructor(client) {
|
|
869
|
+
this.client = client;
|
|
870
|
+
this.buffers = new Map(); // address -> MessageBuffer
|
|
871
|
+
this.flushCount = 0;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
addMessage(address, formattedMessage, bufferOptions) {
|
|
875
|
+
const bufferKey = address;
|
|
876
|
+
|
|
877
|
+
if (!this.buffers.has(bufferKey)) {
|
|
878
|
+
this.buffers.set(bufferKey, new MessageBuffer(
|
|
879
|
+
address,
|
|
880
|
+
bufferOptions,
|
|
881
|
+
(addr) => this.#flushBuffer(addr)
|
|
882
|
+
));
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const buffer = this.buffers.get(bufferKey);
|
|
886
|
+
buffer.add(formattedMessage);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async #flushBuffer(address) {
|
|
890
|
+
const buffer = this.buffers.get(address);
|
|
891
|
+
if (!buffer || buffer.flushing || buffer.messages.length === 0) {
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
buffer.flushing = true;
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
const messages = buffer.extractMessages();
|
|
899
|
+
|
|
900
|
+
if (messages.length === 0) return;
|
|
901
|
+
|
|
902
|
+
// Call the original push logic with the buffered messages
|
|
903
|
+
const requestBody = { items: messages };
|
|
904
|
+
|
|
905
|
+
await withRetry(
|
|
906
|
+
() => this.client._http.post('/api/v1/push', requestBody),
|
|
907
|
+
this.client._config.retryAttempts,
|
|
908
|
+
this.client._config.retryDelay
|
|
909
|
+
);
|
|
910
|
+
|
|
911
|
+
this.flushCount++;
|
|
912
|
+
|
|
913
|
+
// Call onFlush callback if provided
|
|
914
|
+
if (buffer.options.onFlush) {
|
|
915
|
+
buffer.options.onFlush(address, messages.length);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Remove empty buffer
|
|
919
|
+
this.buffers.delete(address);
|
|
920
|
+
|
|
921
|
+
} catch (error) {
|
|
922
|
+
buffer.flushing = false;
|
|
923
|
+
|
|
924
|
+
// Call onError callback if provided
|
|
925
|
+
if (buffer.options.onError) {
|
|
926
|
+
buffer.options.onError(address, error);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
throw error;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
async flushBuffer(address) {
|
|
934
|
+
return this.#flushBuffer(address);
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
async flushAllBuffers() {
|
|
938
|
+
const flushPromises = [];
|
|
939
|
+
for (const address of this.buffers.keys()) {
|
|
940
|
+
flushPromises.push(this.#flushBuffer(address));
|
|
941
|
+
}
|
|
942
|
+
await Promise.all(flushPromises);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
getStats() {
|
|
946
|
+
let totalBufferedMessages = 0;
|
|
947
|
+
let oldestBufferAge = 0;
|
|
948
|
+
|
|
949
|
+
for (const buffer of this.buffers.values()) {
|
|
950
|
+
totalBufferedMessages += buffer.messages.length;
|
|
951
|
+
if (buffer.firstMessageTime) {
|
|
952
|
+
const age = Date.now() - buffer.firstMessageTime;
|
|
953
|
+
oldestBufferAge = Math.max(oldestBufferAge, age);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
activeBuffers: this.buffers.size,
|
|
959
|
+
totalBufferedMessages,
|
|
960
|
+
oldestBufferAge,
|
|
961
|
+
flushesPerformed: this.flushCount
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
cleanup() {
|
|
966
|
+
for (const buffer of this.buffers.values()) {
|
|
967
|
+
buffer.cleanup();
|
|
968
|
+
}
|
|
969
|
+
this.buffers.clear();
|
|
970
|
+
}
|
|
705
971
|
}
|
|
706
972
|
|
|
707
973
|
/**
|
|
@@ -978,7 +1244,11 @@ class PipelineBuilder {
|
|
|
978
1244
|
await tx.commit();
|
|
979
1245
|
} else {
|
|
980
1246
|
// Default: just ACK the messages
|
|
981
|
-
|
|
1247
|
+
// Extract consumer group from queue address if present (format: queue@group)
|
|
1248
|
+
const atIndex = this.#queue.lastIndexOf('@');
|
|
1249
|
+
const consumerGroup = atIndex > 0 ? this.#queue.substring(atIndex + 1) : null;
|
|
1250
|
+
const context = consumerGroup ? { group: consumerGroup } : {};
|
|
1251
|
+
await this.#client.ack(messages, true, context);
|
|
982
1252
|
}
|
|
983
1253
|
|
|
984
1254
|
return {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const httpRequest = async (url, options = {}) => {
|
|
2
|
+
//console.log('httpRequest', url, options);
|
|
2
3
|
const response = await fetch(url, {
|
|
3
4
|
...options,
|
|
4
5
|
headers: {
|
|
@@ -6,7 +7,7 @@ export const httpRequest = async (url, options = {}) => {
|
|
|
6
7
|
...options.headers
|
|
7
8
|
}
|
|
8
9
|
});
|
|
9
|
-
|
|
10
|
+
//console.log('response', response);
|
|
10
11
|
// Handle 204 No Content
|
|
11
12
|
if (response.status === 204) {
|
|
12
13
|
return null; // No content
|
|
@@ -143,7 +144,7 @@ export const createLoadBalancedHttpClient = ({ baseUrls, loadBalancer, timeout =
|
|
|
143
144
|
return await executeRequest(url);
|
|
144
145
|
} catch (error) {
|
|
145
146
|
lastError = error;
|
|
146
|
-
console.warn(`Request failed for ${url}: ${error.message}`);
|
|
147
|
+
console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`);
|
|
147
148
|
|
|
148
149
|
// Don't retry on client errors (4xx)
|
|
149
150
|
if (error.status && error.status >= 400 && error.status < 500) {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
const config = {
|
|
2
|
+
CLIENT: {
|
|
3
|
+
DEFAULT_RETRY_ATTEMPTS: 3,
|
|
4
|
+
DEFAULT_RETRY_DELAY: 1000,
|
|
5
|
+
DEFAULT_RETRY_BACKOFF: 2
|
|
6
|
+
}
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export const withRetry = async (fn, attempts = config.CLIENT.DEFAULT_RETRY_ATTEMPTS, delay = config.CLIENT.DEFAULT_RETRY_DELAY, backoff = config.CLIENT.DEFAULT_RETRY_BACKOFF) => {
|
|
4
10
|
let lastError;
|
|
@@ -40,7 +40,7 @@ The test suite is organized into focused, modular files:
|
|
|
40
40
|
- Subscription modes (all vs new messages)
|
|
41
41
|
- Consumer group isolation
|
|
42
42
|
|
|
43
|
-
#### 5. Edge Cases (`edge-case-tests.js`)
|
|
43
|
+
#### 5. Edge Cases (`edge-case-tests.js` & `partition-transaction-tests.js`)
|
|
44
44
|
- Empty and null payloads
|
|
45
45
|
- Very large payloads
|
|
46
46
|
- Concurrent push/take operations
|
|
@@ -48,6 +48,11 @@ The test suite is organized into focused, modular files:
|
|
|
48
48
|
- Lease expiration and redelivery
|
|
49
49
|
- SQL injection prevention
|
|
50
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
|
|
51
56
|
|
|
52
57
|
#### 6. Advanced Patterns (`advanced-pattern-tests.js`)
|
|
53
58
|
- Multi-stage pipeline workflow
|
|
@@ -138,20 +143,36 @@ for await (const msg of client.take('myqueue/partition@group', { limit: 10 })) {
|
|
|
138
143
|
cd /Users/alice/Work/queen
|
|
139
144
|
|
|
140
145
|
# Run all tests
|
|
141
|
-
nvm use 22 && node
|
|
146
|
+
nvm use 22 && node client-js/test/test-new.js
|
|
142
147
|
|
|
143
148
|
# Run specific test category
|
|
144
|
-
node
|
|
145
|
-
node
|
|
146
|
-
node
|
|
147
|
-
node
|
|
148
|
-
node
|
|
149
|
-
node
|
|
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
|
|
150
155
|
|
|
151
156
|
# Show help
|
|
152
|
-
node
|
|
157
|
+
node client-js/test/test-new.js help
|
|
153
158
|
```
|
|
154
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
|
+
|
|
155
176
|
### Available Test Categories
|
|
156
177
|
|
|
157
178
|
- **`core`** - Core features (queue creation, push, take, ack, delayed processing, FIFO)
|
|
@@ -113,19 +113,23 @@ export async function testBatchMessagePush(client) {
|
|
|
113
113
|
|
|
114
114
|
// Push batch
|
|
115
115
|
await client.push(`${queue}/batch-partition`, messages);
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
// Verify messages were stored
|
|
118
118
|
const count = await getMessageCount(queue, 'batch-partition');
|
|
119
|
+
|
|
119
120
|
if (count !== batchSize) {
|
|
120
121
|
throw new Error(`Expected ${batchSize} messages in database, got ${count}`);
|
|
121
122
|
}
|
|
122
|
-
|
|
123
|
+
|
|
123
124
|
// Take all messages
|
|
124
125
|
const received = [];
|
|
125
126
|
for await (const msg of client.take(`${queue}/batch-partition`, { limit: batchSize })) {
|
|
126
127
|
received.push(msg);
|
|
128
|
+
|
|
127
129
|
await client.ack(msg);
|
|
128
130
|
}
|
|
131
|
+
|
|
132
|
+
|
|
129
133
|
|
|
130
134
|
if (received.length !== batchSize) {
|
|
131
135
|
throw new Error(`Expected ${batchSize} messages, got ${received.length}`);
|
|
@@ -233,6 +237,7 @@ export async function testDelayedProcessing(client) {
|
|
|
233
237
|
await client.queue(queue, {
|
|
234
238
|
delayedProcessing: 2 // 2 seconds delay
|
|
235
239
|
});
|
|
240
|
+
|
|
236
241
|
|
|
237
242
|
const startTime = Date.now();
|
|
238
243
|
|
|
@@ -105,10 +105,11 @@ export async function testVeryLargePayloads(client) {
|
|
|
105
105
|
|
|
106
106
|
// Take and verify
|
|
107
107
|
let received = null;
|
|
108
|
-
for await (const msg of client.take(queue, { limit: 1 })) {
|
|
108
|
+
for await (const msg of client.take(queue, { limit: 1, timeout: 20000 })) {
|
|
109
109
|
received = msg.data;
|
|
110
110
|
await client.ack(msg);
|
|
111
111
|
}
|
|
112
|
+
|
|
112
113
|
|
|
113
114
|
if (!received || !received.array || received.array.length !== 10000) {
|
|
114
115
|
throw new Error('Large payload corrupted');
|