Skip to content
This repository was archived by the owner on May 14, 2024. It is now read-only.

Commit 2e1ef78

Browse files
jsumnersUziTech
andauthored
Refactor client RequestQueue into testable module (#548)
* Refactor client RequestQueue into testable module * Update test/lib/client/request-queue/enqueue.test.js Co-Authored-By: Tony Brix <[email protected]> * Update test/lib/client/request-queue/enqueue.test.js Co-Authored-By: Tony Brix <[email protected]> * Update test/lib/client/request-queue/enqueue.test.js Co-Authored-By: Tony Brix <[email protected]>
1 parent c2786d9 commit 2e1ef78

File tree

10 files changed

+274
-87
lines changed

10 files changed

+274
-87
lines changed

.taprc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
esm: false
2+
3+
files:
4+
- 'test/**/*.test.js'

lib/client/client.js

Lines changed: 3 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const requestQueueFactory = require('./request-queue')
4+
35
var EventEmitter = require('events').EventEmitter
46
var net = require('net')
57
var tls = require('tls')
@@ -86,86 +88,6 @@ function ensureDN (input, strict) {
8688
}
8789
}
8890

89-
/**
90-
* Queue to contain LDAP requests.
91-
*
92-
* @param {Object} opts queue options
93-
*
94-
* Accepted Options:
95-
* - size: Maximum queue size
96-
* - timeout: Set timeout between first queue insertion and queue flush.
97-
*/
98-
function RequestQueue (opts) {
99-
if (!opts || typeof (opts) !== 'object') {
100-
opts = {}
101-
}
102-
this.size = (opts.size > 0) ? opts.size : Infinity
103-
this.timeout = (opts.timeout > 0) ? opts.timeout : 0
104-
this._queue = []
105-
this._timer = null
106-
this._frozen = false
107-
}
108-
109-
/**
110-
* Insert request into queue.
111-
*
112-
*/
113-
RequestQueue.prototype.enqueue = function enqueue (msg, expect, emitter, cb) {
114-
if (this._queue.length >= this.size || this._frozen) {
115-
return false
116-
}
117-
var self = this
118-
this._queue.push([msg, expect, emitter, cb])
119-
if (this.timeout > 0) {
120-
if (this._timer !== null) {
121-
this._timer = setTimeout(function () {
122-
// If queue times out, don't allow new entries until thawed
123-
self.freeze()
124-
self.purge()
125-
}, this.timeout)
126-
}
127-
}
128-
return true
129-
}
130-
131-
/**
132-
* Process all queued requests with callback.
133-
*/
134-
RequestQueue.prototype.flush = function flush (cb) {
135-
if (this._timer) {
136-
clearTimeout(this._timer)
137-
this._timer = null
138-
}
139-
var items = this._queue
140-
this._queue = []
141-
items.forEach(function (req) {
142-
cb(req[0], req[1], req[2], req[3])
143-
})
144-
}
145-
146-
/**
147-
* Purge all queued requests with an error.
148-
*/
149-
RequestQueue.prototype.purge = function purge () {
150-
this.flush(function (msg, expect, emitter, cb) {
151-
cb(new errors.TimeoutError('request queue timeout'))
152-
})
153-
}
154-
155-
/**
156-
* Freeze queue, refusing any new entries.
157-
*/
158-
RequestQueue.prototype.freeze = function freeze () {
159-
this._frozen = true
160-
}
161-
162-
/**
163-
* Thaw queue, allowing new entries again.
164-
*/
165-
RequestQueue.prototype.thaw = function thaw () {
166-
this._frozen = false
167-
}
168-
16991
/**
17092
* Track message callback by messageID.
17193
*/
@@ -323,7 +245,7 @@ function Client (options) {
323245
}
324246
this.strictDN = (options.strictDN !== undefined) ? options.strictDN : true
325247

