queen-mq 0.6.4 → 0.12.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.
@@ -11,6 +11,7 @@ export class HttpClient {
11
11
  #retryAttempts
12
12
  #retryDelayMillis
13
13
  #enableFailover
14
+ #bearerToken
14
15
 
15
16
  constructor(options = {}) {
16
17
  const {
@@ -19,7 +20,8 @@ export class HttpClient {
19
20
  timeoutMillis = 30000,
20
21
  retryAttempts = 3,
21
22
  retryDelayMillis = 1000,
22
- enableFailover = true
23
+ enableFailover = true,
24
+ bearerToken = null
23
25
  } = options
24
26
 
25
27
  this.#baseUrl = baseUrl
@@ -28,13 +30,15 @@ export class HttpClient {
28
30
  this.#retryAttempts = retryAttempts
29
31
  this.#retryDelayMillis = retryDelayMillis
30
32
  this.#enableFailover = enableFailover
33
+ this.#bearerToken = bearerToken
31
34
 
32
35
  logger.log('HttpClient.constructor', {
33
36
  hasLoadBalancer: !!loadBalancer,
34
37
  baseUrl: baseUrl || 'load-balanced',
35
38
  timeoutMillis,
36
39
  retryAttempts,
37
- enableFailover
40
+ enableFailover,
41
+ hasAuth: !!bearerToken
38
42
  })
39
43
  }
40
44
 
@@ -46,10 +50,15 @@ export class HttpClient {
46
50
  const timeoutId = setTimeout(() => controller.abort(), effectiveTimeout)
47
51
 
48
52
  try {
53
+ const headers = { 'Content-Type': 'application/json' }
54
+ if (this.#bearerToken) {
55
+ headers['Authorization'] = `Bearer ${this.#bearerToken}`
56
+ }
57
+
49
58
  const options = {
50
59
  method,
51
60
  signal: controller.signal,
52
- headers: { 'Content-Type': 'application/json' }
61
+ headers
53
62
  }
54
63
 
55
64
  if (body) {
@@ -145,7 +154,7 @@ export class HttpClient {
145
154
  throw lastError
146
155
  }
147
156
 
148
- async #requestWithFailover(method, path, body = null, requestTimeoutMillis = null) {
157
+ async #requestWithFailover(method, path, body = null, requestTimeoutMillis = null, affinityKey = null) {
149
158
  if (!this.#loadBalancer || !this.#enableFailover) {
150
159
  return this.#requestWithRetry(method, path, body, requestTimeoutMillis)
151
160
  }
@@ -154,10 +163,11 @@ export class HttpClient {
154
163
  const attemptedUrls = new Set()
155
164
  let lastError = null
156
165
 
157
- logger.log('HttpClient.failover', { method, path, totalServers: urls.length })
166
+ logger.log('HttpClient.failover', { method, path, totalServers: urls.length, affinityKey })
158
167
 
159
168
  for (let i = 0; i < urls.length; i++) {
160
- const url = this.#loadBalancer.getNextUrl()
169
+ // Pass affinity key to load balancer for consistent routing
170
+ const url = this.#loadBalancer.getNextUrl(affinityKey)
161
171
 
162
172
  if (attemptedUrls.has(url)) {
163
173
  continue
@@ -166,9 +176,20 @@ export class HttpClient {
166
176
  attemptedUrls.add(url)
167
177
 
168
178
  try {
169
- return await this.#executeRequest(url + path, method, body, requestTimeoutMillis)
179
+ const result = await this.#executeRequest(url + path, method, body, requestTimeoutMillis)
180
+
181
+ // Mark backend as healthy on success
182
+ this.#loadBalancer.markHealthy(url)
183
+
184
+ return result
170
185
  } catch (error) {
171
186
  lastError = error
187
+
188
+ // Mark backend as unhealthy on failure (5xx or network errors)
189
+ if (!error.status || error.status >= 500) {
190
+ this.#loadBalancer.markUnhealthy(url)
191
+ }
192
+
172
193
  logger.warn('HttpClient.failover', { url, method, path, error: error.message })
173
194
  console.warn(`Request failed for ${url}: ${method} ${path} - ${error.message}`)
174
195
 
@@ -192,20 +213,20 @@ export class HttpClient {
192
213
  return this.#baseUrl
193
214
  }
194
215
 
195
- async get(path, requestTimeoutMillis = null) {
196
- return this.#requestWithFailover('GET', path, null, requestTimeoutMillis)
216
+ async get(path, requestTimeoutMillis = null, affinityKey = null) {
217
+ return this.#requestWithFailover('GET', path, null, requestTimeoutMillis, affinityKey)
197
218
  }
198
219
 
199
- async post(path, body = null, requestTimeoutMillis = null) {
200
- return this.#requestWithFailover('POST', path, body, requestTimeoutMillis)
220
+ async post(path, body = null, requestTimeoutMillis = null, affinityKey = null) {
221
+ return this.#requestWithFailover('POST', path, body, requestTimeoutMillis, affinityKey)
201
222
  }
202
223
 
203
- async put(path, body = null, requestTimeoutMillis = null) {
204
- return this.#requestWithFailover('PUT', path, body, requestTimeoutMillis)
224
+ async put(path, body = null, requestTimeoutMillis = null, affinityKey = null) {
225
+ return this.#requestWithFailover('PUT', path, body, requestTimeoutMillis, affinityKey)
205
226
  }
206
227
 
207
- async delete(path, requestTimeoutMillis = null) {
208
- return this.#requestWithFailover('DELETE', path, null, requestTimeoutMillis)
228
+ async delete(path, requestTimeoutMillis = null, affinityKey = null) {
229
+ return this.#requestWithFailover('DELETE', path, null, requestTimeoutMillis, affinityKey)
209
230
  }
210
231
 
211
232
  getLoadBalancer() {
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Load balancer for distributing requests across multiple servers
3
+ * Supports round-robin, session, and affinity-based routing with virtual nodes
3
4
  */
4
5
 
5
6
  export class LoadBalancer {
@@ -8,32 +9,245 @@ export class LoadBalancer {
8
9
  #currentIndex = 0
9
10
  #sessionMap = new Map()
10
11
  #sessionId
12
+ #healthStatus
13
+ #virtualNodes = null
14
+ #affinityHashRing
15
+ #healthRetryAfterMillis
11
16
 
12
- constructor(urls, strategy = 'round-robin') {
17
+ constructor(urls, strategy = 'round-robin', options = {}) {
13
18
  this.#urls = urls.map(url => url.replace(/\/$/, '')) // Remove trailing slashes
14
19
  this.#strategy = strategy
15
20
  this.#sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
21
+
22
+ // Virtual node replicas for affinity strategy (default: 150)
23
+ this.#affinityHashRing = options.affinityHashRing || 150
24
+
25
+ // Health retry interval (default: 30 seconds)
26
+ this.#healthRetryAfterMillis = options.healthRetryAfterMillis || 30000
27
+
28
+ // Initialize health status with last failure time
29
+ this.#healthStatus = new Map(this.#urls.map(url => [url, {
30
+ healthy: true,
31
+ failures: 0,
32
+ lastFailure: null
33
+ }]))
34
+
35
+ // Build virtual node ring for affinity strategy
36
+ if (strategy === 'affinity') {
37
+ this.#buildVirtualNodeRing()
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Build virtual node ring for consistent hashing
43
+ * Each real server gets multiple virtual positions in the hash space
44
+ */
45
+ #buildVirtualNodeRing() {
46
+ this.#virtualNodes = []
47
+
48
+ for (const url of this.#urls) {
49
+ for (let i = 0; i < this.#affinityHashRing; i++) {
50
+ // Create virtual node identifier
51
+ const vnodeKey = `${url}#vnode${i}`
52
+ const hash = this.#hashString(vnodeKey)
53
+
54
+ this.#virtualNodes.push({
55
+ hash,
56
+ realServer: url
57
+ })
58
+ }
59
+ }
60
+
61
+ // Sort by hash value to create the ring
62
+ this.#virtualNodes.sort((a, b) => a.hash - b.hash)
63
+ }
64
+
65
+ /**
66
+ * Rebuild virtual node ring with specified URLs
67
+ * @param {Array<string>} urls - URLs to include in the ring (if not provided, uses all healthy URLs)
68
+ */
69
+ #rebuildVirtualNodeRing(urls = null) {
70
+ const urlsToInclude = urls || this.#urls.filter(url => this.#healthStatus.get(url)?.healthy !== false)
71
+
72
+ this.#virtualNodes = []
73
+
74
+ for (const url of urlsToInclude) {
75
+ for (let i = 0; i < this.#affinityHashRing; i++) {
76
+ const vnodeKey = `${url}#vnode${i}`
77
+ const hash = this.#hashString(vnodeKey)
78
+
79
+ this.#virtualNodes.push({
80
+ hash,
81
+ realServer: url
82
+ })
83
+ }
84
+ }
85
+
86
+ this.#virtualNodes.sort((a, b) => a.hash - b.hash)
16
87
  }
17
88
 
18
89
  getNextUrl(sessionKey = null) {
19
90
  const key = sessionKey || this.#sessionId
91
+
92
+ // Get healthy URLs + unhealthy URLs that are ready for retry
93
+ const now = Date.now()
94
+ let needRingRebuild = false
95
+
96
+ const healthyUrls = this.#urls.filter(url => {
97
+ const status = this.#healthStatus.get(url)
98
+ if (status.healthy) return true
99
+
100
+ // Allow retry of unhealthy backends after configured interval
101
+ if (status.lastFailure && (now - status.lastFailure) >= this.#healthRetryAfterMillis) {
102
+ needRingRebuild = true // Need to rebuild ring to include this server's vnodes
103
+ return true
104
+ }
105
+
106
+ return false
107
+ })
108
+
109
+ // Rebuild vnode ring if we have servers eligible for retry
110
+ // Pass healthyUrls so it includes servers ready for retry
111
+ if (needRingRebuild && this.#virtualNodes) {
112
+ this.#rebuildVirtualNodeRing(healthyUrls)
113
+ }
114
+
115
+ if (healthyUrls.length === 0) {
116
+ // All backends unhealthy, try any backend as fallback
117
+ return this.#urls[this.#currentIndex % this.#urls.length]
118
+ }
20
119
 
120
+ if (this.#strategy === 'affinity') {
121
+ // Virtual node consistent hashing
122
+ return this.#getAffinityUrl(key, healthyUrls)
123
+ }
124
+
21
125
  if (this.#strategy === 'session') {
22
126
  // Session affinity: stick to the same server per session
23
127
  if (!this.#sessionMap.has(key)) {
24
128
  const assignedIndex = this.#currentIndex
25
129
  this.#sessionMap.set(key, assignedIndex)
26
- this.#currentIndex = (this.#currentIndex + 1) % this.#urls.length
130
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
27
131
  }
28
- return this.#urls[this.#sessionMap.get(key)]
132
+
133
+ const assignedUrl = this.#urls[this.#sessionMap.get(key)]
134
+ // If assigned URL is unhealthy, reassign
135
+ if (!healthyUrls.includes(assignedUrl)) {
136
+ const newIndex = this.#currentIndex % healthyUrls.length
137
+ this.#sessionMap.set(key, newIndex)
138
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
139
+ return healthyUrls[newIndex]
140
+ }
141
+
142
+ return assignedUrl
29
143
  }
30
144
 
31
- // Round robin: cycle through URLs
32
- const url = this.#urls[this.#currentIndex]
33
- this.#currentIndex = (this.#currentIndex + 1) % this.#urls.length
145
+ // Round robin: cycle through healthy URLs only
146
+ const url = healthyUrls[this.#currentIndex % healthyUrls.length]
147
+ this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
34
148
  return url
35
149
  }
36
150
 
151
+ /**
152
+ * Affinity-based routing using consistent hashing with virtual nodes
153
+ * Same key always routes to same backend (unless unhealthy)
154
+ */
155
+ #getAffinityUrl(key, healthyUrls) {
156
+ if (!key || !this.#virtualNodes || this.#virtualNodes.length === 0) {
157
+ // No key or no vnodes, fall back to round-robin
158
+ return healthyUrls[this.#currentIndex++ % healthyUrls.length]
159
+ }
160
+
161
+ // Hash the consumer group key
162
+ const keyHash = this.#hashString(key)
163
+
164
+ // Binary search to find the first vnode >= keyHash (clockwise on ring)
165
+ let left = 0
166
+ let right = this.#virtualNodes.length - 1
167
+ let result = 0
168
+
169
+ while (left <= right) {
170
+ const mid = Math.floor((left + right) / 2)
171
+ if (this.#virtualNodes[mid].hash >= keyHash) {
172
+ result = mid
173
+ right = mid - 1
174
+ } else {
175
+ left = mid + 1
176
+ }
177
+ }
178
+
179
+ // Walk forward on the ring to find first healthy server
180
+ const startIdx = result
181
+ for (let i = 0; i < this.#virtualNodes.length; i++) {
182
+ const idx = (startIdx + i) % this.#virtualNodes.length
183
+ const vnode = this.#virtualNodes[idx]
184
+
185
+ if (healthyUrls.includes(vnode.realServer)) {
186
+ return vnode.realServer
187
+ }
188
+ }
189
+
190
+ // Fallback (should never happen if healthyUrls is not empty)
191
+ return healthyUrls[0]
192
+ }
193
+
194
+ /**
195
+ * FNV-1a hash function for consistent hashing
196
+ */
197
+ #hashString(str) {
198
+ let hash = 2166136261 // FNV offset basis
199
+ for (let i = 0; i < str.length; i++) {
200
+ hash ^= str.charCodeAt(i)
201
+ hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24)
202
+ }
203
+ return hash >>> 0 // Convert to unsigned 32-bit integer
204
+ }
205
+
206
+ /**
207
+ * Mark a backend as unhealthy after failure
208
+ */
209
+ markUnhealthy(url) {
210
+ const status = this.#healthStatus.get(url)
211
+ if (status) {
212
+ status.healthy = false
213
+ status.failures++
214
+ status.lastFailure = Date.now()
215
+
216
+ const retryInSeconds = (this.#healthRetryAfterMillis / 1000).toFixed(0)
217
+
218
+ // Rebuild vnode ring to exclude unhealthy server
219
+ if (this.#virtualNodes) {
220
+ this.#rebuildVirtualNodeRing()
221
+ }
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Mark a backend as healthy after success
227
+ */
228
+ markHealthy(url) {
229
+ const status = this.#healthStatus.get(url)
230
+ if (!status) return
231
+
232
+ const wasUnhealthy = status.healthy === false
233
+
234
+ status.healthy = true
235
+ status.failures = 0
236
+ status.lastFailure = null
237
+
238
+ // Rebuild vnode ring to include recovered server
239
+ if (this.#virtualNodes && wasUnhealthy) {
240
+ this.#rebuildVirtualNodeRing()
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Get health status of all backends
246
+ */
247
+ getHealthStatus() {
248
+ return new Map(this.#healthStatus)
249
+ }
250
+
37
251
  getAllUrls() {
38
252
  return [...this.#urls]
39
253
  }
@@ -41,6 +255,13 @@ export class LoadBalancer {
41
255
  getStrategy() {
42
256
  return this.#strategy
43
257
  }
258
+
259
+ /**
260
+ * Get number of virtual nodes in the ring
261
+ */
262
+ getVirtualNodeCount() {
263
+ return this.#virtualNodes ? this.#virtualNodes.length : 0
264
+ }
44
265
 
45
266
  reset() {
46
267
  this.#currentIndex = 0
@@ -3,5 +3,6 @@
3
3
  */
4
4
 
5
5
  export { Queen } from './Queen.js'
6
+ export { Admin } from './admin/Admin.js'
6
7
  export { CLIENT_DEFAULTS, QUEUE_DEFAULTS, CONSUME_DEFAULTS, POP_DEFAULTS, BUFFER_DEFAULTS } from './utils/defaults.js'
7
8
 
@@ -10,8 +10,11 @@ export const CLIENT_DEFAULTS = {
10
10
  timeoutMillis: 30000, // 30 seconds
11
11
  retryAttempts: 3, // 3 retry attempts
12
12
  retryDelayMillis: 1000, // 1 second initial delay (exponential backoff)
13
- loadBalancingStrategy: 'round-robin', // 'round-robin' or 'session'
14
- enableFailover: true // Auto-failover to other servers
13
+ loadBalancingStrategy: 'affinity', // 'round-robin', 'session', or 'affinity'
14
+ affinityHashRing: 128, // Number of virtual nodes per server for affinity strategy
15
+ enableFailover: true, // Auto-failover to other servers
16
+ healthRetryAfterMillis: 5000, // Retry unhealthy backends after 5 seconds
17
+ bearerToken: null // Bearer token for proxy authentication
15
18
  }
16
19
 
17
20
  export const QUEUE_DEFAULTS = {
@@ -1,9 +1,20 @@
1
1
  /**
2
2
  * Logger utility for Queen Client v2
3
- * Controlled by QUEEN_CLIENT_LOG environment variable
3
+ * Controlled by QUEEN_CLIENT_LOG environment variable (Node.js)
4
+ * or window.QUEEN_CLIENT_LOG (Browser)
4
5
  */
5
6
 
6
- const LOG_ENABLED = process.env.QUEEN_CLIENT_LOG === 'true'
7
+ const LOG_ENABLED = (() => {
8
+ // Node.js environment
9
+ if (typeof process !== 'undefined' && process.env) {
10
+ return process.env.QUEEN_CLIENT_LOG === 'true'
11
+ }
12
+ // Browser environment
13
+ if (typeof window !== 'undefined') {
14
+ return window.QUEEN_CLIENT_LOG === true
15
+ }
16
+ return false
17
+ })()
7
18
 
8
19
  /**
9
20
  * Get formatted timestamp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "queen-mq",
3
- "version": "0.6.4",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "description": "High-performance C++ message queue backed by PostgreSQL",
6
6
  "main": "client-v2/index.js",
@@ -8,12 +8,13 @@
8
8
  "test": "node test-v2/run.js human"
9
9
  },
10
10
  "files": [
11
- "client-v2/**/*",
11
+ "client-v2/**/*",
12
12
  "test-v2/**/*",
13
13
  "README.md"
14
14
  ],
15
15
  "dependencies": {
16
16
  "axios": "^1.12.2",
17
+ "pg": "^8.16.3",
17
18
  "uuid": "^13.0.0"
18
19
  },
19
20
  "engines": {
@@ -51,60 +51,6 @@ export async function testConsumerTrace(client) {
51
51
  return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
52
52
  }
53
53
 
54
- export async function testConsumerNamespace(client) {
55
- const queue = await client.queue('test-queue-v2-namespace')
56
- .namespace('test-namespace')
57
- .create()
58
- if (!queue.configured) {
59
- return { success: false, message: 'Queue not created' }
60
- }
61
-
62
- await client
63
- .queue('test-queue-v2-namespace')
64
- .push([{ data: { message: 'Hello, world!' } }])
65
-
66
- let msgToReturn = null
67
-
68
- await client
69
- .queue()
70
- .namespace('test-namespace')
71
- .batch(1)
72
- .limit(1)
73
- .each()
74
- .consume(async msg => {
75
- msgToReturn = msg
76
- })
77
-
78
- return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
79
- }
80
-
81
- export async function testConsumerTask(client) {
82
- const queue = await client.queue('test-queue-v2-task')
83
- .task('test-task')
84
- .create()
85
- if (!queue.configured) {
86
- return { success: false, message: 'Queue not created' }
87
- }
88
-
89
- await client
90
- .queue('test-queue-v2-task')
91
- .push([{ data: { message: 'Hello, world!' } }])
92
-
93
- let msgToReturn = null
94
-
95
- await client
96
- .queue()
97
- .task('test-task')
98
- .batch(1)
99
- .limit(1)
100
- .each()
101
- .consume(async msg => {
102
- msgToReturn = msg
103
- })
104
-
105
- return { success: msgToReturn !== null, message: 'Consumer test completed successfully' }
106
- }
107
-
108
54
  export async function testConsumerWithPartition(client) {
109
55
  const queue = await client.queue('test-queue-v2-consume-with-partition').create()
110
56
  if (!queue.configured) {
@@ -252,6 +198,7 @@ export async function testConsumerOrderingBatch(client) {
252
198
 
253
199
 
254
200
  export async function testConsumerBatchConsumeBatch(client) {
201
+ await client.queue('test-queue-v2-consume-batch').delete()
255
202
  const queue = await client.queue('test-queue-v2-consume-batch').create()
256
203
  if (!queue.configured) {
257
204
  return { success: false, message: 'Queue not created' }
@@ -271,7 +218,7 @@ export async function testConsumerBatchConsumeBatch(client) {
271
218
  .queue('test-queue-v2-consume-batch')
272
219
  .batch(10)
273
220
  .wait(false)
274
- .limit(1)
221
+ .limit(3)
275
222
  .consume(async msgs => {
276
223
  msgLength = msgs.length
277
224
  })
@@ -465,12 +412,13 @@ export async function manualAck(client) {
465
412
 
466
413
  await client
467
414
  .queue('test-queue-v2-manual-ack')
468
- .concurrency(10)
415
+ .concurrency(1)
469
416
  .subscriptionMode('from_beginning')
470
417
  .batch(1000)
471
418
  .wait(false)
472
- .limit(1)
419
+ .limit(10000)
473
420
  .consume(async msgs => {
421
+ console.log('Messages: ' + msgs.length)
474
422
  for (const msg of msgs) {
475
423
  if (lastId === null) {
476
424
  lastId = msg.data.id
@@ -525,6 +473,7 @@ export async function retries(client) {
525
473
  .limit(300) // Allow up to 300 messages (3 batches of 100)
526
474
  .consume(async msgs => {
527
475
  attemptCount++;
476
+ console.log('Attempt: ' + attemptCount)
528
477
  if (attemptCount < 3) {
529
478
  // Fail first 2 attempts, succeed on 3rd
530
479
  throw new Error('Test error - triggering retry')
package/test-v2/load.js CHANGED
@@ -11,7 +11,7 @@ export async function testLoad(client) {
11
11
  let messages = [];
12
12
  for (let i = 0; i < messagesToPush; i++) {
13
13
  messages.push({ data: { id: i } })
14
- if (messages.length >= 10000) {
14
+ if (messages.length >= 100) {
15
15
  await client.queue('test-queue-v2-load').push(messages)
16
16
  messages = [];
17
17
  }
@@ -24,11 +24,10 @@ export async function testLoad(client) {
24
24
  await client
25
25
  .queue('test-queue-v2-load')
26
26
  .concurrency(10)
27
- .batch(10000)
27
+ .batch(100)
28
28
  .wait(false)
29
- .limit(1)
29
+ .limit(10000)
30
30
  .consume(async msgs => {
31
- console.log(`Messages: ${msgs.length}`)
32
31
  for (const msg of msgs) {
33
32
  if (lastId === null) {
34
33
  lastId = msg.data.id
@@ -60,35 +59,37 @@ export async function testLoadPartition(client) {
60
59
 
61
60
  let messages = [];
62
61
  let k = 0
63
- for (let i = 0; i < messagesToPush; i++) {
62
+ for (let k = 0; k < 10; k++) {
63
+ for (let i = 0; i < messagesToPush / 10; i++) {
64
64
  messages.push({ data: { id: i } })
65
- if (messages.length >= 10000) {
65
+ if (messages.length >= 100) {
66
66
  await client
67
67
  .queue('test-queue-v2-load-partition')
68
68
  .partition(k.toString())
69
69
  .push(messages)
70
70
  messages = [];
71
- k++;
72
71
  }
73
72
  }
74
- await client.queue('test-queue-v2-load-partition').push(messages)
73
+ }
75
74
 
76
75
  let uniqueIds = new Set();
77
- let lastId = null;
76
+
78
77
 
79
78
  for (let i = 0; i < 10; i++) {
79
+ let lastId = null;
80
80
  await client
81
81
  .queue('test-queue-v2-load-partition')
82
82
  .partition(i.toString())
83
- .batch(10000)
83
+ .batch(100)
84
84
  .wait(false)
85
- .limit(1)
85
+ .limit(10000)
86
86
  .consume(async msgs => {
87
87
  for (const msg of msgs) {
88
88
  if (lastId === null) {
89
89
  lastId = msg.data.id
90
90
  } else {
91
91
  if (msg.data.id !== lastId + 1) {
92
+ console.log(`Message ordering violation: ${msg.data.id} !== ${lastId + 1}`)
92
93
  throw new Error('Message ordering violation')
93
94
  }
94
95
  lastId = msg.data.id
@@ -98,14 +99,15 @@ export async function testLoadPartition(client) {
98
99
  })
99
100
  }
100
101
 
102
+ console.log(uniqueIds.size, messagesToPush)
101
103
  const uniqueIdsCount = uniqueIds.size;
102
104
 
103
- return { success: uniqueIdsCount === messagesToPush, message: 'Load test completed successfully' }
105
+ return { success: uniqueIdsCount === messagesToPush / 10, message: 'Load test completed successfully' }
104
106
  }
105
107
 
106
108
  export async function testLoadConsumerGroup(client) {
107
109
  const queue = await client
108
- .queue('test-queue-v2-load-consumer-group')
110
+ .queue('')
109
111
  .create()
110
112
  if (!queue.configured) {
111
113
  return { success: false, message: 'Queue not created' }
@@ -116,7 +118,7 @@ export async function testLoadConsumerGroup(client) {
116
118
  let messages = [];
117
119
  for (let i = 0; i < messagesToPush; i++) {
118
120
  messages.push({ data: { id: i } })
119
- if (messages.length >= 10000) {
121
+ if (messages.length >= 100) {
120
122
  await client.queue('test-queue-v2-load-consumer-group').push(messages)
121
123
  messages = [];
122
124
  }
@@ -129,11 +131,11 @@ export async function testLoadConsumerGroup(client) {
129
131
  await client
130
132
  .queue('test-queue-v2-load-consumer-group')
131
133
  .group('test-consumer-group-a')
132
- .subscriptionMode('from_beginning')
134
+ .subscriptionMode('all')
133
135
  .concurrency(10)
134
- .batch(10000)
136
+ .batch(100)
135
137
  .wait(false)
136
- .limit(1)
138
+ .limit(10000)
137
139
  .consume(async msgs => {
138
140
  for (const msg of msgs) {
139
141
  if (lastIdA === null) {
@@ -154,11 +156,11 @@ export async function testLoadConsumerGroup(client) {
154
156
  await client
155
157
  .queue('test-queue-v2-load-consumer-group')
156
158
  .group('test-consumer-group-b')
157
- .subscriptionMode('from_beginning')
159
+ .subscriptionMode('all')
158
160
  .concurrency(10)
159
- .batch(10000)
161
+ .batch(100)
160
162
  .wait(false)
161
- .limit(1)
163
+ .limit(10000)
162
164
  .consume(async msgs => {
163
165
  for (const msg of msgs) {
164
166
  if (lastIdB === null) {