comq 0.14.0 → 0.15.1
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/.github/workflows/dev.yaml +28 -0
- package/.github/workflows/release.yaml +3 -3
- package/.husky/pre-push +5 -0
- package/.releaserc +1 -1
- package/CHANGELOG.md +26 -0
- package/docker-compose.yaml +2 -2
- package/features/connection.feature +1 -0
- package/features/recovery.feature +1 -0
- package/features/rpc.streams.feature +1 -0
- package/features/rpc.streams.shards.feature +1 -0
- package/features/shards.feature +5 -3
- package/features/steps/backpressure.js +22 -4
- package/features/steps/brokers.js +1 -1
- package/features/steps/connection.js +15 -0
- package/features/steps/events.js +1 -1
- package/features/steps/rpc.js +1 -1
- package/features/steps/yaml.js +15 -0
- package/package.json +13 -7
- package/readme.md +8 -5
- package/scripts/feat.js +67 -0
- package/source/.io/ReplyStream.js +11 -2
- package/source/channel.js +12 -1
- package/source/connection.js +92 -12
- package/source/events.js +1 -1
- package/source/io.js +42 -8
- package/test/ReplyStream.test.js +45 -0
- package/test/amqplib.mock.js +6 -0
- package/test/channel.test.js +23 -0
- package/test/connection.test.js +112 -5
- package/test/io.request.timeout.test.js +61 -0
- package/types/diagnostic.d.ts +1 -1
- package/types/io.d.ts +6 -2
- package/.github/workflows/codeql.yml +0 -78
- package/fixplan.md +0 -507
package/source/connection.js
CHANGED
|
@@ -25,9 +25,15 @@ class Connection {
|
|
|
25
25
|
/** @type {Promex} */
|
|
26
26
|
#recovery = new Promex()
|
|
27
27
|
|
|
28
|
+
/** @type {Promise<void> | null} */
|
|
29
|
+
#opening = null
|
|
30
|
+
|
|
28
31
|
/** @type {boolean} */
|
|
29
32
|
#running = false
|
|
30
33
|
|
|
34
|
+
/** @type {NodeJS.Timeout | null} */
|
|
35
|
+
#heartbeatTimer = null
|
|
36
|
+
|
|
31
37
|
#diagnostics = emitter.create()
|
|
32
38
|
|
|
33
39
|
/**
|
|
@@ -35,10 +41,17 @@ class Connection {
|
|
|
35
41
|
*/
|
|
36
42
|
constructor (url) {
|
|
37
43
|
this.#url = url
|
|
44
|
+
|
|
45
|
+
// EventEmitter throws on 'error' with no listeners
|
|
46
|
+
this.#diagnostics.on('error', noop)
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
async open () {
|
|
41
|
-
|
|
50
|
+
if (this.#opening !== null) return this.#opening
|
|
51
|
+
|
|
52
|
+
this.#opening = retry(this.#open).finally(() => { this.#opening = null })
|
|
53
|
+
|
|
54
|
+
await this.#opening
|
|
42
55
|
|
|
43
56
|
this.#running = true
|
|
44
57
|
}
|
|
@@ -71,8 +84,11 @@ class Connection {
|
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
#open = async (retry) => {
|
|
87
|
+
/** @type {comq.amqp.Connection} */
|
|
88
|
+
let connection
|
|
89
|
+
|
|
74
90
|
try {
|
|
75
|
-
|
|
91
|
+
connection = await amqp.connect(this.#url)
|
|
76
92
|
} catch (exception) {
|
|
77
93
|
if (this.#transient(exception)) return retry
|
|
78
94
|
else throw exception
|
|
@@ -80,40 +96,104 @@ class Connection {
|
|
|
80
96
|
|
|
81
97
|
// This prevents the process from crashing; 'close' will be emitted next.
|
|
82
98
|
// https://amqp-node.github.io/amqplib/channel_api.html#model_events
|
|
83
|
-
|
|
99
|
+
connection.on('error', noop)
|
|
84
100
|
|
|
85
|
-
|
|
101
|
+
connection.on('close', (error) => this.#close(connection, error))
|
|
102
|
+
this.#connection = connection
|
|
103
|
+
this.#armWatchdog(connection)
|
|
86
104
|
this.#diagnostics.emit('open')
|
|
87
105
|
|
|
88
|
-
|
|
106
|
+
try {
|
|
107
|
+
for (const channel of this.#channels) await channel.recover(connection)
|
|
108
|
+
} catch (exception) {
|
|
109
|
+
this.#disarmWatchdog()
|
|
110
|
+
this.#diagnostics.emit('error', exception)
|
|
111
|
+
connection.removeAllListeners()
|
|
112
|
+
|
|
113
|
+
await connection.close().catch(noop)
|
|
114
|
+
|
|
115
|
+
if (this.#connection === connection) this.#connection = undefined
|
|
116
|
+
|
|
117
|
+
return retry
|
|
118
|
+
}
|
|
89
119
|
|
|
90
120
|
this.#recovery.resolve()
|
|
91
121
|
this.#recovery = new Promex()
|
|
92
122
|
}
|
|
93
123
|
|
|
94
124
|
/**
|
|
95
|
-
* @param {
|
|
125
|
+
* @param {comq.amqp.Connection} connection
|
|
126
|
+
* @param {Error} [error]
|
|
96
127
|
*/
|
|
97
|
-
#close =
|
|
128
|
+
#close = (connection, error) => {
|
|
129
|
+
this.#disarmWatchdog()
|
|
130
|
+
|
|
131
|
+
if (this.#connection !== connection) return
|
|
132
|
+
|
|
98
133
|
this.#diagnostics.emit('close', error)
|
|
99
|
-
|
|
134
|
+
connection.removeAllListeners()
|
|
100
135
|
this.#connection = undefined
|
|
101
136
|
|
|
102
|
-
if (error !== undefined)
|
|
137
|
+
if (error !== undefined) {
|
|
138
|
+
this.open().catch((exception) => this.#diagnostics.emit('error', exception))
|
|
139
|
+
}
|
|
103
140
|
}
|
|
104
141
|
|
|
105
142
|
#recover () {
|
|
106
143
|
return this.#recovery
|
|
107
144
|
}
|
|
108
145
|
|
|
146
|
+
/**
|
|
147
|
+
* @param {comq.amqp.Connection} connection
|
|
148
|
+
* @param {number} [timeoutMs]
|
|
149
|
+
*/
|
|
150
|
+
#armWatchdog (connection, timeoutMs = WATCHDOG_MS) {
|
|
151
|
+
const socket = connection.connection?.stream
|
|
152
|
+
|
|
153
|
+
if (socket === undefined) return
|
|
154
|
+
|
|
155
|
+
const reset = () => {
|
|
156
|
+
clearTimeout(this.#heartbeatTimer)
|
|
157
|
+
this.#heartbeatTimer = setTimeout(() => socket.destroy(), timeoutMs)
|
|
158
|
+
this.#heartbeatTimer.unref()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
reset()
|
|
162
|
+
socket.on('data', reset)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
#disarmWatchdog () {
|
|
166
|
+
clearTimeout(this.#heartbeatTimer)
|
|
167
|
+
this.#heartbeatTimer = null
|
|
168
|
+
}
|
|
169
|
+
|
|
109
170
|
#transient (exception) {
|
|
110
|
-
|
|
111
|
-
|
|
171
|
+
if (this.#running) return true
|
|
172
|
+
if (TRANSIENT_CODES.has(exception.code)) return true
|
|
173
|
+
if (TRANSIENT_MESSAGES.has(exception.message)) return true
|
|
112
174
|
|
|
113
|
-
return
|
|
175
|
+
return false
|
|
114
176
|
}
|
|
115
177
|
}
|
|
116
178
|
|
|
179
|
+
/** @type {number} */
|
|
180
|
+
const WATCHDOG_MS = 60_000
|
|
181
|
+
|
|
182
|
+
const TRANSIENT_CODES = new Set([
|
|
183
|
+
'ECONNREFUSED',
|
|
184
|
+
'EAI_AGAIN',
|
|
185
|
+
'ENOTFOUND',
|
|
186
|
+
'ETIMEDOUT',
|
|
187
|
+
'ECONNRESET',
|
|
188
|
+
'EHOSTUNREACH',
|
|
189
|
+
'ENETUNREACH'
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
const TRANSIENT_MESSAGES = new Set([
|
|
193
|
+
'Socket closed abruptly during opening handshake',
|
|
194
|
+
'Client network socket disconnected before secure TLS connection was established'
|
|
195
|
+
])
|
|
196
|
+
|
|
117
197
|
function noop () {}
|
|
118
198
|
|
|
119
199
|
exports.Connection = Connection
|
package/source/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
/** @type {comq.diagnostics.Event[]} */
|
|
4
|
-
exports.connection = ['open', 'close']
|
|
4
|
+
exports.connection = ['open', 'close', 'error']
|
|
5
5
|
|
|
6
6
|
/** @type {comq.diagnostics.Event[]} */
|
|
7
7
|
exports.channel = ['flow', 'drain', 'recover', 'discard', 'pause', 'resume']
|
package/source/io.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const stream = require('node:stream')
|
|
4
4
|
const { EventEmitter } = require('node:events')
|
|
5
5
|
const { randomBytes } = require('node:crypto')
|
|
6
|
-
const { setTimeout } = require('node:timers/promises')
|
|
6
|
+
const { setTimeout: delay } = require('node:timers/promises')
|
|
7
7
|
const { Promex } = require('promex')
|
|
8
8
|
const { memo, failsafe, lazy, track } = require('./attributes')
|
|
9
9
|
|
|
@@ -75,24 +75,45 @@ class IO {
|
|
|
75
75
|
/**
|
|
76
76
|
* @param {string} queue
|
|
77
77
|
* @param {any | Readable} payload
|
|
78
|
-
* @param {comq.Encoding} [encoding]
|
|
78
|
+
* @param {comq.Encoding | number} [encoding]
|
|
79
|
+
* @param {number} [timeout]
|
|
79
80
|
* @returns {Promise<any | Readable>}
|
|
80
81
|
*/
|
|
81
|
-
async (queue, payload, encoding) => {
|
|
82
|
+
async (queue, payload, encoding, timeout) => {
|
|
83
|
+
if (typeof encoding === 'number') {
|
|
84
|
+
timeout = encoding
|
|
85
|
+
encoding = undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
82
88
|
if (payload instanceof stream.Readable) {
|
|
83
89
|
return pipeline(
|
|
84
90
|
payload,
|
|
85
|
-
(payload) => this.request(queue, payload, encoding),
|
|
91
|
+
(payload) => this.request(queue, payload, encoding, timeout),
|
|
86
92
|
this.#requests
|
|
87
93
|
)
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
const request = this.#createRequest(queue, payload, encoding)
|
|
96
|
+
const request = this.#createRequest(queue, payload, /** @type {comq.Encoding} */ encoding)
|
|
91
97
|
const reply = this.#createReply(request)
|
|
92
98
|
|
|
93
|
-
|
|
99
|
+
const done = this.#requests.send(queue, request.buffer, request.properties)
|
|
100
|
+
.then(() => reply)
|
|
101
|
+
|
|
102
|
+
if (typeof timeout !== 'number') return done
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
try {
|
|
105
|
+
return await Promise.race([
|
|
106
|
+
done,
|
|
107
|
+
delay(timeout).then(() => Promise.reject(timeoutError(timeout)))
|
|
108
|
+
])
|
|
109
|
+
} catch (exception) {
|
|
110
|
+
if (exception.code === 'ETIMEDOUT') {
|
|
111
|
+
request.emitter.removeAllListeners(request.properties.correlationId)
|
|
112
|
+
reply.reject(exception)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw exception
|
|
116
|
+
}
|
|
96
117
|
}))
|
|
97
118
|
|
|
98
119
|
consume = lazy(this, this.#createEventChannel,
|
|
@@ -396,7 +417,7 @@ class IO {
|
|
|
396
417
|
Even if these messages are lost, the reply stream will be closed anyway,
|
|
397
418
|
either due to missing heartbeat or the deletion of the stream queue.
|
|
398
419
|
*/
|
|
399
|
-
await
|
|
420
|
+
await delay(50)
|
|
400
421
|
}
|
|
401
422
|
|
|
402
423
|
/**
|
|
@@ -456,6 +477,19 @@ const DEFAULT = 'application/json'
|
|
|
456
477
|
|
|
457
478
|
const RETRANSMISSION = /** @type {Error} */ Symbol('retransmission')
|
|
458
479
|
|
|
480
|
+
/**
|
|
481
|
+
* @param {number} timeout
|
|
482
|
+
* @returns {Error}
|
|
483
|
+
*/
|
|
484
|
+
function timeoutError (timeout) {
|
|
485
|
+
const error = /** @type {Error & { code: string }} */ new Error(
|
|
486
|
+
`Request timed out after ${timeout}ms`)
|
|
487
|
+
|
|
488
|
+
error.code = 'ETIMEDOUT'
|
|
489
|
+
|
|
490
|
+
return error
|
|
491
|
+
}
|
|
492
|
+
|
|
459
493
|
function noop () {}
|
|
460
494
|
|
|
461
495
|
exports.IO = IO
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('node:events')
|
|
4
|
+
const { once } = require('node:events')
|
|
5
|
+
const { ReplyStream } = require('../source/.io/ReplyStream')
|
|
6
|
+
|
|
7
|
+
/** @type {jest.Mock} */
|
|
8
|
+
let reply
|
|
9
|
+
|
|
10
|
+
/** @type {ReplyStream} */
|
|
11
|
+
let stream
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
global.COMQ_TESTING_MAX_BUFFER_SIZE = 3
|
|
15
|
+
global.COMQ_TESTING_IDLE_INTERVAL = 60_000
|
|
16
|
+
|
|
17
|
+
reply = jest.fn().mockResolvedValue(true)
|
|
18
|
+
|
|
19
|
+
const request = {
|
|
20
|
+
emitter: new EventEmitter(),
|
|
21
|
+
properties: { correlationId: 'test-correlation' }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
stream = new ReplyStream(request, reply)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
delete global.COMQ_TESTING_MAX_BUFFER_SIZE
|
|
29
|
+
delete global.COMQ_TESTING_IDLE_INTERVAL
|
|
30
|
+
|
|
31
|
+
stream.destroy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should destroy without calling reply when buffer overflows before control.ok', async () => {
|
|
35
|
+
const closed = once(stream, 'close')
|
|
36
|
+
|
|
37
|
+
for (let index = 1; index <= 5; index++) {
|
|
38
|
+
stream.arrange(index, { headers: { index } })
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await closed
|
|
42
|
+
|
|
43
|
+
expect(stream.destroyed).toBe(true)
|
|
44
|
+
expect(reply).not.toHaveBeenCalled()
|
|
45
|
+
})
|
package/test/amqplib.mock.js
CHANGED
|
@@ -21,6 +21,12 @@ class Connection extends EventEmitter {
|
|
|
21
21
|
constructor () {
|
|
22
22
|
super()
|
|
23
23
|
|
|
24
|
+
const stream = new EventEmitter()
|
|
25
|
+
|
|
26
|
+
stream.destroy = jest.fn()
|
|
27
|
+
|
|
28
|
+
this.connection = { stream }
|
|
29
|
+
|
|
24
30
|
// noinspection JSValidateTypes
|
|
25
31
|
this.removeAllListeners = jest.spyOn(this, 'removeAllListeners')
|
|
26
32
|
|
package/test/channel.test.js
CHANGED
|
@@ -552,6 +552,29 @@ describe('recovery', () => {
|
|
|
552
552
|
await expect(channel.consume(queue, consumer)).rejects.toStrictEqual(exception)
|
|
553
553
|
})
|
|
554
554
|
|
|
555
|
+
it('should not deadlock when recall hits a transient error during recover', async () => {
|
|
556
|
+
await channel.consume(queue, consumer)
|
|
557
|
+
|
|
558
|
+
const replacement = await amqplib.connect()
|
|
559
|
+
const method = `create${topology.confirms ? 'Confirm' : ''}Channel`
|
|
560
|
+
|
|
561
|
+
replacement[method].mockImplementationOnce(async () => {
|
|
562
|
+
const transient = await amqplib.connect()
|
|
563
|
+
const bad = await transient[method]()
|
|
564
|
+
|
|
565
|
+
bad.consume.mockImplementation(async () => { throw new Error('Channel closed') })
|
|
566
|
+
|
|
567
|
+
return bad
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
const deadlock = timeout(200).then(() => {
|
|
571
|
+
throw new Error('recover hung (deadlock)')
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
await expect(Promise.race([channel.recover(replacement), deadlock]))
|
|
575
|
+
.rejects.toThrow('Channel closed')
|
|
576
|
+
})
|
|
577
|
+
|
|
555
578
|
describe.each(/** @type {[string, boolean][]} */ [
|
|
556
579
|
['', false],
|
|
557
580
|
[' not', true]
|
package/test/connection.test.js
CHANGED
|
@@ -40,7 +40,15 @@ describe('initial connection', () => {
|
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
it.each(/** @type {[string, Partial<Error>][]} */[
|
|
43
|
-
['Socket closed', { message: 'Socket closed abruptly during opening handshake' }]
|
|
43
|
+
['Socket closed', { message: 'Socket closed abruptly during opening handshake' }],
|
|
44
|
+
['TLS disconnect', { message: 'Client network socket disconnected before secure TLS connection was established' }],
|
|
45
|
+
['ECONNREFUSED', { code: 'ECONNREFUSED' }],
|
|
46
|
+
['EAI_AGAIN', { code: 'EAI_AGAIN' }],
|
|
47
|
+
['ENOTFOUND', { code: 'ENOTFOUND' }],
|
|
48
|
+
['ETIMEDOUT', { code: 'ETIMEDOUT' }],
|
|
49
|
+
['ECONNRESET', { code: 'ECONNRESET' }],
|
|
50
|
+
['EHOSTUNREACH', { code: 'EHOSTUNREACH' }],
|
|
51
|
+
['ENETUNREACH', { code: 'ENETUNREACH' }]
|
|
44
52
|
])('should reconnect on %s',
|
|
45
53
|
async (_, error) => {
|
|
46
54
|
amqplib.connect.mockImplementationOnce(async () => { throw error })
|
|
@@ -50,10 +58,9 @@ describe('initial connection', () => {
|
|
|
50
58
|
expect(amqplib.connect).toHaveBeenCalledTimes(2)
|
|
51
59
|
})
|
|
52
60
|
|
|
53
|
-
it
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
])('should throw if error is %s', async (_, exception) => {
|
|
61
|
+
it('should throw if error is permanent', async () => {
|
|
62
|
+
const exception = new Error(generate())
|
|
63
|
+
|
|
57
64
|
amqplib.connect.mockImplementationOnce(async () => { throw exception })
|
|
58
65
|
|
|
59
66
|
await expect(connection.open()).rejects.toStrictEqual(exception)
|
|
@@ -109,6 +116,68 @@ describe('reconnection', () => {
|
|
|
109
116
|
|
|
110
117
|
expect(channel.recover).toHaveBeenCalledWith(replacement)
|
|
111
118
|
})
|
|
119
|
+
|
|
120
|
+
it('should retry when channel recover fails', async () => {
|
|
121
|
+
const channel = await connection.createChannel('request')
|
|
122
|
+
|
|
123
|
+
channel.recover
|
|
124
|
+
.mockRejectedValueOnce(new Error('Channel closed'))
|
|
125
|
+
.mockResolvedValue(undefined)
|
|
126
|
+
|
|
127
|
+
conn.emit('close', new Error())
|
|
128
|
+
|
|
129
|
+
const start = Date.now()
|
|
130
|
+
|
|
131
|
+
while (channel.recover.mock.calls.length < 2 && Date.now() - start < 10000) {
|
|
132
|
+
await timeout(50)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(channel.recover).toHaveBeenCalledTimes(2)
|
|
136
|
+
expect(amqplib.connect.mock.calls.length).toBeGreaterThanOrEqual(3)
|
|
137
|
+
}, 15000)
|
|
138
|
+
|
|
139
|
+
it('should emit error when reconnect open fails', async () => {
|
|
140
|
+
const errors = []
|
|
141
|
+
const unhandled = jest.fn()
|
|
142
|
+
|
|
143
|
+
connection.diagnose('error', (exception) => errors.push(exception))
|
|
144
|
+
process.on('unhandledRejection', unhandled)
|
|
145
|
+
|
|
146
|
+
const boom = new Error('reconnect failed')
|
|
147
|
+
|
|
148
|
+
connection.open = jest.fn(async () => { throw boom })
|
|
149
|
+
|
|
150
|
+
conn.emit('close', new Error('broker down'))
|
|
151
|
+
|
|
152
|
+
await timeout(10)
|
|
153
|
+
|
|
154
|
+
process.off('unhandledRejection', unhandled)
|
|
155
|
+
|
|
156
|
+
expect(errors).toContain(boom)
|
|
157
|
+
expect(unhandled).not.toHaveBeenCalled()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('should ignore close from a stale connection', async () => {
|
|
161
|
+
const stale = conn
|
|
162
|
+
const closeHandler = stale.on.mock.calls.find(([event]) => event === 'close')[1]
|
|
163
|
+
|
|
164
|
+
stale.emit('close', new Error('gone'))
|
|
165
|
+
|
|
166
|
+
await timeout(50)
|
|
167
|
+
|
|
168
|
+
const live = await amqplib.connect.mock.results[1].value
|
|
169
|
+
const connects = amqplib.connect.mock.calls.length
|
|
170
|
+
|
|
171
|
+
expect(live).not.toBe(stale)
|
|
172
|
+
|
|
173
|
+
live.removeAllListeners.mockClear()
|
|
174
|
+
closeHandler(new Error('late'))
|
|
175
|
+
|
|
176
|
+
await timeout(10)
|
|
177
|
+
|
|
178
|
+
expect(amqplib.connect).toHaveBeenCalledTimes(connects)
|
|
179
|
+
expect(live.removeAllListeners).not.toHaveBeenCalled()
|
|
180
|
+
})
|
|
112
181
|
})
|
|
113
182
|
|
|
114
183
|
describe('create channel', () => {
|
|
@@ -203,6 +272,44 @@ describe('create channel', () => {
|
|
|
203
272
|
})
|
|
204
273
|
})
|
|
205
274
|
|
|
275
|
+
describe('watchdog', () => {
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
jest.useRealTimers()
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('should destroy silent connection when watchdog expires', async () => {
|
|
281
|
+
jest.useFakeTimers()
|
|
282
|
+
|
|
283
|
+
await connection.open()
|
|
284
|
+
|
|
285
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
286
|
+
|
|
287
|
+
await jest.advanceTimersByTimeAsync(60_000)
|
|
288
|
+
|
|
289
|
+
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should reset watchdog on socket data', async () => {
|
|
293
|
+
jest.useFakeTimers()
|
|
294
|
+
|
|
295
|
+
await connection.open()
|
|
296
|
+
|
|
297
|
+
const conn = await amqplib.connect.mock.results[0].value
|
|
298
|
+
|
|
299
|
+
await jest.advanceTimersByTimeAsync(50_000)
|
|
300
|
+
|
|
301
|
+
conn.connection.stream.emit('data', Buffer.alloc(0))
|
|
302
|
+
|
|
303
|
+
await jest.advanceTimersByTimeAsync(50_000)
|
|
304
|
+
|
|
305
|
+
expect(conn.connection.stream.destroy).not.toHaveBeenCalled()
|
|
306
|
+
|
|
307
|
+
await jest.advanceTimersByTimeAsync(10_000)
|
|
308
|
+
|
|
309
|
+
expect(conn.connection.stream.destroy).toHaveBeenCalled()
|
|
310
|
+
})
|
|
311
|
+
})
|
|
312
|
+
|
|
206
313
|
describe('close', () => {
|
|
207
314
|
it('should close connection', async () => {
|
|
208
315
|
await connection.open()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { generate } = require('randomstring')
|
|
4
|
+
|
|
5
|
+
const mock = require('./connection.mock')
|
|
6
|
+
const { IO } = require('../source/io')
|
|
7
|
+
|
|
8
|
+
/** @type {comq.IO} */
|
|
9
|
+
let io
|
|
10
|
+
|
|
11
|
+
/** @type {jest.MockedObject<comq.Connection>} */
|
|
12
|
+
let connection
|
|
13
|
+
|
|
14
|
+
const queue = generate()
|
|
15
|
+
const payload = { [generate()]: generate() }
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks()
|
|
19
|
+
|
|
20
|
+
connection = mock.connection()
|
|
21
|
+
io = new IO(connection)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should reject when request times out', async () => {
|
|
25
|
+
await expect(io.request(queue, payload, 50)).rejects.toMatchObject({
|
|
26
|
+
message: expect.stringContaining('timed out'),
|
|
27
|
+
code: 'ETIMEDOUT'
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should reject when request times out with encoding', async () => {
|
|
32
|
+
await expect(io.request(queue, payload, 'application/json', 50)).rejects.toMatchObject({
|
|
33
|
+
code: 'ETIMEDOUT'
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should time out while send is still pending', async () => {
|
|
38
|
+
connection.createChannel.mockImplementation(async () => hangingChannel())
|
|
39
|
+
|
|
40
|
+
await expect(io.request(queue, payload, 50)).rejects.toMatchObject({
|
|
41
|
+
code: 'ETIMEDOUT'
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {jest.MockedObject<comq.Channel>}
|
|
47
|
+
*/
|
|
48
|
+
function hangingChannel () {
|
|
49
|
+
return /** @type {jest.MockedObject<comq.Channel>} */ ({
|
|
50
|
+
sharded: false,
|
|
51
|
+
consume: jest.fn(async () => undefined),
|
|
52
|
+
send: jest.fn(() => new Promise(() => {})),
|
|
53
|
+
deliver: jest.fn(async () => undefined),
|
|
54
|
+
fire: jest.fn(async () => undefined),
|
|
55
|
+
subscribe: jest.fn(async () => undefined),
|
|
56
|
+
publish: jest.fn(async () => undefined),
|
|
57
|
+
diagnose: jest.fn(),
|
|
58
|
+
seal: jest.fn(async () => undefined),
|
|
59
|
+
recover: jest.fn(async () => undefined)
|
|
60
|
+
})
|
|
61
|
+
}
|
package/types/diagnostic.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
declare namespace comq.diagnostics {
|
|
2
2
|
|
|
3
|
-
type Event = 'open' | 'close' | 'flow' | 'drain' | 'remove' | 'recover' | 'discard' |
|
|
3
|
+
type Event = 'open' | 'close' | 'error' | 'flow' | 'drain' | 'remove' | 'recover' | 'discard' |
|
|
4
4
|
'pause' | 'resume' | 'return'
|
|
5
5
|
|
|
6
6
|
interface Diagnosable {
|
package/types/io.d.ts
CHANGED
|
@@ -27,9 +27,11 @@ declare namespace comq {
|
|
|
27
27
|
interface IO extends _diagnostics.Diagnosable {
|
|
28
28
|
reply(queue: string, produce: Producer): Promise<void>
|
|
29
29
|
|
|
30
|
-
request<Reply = any, Request = any>(queue: string, payload: Request, encoding?: _encoding.Encoding): Promise<Reply> | Promise<Readable>
|
|
30
|
+
request<Reply = any, Request = any>(queue: string, payload: Request, encoding?: _encoding.Encoding, timeout?: number): Promise<Reply> | Promise<Readable>
|
|
31
31
|
|
|
32
|
-
request(queue: string,
|
|
32
|
+
request<Reply = any, Request = any>(queue: string, payload: Request, timeout: number): Promise<Reply> | Promise<Readable>
|
|
33
|
+
|
|
34
|
+
request(queue: string, stream: Readable, encoding?: _encoding.Encoding, timeout?: number): Promise<Readable>
|
|
33
35
|
|
|
34
36
|
consume<T = any>(exchange: string, group: string, consumer: Consumer<T>): Promise<void>
|
|
35
37
|
|
|
@@ -61,6 +63,8 @@ declare namespace comq {
|
|
|
61
63
|
|
|
62
64
|
diagnose(event: 'close', listener: (index?: number) => void): void
|
|
63
65
|
|
|
66
|
+
diagnose(event: 'error', listener: (error: Error, index?: number) => void): void
|
|
67
|
+
|
|
64
68
|
diagnose(event: 'flow', listener: (channel: _topology.type, index?: number) => void): void
|
|
65
69
|
|
|
66
70
|
diagnose(event: 'drain', listener: (channel: _topology.type, index?: number) => void): void
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# For most projects, this workflow file will not need changing; you simply need
|
|
2
|
-
# to commit it to your repository.
|
|
3
|
-
#
|
|
4
|
-
# You may wish to alter this file to override the set of languages analyzed,
|
|
5
|
-
# or to provide custom queries or build logic.
|
|
6
|
-
#
|
|
7
|
-
# ******** NOTE ********
|
|
8
|
-
# We have attempted to detect the languages in your repository. Please check
|
|
9
|
-
# the `language` matrix defined below to confirm you have the correct set of
|
|
10
|
-
# supported CodeQL languages.
|
|
11
|
-
#
|
|
12
|
-
name: "CodeQL"
|
|
13
|
-
|
|
14
|
-
on:
|
|
15
|
-
push:
|
|
16
|
-
branches: [ "main" ]
|
|
17
|
-
paths: [ "source/**" ]
|
|
18
|
-
pull_request:
|
|
19
|
-
# The branches below must be a subset of the branches above
|
|
20
|
-
branches: [ "main" ]
|
|
21
|
-
paths: [ "source/**" ]
|
|
22
|
-
schedule:
|
|
23
|
-
- cron: '15 8 * * 3'
|
|
24
|
-
|
|
25
|
-
jobs:
|
|
26
|
-
analyze:
|
|
27
|
-
name: Analyze
|
|
28
|
-
runs-on: ubuntu-latest
|
|
29
|
-
permissions:
|
|
30
|
-
actions: read
|
|
31
|
-
contents: read
|
|
32
|
-
security-events: write
|
|
33
|
-
|
|
34
|
-
strategy:
|
|
35
|
-
fail-fast: false
|
|
36
|
-
matrix:
|
|
37
|
-
language: [ 'javascript' ]
|
|
38
|
-
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
39
|
-
# Use only 'java' to analyze code written in Java, Kotlin or both
|
|
40
|
-
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
|
41
|
-
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
|
42
|
-
|
|
43
|
-
steps:
|
|
44
|
-
- name: Checkout repository
|
|
45
|
-
uses: actions/checkout@v6
|
|
46
|
-
|
|
47
|
-
# Initializes the CodeQL tools for scanning.
|
|
48
|
-
- name: Initialize CodeQL
|
|
49
|
-
uses: github/codeql-action/init@v4
|
|
50
|
-
with:
|
|
51
|
-
languages: ${{ matrix.language }}
|
|
52
|
-
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
53
|
-
# By default, queries listed here will override any specified in a config file.
|
|
54
|
-
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
55
|
-
|
|
56
|
-
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
57
|
-
# queries: security-extended,security-and-quality
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java).
|
|
61
|
-
# If this step fails, then you should remove it and run the build manually (see below)
|
|
62
|
-
- name: Autobuild
|
|
63
|
-
uses: github/codeql-action/autobuild@v4
|
|
64
|
-
|
|
65
|
-
# ℹ️ Command-line programs to run using the OS shell.
|
|
66
|
-
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
67
|
-
|
|
68
|
-
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
|
69
|
-
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
|
70
|
-
|
|
71
|
-
# - run: |
|
|
72
|
-
# echo "Run, Build Application using script"
|
|
73
|
-
# ./location_of_script_within_repo/buildscript.sh
|
|
74
|
-
|
|
75
|
-
- name: Perform CodeQL Analysis
|
|
76
|
-
uses: github/codeql-action/analyze@v4
|
|
77
|
-
with:
|
|
78
|
-
category: "/language:${{matrix.language}}"
|