Merge pull request #658 from wision/multi-servers

Client support multiple servers
This commit is contained in:
James Sumners 2020-09-20 20:04:12 -04:00 committed by GitHub
commit fadf3fcc64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 20 deletions

View File

@ -14,7 +14,7 @@ The code to create a new client looks like:
var ldap = require('ldapjs');
var client = ldap.createClient({
url: 'ldap://127.0.0.1:1389'
url: ['ldap://127.0.0.1:1389', 'ldap://127.0.0.2:1389']
});
You can use `ldap://` or `ldaps://`; the latter would connect over SSL (note
@ -24,7 +24,7 @@ client is:
|Attribute |Description |
|---------------|-----------------------------------------------------------|
|url |A valid LDAP URL (proto/host/port only) |
|url |A string or array of valid LDAP URL(s) (proto/host/port) |
|socketPath |Socket path if using AF\_UNIX sockets |
|log |A compatible logger instance (Default: no-op logger) |
|timeout |Milliseconds client should let operations live for before timing out (Default: Infinity)|
@ -34,6 +34,13 @@ client is:
|strictDN |Force strict DN parsing for client methods (Default is true)|
|reconnect |Try to reconnect when the connection gets lost (Default is false)|
### url
This parameter takes a single connection string or an array of connection strings
as an input. In case an array is provided, the client tries to connect to the
servers in given order. To achieve random server strategy (e.g. to distribute
the load among the servers), please shuffle the array before passing it as an
argument.
### Note On Logger
A passed in logger is expected to conform to the [Bunyan](https://www.npmjs.com/package/bunyan)

View File

@ -110,12 +110,13 @@ function Client (options) {
EventEmitter.call(this, options)
var self = this
var _url
if (options.url) { _url = url.parse(options.url) }
this.host = _url ? _url.hostname : undefined
this.port = _url ? _url.port : false
this.secure = _url ? _url.secure : false
this.url = _url
this.urls = options.url ? [].concat(options.url).map(url.parse) : []
this._nextServer = 0
// updated in connectSocket() after each connect
this.host = undefined
this.port = undefined
this.secure = undefined
this.url = undefined
this.tlsOptions = options.tlsOptions
this.socketPath = options.socketPath || false
@ -792,6 +793,9 @@ Client.prototype.connect = function connect () {
// Establish basic socket connection
function connectSocket (cb) {
var server = self.urls[self._nextServer]
self._nextServer = (self._nextServer + 1) % self.urls.length
cb = once(cb)
function onResult (err, res) {
@ -820,16 +824,17 @@ Client.prototype.connect = function connect () {
setupClient(cb)
}
var port = (self.port || self.socketPath)
if (self.secure) {
socket = tls.connect(port, self.host, self.tlsOptions)
var port = (server && server.port) || self.socketPath
var host = server && server.hostname
if (server && server.secure) {
socket = tls.connect(port, host, self.tlsOptions)
socket.once('secureConnect', onConnect)
} else {
socket = net.connect(port, self.host)
socket = net.connect(port, host)
socket.once('connect', onConnect)
}
socket.once('error', onResult)
initSocket()
initSocket(server)
// Setup connection timeout handling, if desired
if (self.connectTimeout) {
@ -844,9 +849,9 @@ Client.prototype.connect = function connect () {
}
// Initialize socket events and LDAP parser.
function initSocket () {
function initSocket (url) {
tracker = messageTrackerFactory({
id: self.url ? self.url.href : self.socketPath,
id: url ? url.href : self.socketPath,
parser: new Parser({ log: log })
})
@ -965,6 +970,13 @@ Client.prototype.connect = function connect () {
self.emit('socketTimeout')
socket.end()
})
var server = self.urls[self._nextServer]
if (server) {
self.host = server.hostname
self.port = server.port
self.secure = server.secure
}
}
var retry
@ -975,12 +987,15 @@ Client.prototype.connect = function connect () {
maxDelay: this.reconnect.maxDelay
})
failAfter = this.reconnect.failAfter
if (this.urls.length > 1 && failAfter) {
failAfter *= this.urls.length
}
} else {
retry = backoff.exponential({
initialDelay: 1,
maxDelay: 2
})
failAfter = 1
failAfter = this.urls.length || 1
}
retry.failAfter(failAfter)

View File

@ -7,7 +7,7 @@ module.exports = {
Client: Client,
createClient: function createClient (options) {
if (isObject(options) === false) throw TypeError('options (object) required')
if (options.url && typeof options.url !== 'string') throw TypeError('options.url (string) required')
if (options.url && typeof options.url !== 'string' && !Array.isArray(options.url)) throw TypeError('options.url (string|array) required')
if (options.socketPath && typeof options.socketPath !== 'string') throw TypeError('options.socketPath must be a string')
if ((options.url && options.socketPath) || !(options.url || options.socketPath)) throw TypeError('options.url ^ options.socketPath (String) required')
if (!options.log) options.log = logger

View File

@ -340,10 +340,9 @@ tap.test('createClient', t => {
t.throws(() => ldap.createClient(42), match)
})
t.test('url must be a string', async t => {
const match = /options\.url \(string\) required/
t.test('url must be a string or array', async t => {
const match = /options\.url \(string\|array\) required/
t.throws(() => ldap.createClient({ url: {} }), match)
t.throws(() => ldap.createClient({ url: [] }), match)
t.throws(() => ldap.createClient({ url: 42 }), match)
})
@ -379,6 +378,20 @@ tap.test('createClient', t => {
}
})
t.test('url array is correctly assigned', async t => {
getPort().then(function (unusedPortNumber) {
const client = ldap.createClient({
url: [
`ldap://127.0.0.1:${unusedPortNumber}`,
`ldap://127.0.0.2:${unusedPortNumber}`
],
connectTimeout: 1
})
t.equal(client.urls.length, 2)
})
})
// TODO: this test is really flaky. It would be better if we could validate
// the options _withouth_ having to connect to a server.
// t.test('attaches a child function to logger', async t => {