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.
- package/README.md +1 -1
- package/client-v2/Queen.js +31 -4
- package/client-v2/README.md +53 -2
- package/client-v2/admin/Admin.js +414 -0
- package/client-v2/admin/index.js +6 -0
- package/client-v2/builders/QueueBuilder.js +24 -1
- package/client-v2/consumer/ConsumerManager.js +25 -4
- package/client-v2/http/HttpClient.js +36 -15
- package/client-v2/http/LoadBalancer.js +227 -6
- package/client-v2/index.js +1 -0
- package/client-v2/utils/defaults.js +5 -2
- package/client-v2/utils/logger.js +13 -2
- package/package.json +3 -2
- package/test-v2/consume.js +6 -57
- package/test-v2/load.js +22 -20
- package/test-v2/maintenance.js +3 -1
- package/test-v2/push.js +0 -29
- package/test-v2/run.js +24 -3
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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) %
|
|
130
|
+
this.#currentIndex = (this.#currentIndex + 1) % healthyUrls.length
|
|
27
131
|
}
|
|
28
|
-
|
|
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 =
|
|
33
|
-
this.#currentIndex = (this.#currentIndex + 1) %
|
|
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
|
package/client-v2/index.js
CHANGED
|
@@ -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: '
|
|
14
|
-
|
|
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 =
|
|
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.
|
|
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": {
|
package/test-v2/consume.js
CHANGED
|
@@ -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(
|
|
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(
|
|
415
|
+
.concurrency(1)
|
|
469
416
|
.subscriptionMode('from_beginning')
|
|
470
417
|
.batch(1000)
|
|
471
418
|
.wait(false)
|
|
472
|
-
.limit(
|
|
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 >=
|
|
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(
|
|
27
|
+
.batch(100)
|
|
28
28
|
.wait(false)
|
|
29
|
-
.limit(
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
73
|
+
}
|
|
75
74
|
|
|
76
75
|
let uniqueIds = new Set();
|
|
77
|
-
|
|
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(
|
|
83
|
+
.batch(100)
|
|
84
84
|
.wait(false)
|
|
85
|
-
.limit(
|
|
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('
|
|
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 >=
|
|
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('
|
|
134
|
+
.subscriptionMode('all')
|
|
133
135
|
.concurrency(10)
|
|
134
|
-
.batch(
|
|
136
|
+
.batch(100)
|
|
135
137
|
.wait(false)
|
|
136
|
-
.limit(
|
|
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('
|
|
159
|
+
.subscriptionMode('all')
|
|
158
160
|
.concurrency(10)
|
|
159
|
-
.batch(
|
|
161
|
+
.batch(100)
|
|
160
162
|
.wait(false)
|
|
161
|
-
.limit(
|
|
163
|
+
.limit(10000)
|
|
162
164
|
.consume(async msgs => {
|
|
163
165
|
for (const msg of msgs) {
|
|
164
166
|
if (lastIdB === null) {
|