queen-mq 0.2.0 → 0.2.11
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 +165 -769
- package/{src → client-js}/benchmark/consumer.js +5 -3
- package/{src → client-js}/benchmark/producer.js +9 -4
- package/{src → client-js}/client/client.js +272 -6
- 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/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/qos0-tests.js +334 -0
- package/{src → client-js}/test/test-new.js +42 -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/README.md +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
|
|
|
@@ -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 = 10000; // 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
|
/**
|
|
@@ -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;
|
|
@@ -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');
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test written by an human
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { startTest, passTest, failTest, sleep, log } from './utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Test: Pipeline API - Basic Processing
|
|
9
|
+
*/
|
|
10
|
+
export async function testCreateQueue(client) {
|
|
11
|
+
startTest('Create Queue', 'human');
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const queue = 'test-create-queue-hm';
|
|
15
|
+
await client.queue(queue, { leaseTime: 30 });
|
|
16
|
+
passTest('Queue created successfully');
|
|
17
|
+
} catch (error) {
|
|
18
|
+
failTest('Create Queue', error );
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function workerQueueTest(client) {
|
|
23
|
+
startTest('Worker Queue', 'human');
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const queue = 'test-worker-queue-hm';
|
|
27
|
+
const processedQueue = queue + '-processed';
|
|
28
|
+
await client.queueDelete(queue);
|
|
29
|
+
await client.queueDelete(processedQueue);
|
|
30
|
+
await client.queue(queue, { leaseTime: 30 });
|
|
31
|
+
await client.queue(processedQueue, { leaseTime: 30 });
|
|
32
|
+
const pushMessages = async (count) => {
|
|
33
|
+
let messages = [];
|
|
34
|
+
for (let i = 0; i < count; i++) {
|
|
35
|
+
messages.push({ id: i, data: `Message ${i}` });
|
|
36
|
+
}
|
|
37
|
+
await client.push(queue, messages);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
// Test 1
|
|
42
|
+
await pushMessages(10);
|
|
43
|
+
|
|
44
|
+
let counter1 = 0;
|
|
45
|
+
await client
|
|
46
|
+
.pipeline(queue)
|
|
47
|
+
.take(10)
|
|
48
|
+
.process(async (message) => {
|
|
49
|
+
counter1++;
|
|
50
|
+
return { id: message.data.id, data: message.data.data };
|
|
51
|
+
})
|
|
52
|
+
.execute();
|
|
53
|
+
|
|
54
|
+
if (counter1 !== 10) {
|
|
55
|
+
throw new Error('Expected 10 messages to be processed, got ' + counter1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Test 2
|
|
59
|
+
await pushMessages(100);
|
|
60
|
+
|
|
61
|
+
let counter2 = 0;
|
|
62
|
+
await client
|
|
63
|
+
.pipeline(queue)
|
|
64
|
+
.take(10)
|
|
65
|
+
.processBatch(async (messages) => {
|
|
66
|
+
counter2 += messages.length;
|
|
67
|
+
return messages;
|
|
68
|
+
})
|
|
69
|
+
.repeat({ maxIterations: 1, continuous: false })
|
|
70
|
+
.execute();
|
|
71
|
+
|
|
72
|
+
if (counter2 !== 10) {
|
|
73
|
+
throw new Error('Expected 10 messages to be processed, got ' + counter2);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Test 3
|
|
77
|
+
let counter3 = 0;
|
|
78
|
+
await client
|
|
79
|
+
.pipeline(queue)
|
|
80
|
+
.take(10)
|
|
81
|
+
.processBatch(async (messages) => {
|
|
82
|
+
counter3 += messages.length;
|
|
83
|
+
return messages;
|
|
84
|
+
})
|
|
85
|
+
.repeat({ maxIterations: 9, continuous: false })
|
|
86
|
+
.execute();
|
|
87
|
+
|
|
88
|
+
if (counter3 !== 90) {
|
|
89
|
+
throw new Error('Expected 90 messages to be processed, got ' + counter3);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await pushMessages(100);
|
|
93
|
+
|
|
94
|
+
await client
|
|
95
|
+
.pipeline(queue)
|
|
96
|
+
.take(10)
|
|
97
|
+
.processBatch(async (messages) => {
|
|
98
|
+
return messages;
|
|
99
|
+
})
|
|
100
|
+
.atomically((tx, originalMessages, processedMessages) => {
|
|
101
|
+
tx.ack(originalMessages);
|
|
102
|
+
})
|
|
103
|
+
.repeat({ maxIterations: 10, continuous: true })
|
|
104
|
+
.execute();
|
|
105
|
+
|
|
106
|
+
const messagesPopped = await client.takeSingleBatch(queue);
|
|
107
|
+
let counter4 = messagesPopped.length;
|
|
108
|
+
if (counter4 !== 0) {
|
|
109
|
+
throw new Error('Expected 0 message to be processed, got ' + counter4);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Test transaction that works correctly
|
|
113
|
+
await pushMessages(100);
|
|
114
|
+
|
|
115
|
+
await client
|
|
116
|
+
.pipeline(queue)
|
|
117
|
+
.take(10)
|
|
118
|
+
.processBatch(async (messages) => {
|
|
119
|
+
return messages;
|
|
120
|
+
})
|
|
121
|
+
.atomically((tx, originalMessages, processedMessages) => {
|
|
122
|
+
tx.ack(originalMessages);
|
|
123
|
+
tx.push(processedQueue, processedMessages);
|
|
124
|
+
})
|
|
125
|
+
.repeat({ maxIterations: 10, continuous: true })
|
|
126
|
+
.execute();
|
|
127
|
+
|
|
128
|
+
const messagesPopped2 = [];
|
|
129
|
+
for await (const messages of client.takeBatch(processedQueue, { batch: 100 })) {
|
|
130
|
+
messagesPopped2.push(...messages);
|
|
131
|
+
}
|
|
132
|
+
if (messagesPopped2.length !== 100) {
|
|
133
|
+
throw new Error('Expected 100 message to be processed, got ' + counter5);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Test transaction that fails
|
|
137
|
+
|
|
138
|
+
await pushMessages(100);
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await client
|
|
142
|
+
.pipeline(queue)
|
|
143
|
+
.take(10)
|
|
144
|
+
.processBatch(async (messages) => {
|
|
145
|
+
return messages;
|
|
146
|
+
})
|
|
147
|
+
.atomically((tx, originalMessages, processedMessages) => {
|
|
148
|
+
originalMessages.map(x => x.leaseId = 'invalid-lease-id-12345');
|
|
149
|
+
tx.ack(originalMessages);
|
|
150
|
+
tx.push(processedQueue, processedMessages);
|
|
151
|
+
})
|
|
152
|
+
.repeat({ maxIterations: 10, continuous: true })
|
|
153
|
+
.execute();
|
|
154
|
+
failTest('Expected transaction to fail, got ' + error);
|
|
155
|
+
} catch (error) {}
|
|
156
|
+
|
|
157
|
+
passTest('Worker Queue test passed');
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error(error);
|
|
160
|
+
failTest('Worker Queue test failed', error);
|
|
161
|
+
}
|
|
162
|
+
}
|