326-
this.queue = new RequestQueue({
248+
this.queue = requestQueueFactory({
327249
size: parseInt((options.queueSize || 0), 10),
328250
timeout: parseInt((options.queueTimeout || 0), 10)
329251
})

lib/client/request-queue/enqueue.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
/**
4+
* Adds requests to the queue. If a timeout has been added to the queue then
5+
* this will freeze the queue with the newly added item, flush it, and then
6+
* unfreeze it when the queue has been cleared.
7+
*
8+
* @param {object} message An LDAP message object.
9+
* @param {object} expect An expectation object.
10+
* @param {object} emitter An event emitter or `null`.
11+
* @param {function} cb A callback to invoke when the request is finished.
12+
*
13+
* @returns {boolean} `true` if the requested was queued. `false` if the queue
14+
* is not accepting any requests.
15+
*/
16+
module.exports = function enqueue (message, expect, emitter, cb) {
17+
if (this._queue.length >= this.size || this._frozen) {
18+
return false
19+
}
20+
21+
this._queue.add({ message, expect, emitter, cb })
22+
23+
if (this.timeout === 0) return true
24+
if (this._timer === null) return true
25+
26+
// A queue can have a specified time allotted for it to be cleared. If that
27+
// time has been reached, reject new entries until the queue has been cleared.
28+
this._timer = setTimeout(queueTimeout.bind(this), this.timeout)
29+
30+
return true
31+
32+
function queueTimeout () {
33+
this.freeze()
34+
this.purge()
35+
}
36+
}

lib/client/request-queue/flush.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict'
2+
3+
/**
4+
* Invokes all requests in the queue by passing them to the supplied callback
5+
* function and then clears all items from the queue.
6+
*
7+
* @param {function} cb A function used to handle the requests.
8+
*/
9+
module.exports = function flush (cb) {
10+
if (this._timer) {
11+
clearTimeout(this._timer)
12+
this._timer = null
13+
}
14+
15+
// We must get a local copy of the queue and clear it before iterating it.
16+
// The client will invoke this flush function _many_ times. If we try to
17+
// iterate it without a local copy and clearing first then we will overflow
18+
// the stack.
19+
const requests = Array.from(this._queue.values())
20+
this._queue.clear()
21+
for (const req of requests) {
22+
cb(req.message, req.expect, req.emitter, req.cb)
23+
}
24+
}

lib/client/request-queue/index.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
'use strict'
2+
3+
const enqueue = require('./enqueue')
4+
const flush = require('./flush')
5+
const purge = require('./purge')
6+
7+
/**
8+
* Builds a request queue object and returns it.
9+
*
10+
* @param {object} [options]
11+
* @param {integer} [options.size] Maximum size of the request queue. Must be
12+
* a number greater than `0` if supplied. Default: `Infinity`.
13+
* @param {integer} [options.timeout] Time in milliseconds a queue has to
14+
* complete the requests it contains.
15+
*
16+
* @returns {object} A queue instance.
17+
*/
18+
module.exports = function requestQueueFactory (options) {
19+
const opts = Object.assign({}, options)
20+
const q = {
21+
size: (opts.size > 0) ? opts.size : Infinity,
22+
timeout: (opts.timeout > 0) ? opts.timeout : 0,
23+
_queue: new Set(),
24+
_timer: null,
25+
_frozen: false
26+
}
27+
28+
q.enqueue = enqueue.bind(q)
29+
q.flush = flush.bind(q)
30+
q.purge = purge.bind(q)
31+
q.freeze = function freeze () {
32+
this._frozen = true
33+
}
34+
q.thaw = function thaw () {
35+
this._frozen = false
36+
}
37+
38+
return q
39+
}

lib/client/request-queue/purge.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use strict'
2+
3+
const { TimeoutError } = require('../../errors')
4+
5+
/**
6+
* Flushes the queue by rejecting all pending requests with a timeout error.
7+
*/
8+
module.exports = function purge () {
9+
this.flush(function flushCB (a, b, c, cb) {
10+
cb(new TimeoutError('request queue timeout'))
11+
})
12+
}

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@
3434
"uuid": "^3.3.3"
3535
},
3636
"scripts": {
37-
"test": "tap --no-cov test/**/*.test.js",
38-
"test:cov": "tap test/**/*.test.js",
39-
"test:cov:html": "tap --coverage-report=html test/**/*.test.js",
40-
"test:watch": "tap -n -w --no-coverage-report test/**/*.test.js",
41-
"lint": "standard examples/**/*.js lib/**/*.js test/**/*.js | snazzy",
42-
"lint:ci": "standard examples/**/*.js lib/**/*.js test/**/*.js"
37+
"test": "tap --no-cov",
38+
"test:cov": "tap",
39+
"test:cov:html": "tap --coverage-report=html",
40+
"test:watch": "tap -n -w --no-coverage-report",
41+
"lint": "standard | snazzy",
42+
"lint:ci": "standard"
4343
},
4444
"husky": {
4545
"hooks": {
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const enqueue = require('../../../../lib/client/request-queue/enqueue')
5+
6+
test('rejects new requests if size is exceeded', async t => {
7+
const q = { _queue: { length: 5 }, size: 5 }
8+
const result = enqueue.call(q, 'foo', 'bar', {}, {})
9+
t.false(result)
10+
})
11+
12+
test('rejects new requests if queue is frozen', async t => {
13+
const q = { _queue: { length: 0 }, size: 5, _frozen: true }
14+
const result = enqueue.call(q, 'foo', 'bar', {}, {})
15+
t.false(result)
16+
})
17+
18+
test('adds a request and returns if no timeout', async t => {
19+
const q = {
20+
_queue: {
21+
length: 0,
22+
add (obj) {
23+
t.deepEqual(obj, {
24+
message: 'foo',
25+
expect: 'bar',
26+
emitter: 'baz',
27+
cb: 'bif'
28+
})
29+
}
30+
},
31+
_frozen: false,
32+
timeout: 0
33+
}
34+
const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif')
35+
t.true(result)
36+
})
37+
38+
test('adds a request and returns timer not set', async t => {
39+
const q = {
40+
_queue: {
41+
length: 0,
42+
add (obj) {
43+
t.deepEqual(obj, {
44+
message: 'foo',
45+
expect: 'bar',
46+
emitter: 'baz',
47+
cb: 'bif'
48+
})
49+
}
50+
},
51+
_frozen: false,
52+
timeout: 100,
53+
_timer: null
54+
}
55+
const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif')
56+
t.true(result)
57+
})
58+
59+
test('adds a request, returns true, and clears queue', t => {
60+
// Must not be an async test due to an internal `setTimeout`
61+
t.plan(4)
62+
const q = {
63+
_queue: {
64+
length: 0,
65+
add (obj) {
66+
t.deepEqual(obj, {
67+
message: 'foo',
68+
expect: 'bar',
69+
emitter: 'baz',
70+
cb: 'bif'
71+
})
72+
}
73+
},
74+
_frozen: false,
75+
timeout: 5,
76+
_timer: 123,
77+
freeze () { t.pass() },
78+
purge () { t.pass() }
79+
}
80+
const result = enqueue.call(q, 'foo', 'bar', 'baz', 'bif')
81+
t.true(result)
82+
})
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const flush = require('../../../../lib/client/request-queue/flush')
5+
6+
test('clears timer', async t => {
7+
t.plan(2)
8+
const q = {
9+
_timer: 123,
10+
_queue: {
11+
values () {
12+
return []
13+
},
14+
clear () {
15+
t.pass()
16+
}
17+
}
18+
}
19+
flush.call(q)
20+
t.is(q._timer, null)
21+
})
22+
23+
test('invokes callback with parameters', async t => {
24+
t.plan(6)
25+
const req = {
26+
message: 'foo',
27+
expect: 'bar',
28+
emitter: 'baz',
29+
cb: theCB
30+
}
31+
const q = {
32+
_timer: 123,
33+
_queue: {
34+
values () {
35+
return [req]
36+
},
37+
clear () {
38+
t.pass()
39+
}
40+
}
41+
}
42+
flush.call(q, (message, expect, emitter, cb) => {
43+
t.is(message, 'foo')
44+
t.is(expect, 'bar')
45+
t.is(emitter, 'baz')
46+
t.is(cb, theCB)
47+
})
48+
t.is(q._timer, null)
49+
50+
function theCB () {}
51+
})
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const purge = require('../../../../lib/client/request-queue/purge')
5+
6+
test('flushes the queue with timeout errors', async t => {
7+
t.plan(3)
8+
const q = {
9+
flush (func) {
10+
func('a', 'b', 'c', (err) => {
11+
t.ok(err)
12+
t.is(err.name, 'TimeoutError')
13+
t.is(err.message, 'request queue timeout')
14+
})
15+
}
16+
}
17+
purge.call(q)
18+
})

0 commit comments

Comments
 (0)