From b9c47938bb3914d8dae5db17eef7ffdab0dd0399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=90=E9=AA=85?= Date: Wed, 17 Oct 2018 19:55:24 +0800 Subject: [PATCH] feat: support customize dns lookup function (#723) Since Redis cluster doesn't support hostname at all (antirez/redis#2410), it's reasonable to resolve the hostnames to IPs before connecting. --- lib/cluster/ClusterOptions.ts | 23 +++- lib/cluster/index.ts | 166 +++++++++++++++++------- lib/cluster/util.ts | 15 ++- lib/types.ts | 2 +- lib/utils/index.ts | 8 ++ test/functional/cluster/connect.js | 9 ++ test/functional/cluster/dnsLookup.js | 58 +++++++++ test/functional/cluster/quit.js | 2 + test/functional/connection.js | 16 ++- test/functional/maxRetriesPerRequest.js | 4 + test/functional/pipeline.js | 17 +++ test/functional/scan_stream.js | 22 +--- test/functional/scripting.js | 10 ++ test/functional/select.js | 4 + 14 files changed, 286 insertions(+), 70 deletions(-) create mode 100644 test/functional/cluster/dnsLookup.js diff --git a/lib/cluster/ClusterOptions.ts b/lib/cluster/ClusterOptions.ts index de29f366..3fe9c8e6 100644 --- a/lib/cluster/ClusterOptions.ts +++ b/lib/cluster/ClusterOptions.ts @@ -1,4 +1,7 @@ import {NodeRole} from './util' +import {lookup} from 'dns' + +export type DNSLookupFunction = (hostname: string, callback: (err: NodeJS.ErrnoException, address: string, family: number) => void) => void /** * Options for Cluster constructor @@ -93,9 +96,26 @@ export interface IClusterOptions { redisOptions?: any /** + * By default, When a new Cluster instance is created, + * it will connect to the Redis cluster automatically. + * If you want to keep the instance disconnected until the first command is called, + * set this option to `true`. + * * @default false */ lazyConnect?: boolean + + /** + * Hostnames will be resolved to IP addresses via this function. + * This is needed when the addresses of startup nodes are hostnames instead + * of IPs. + * + * You may provide a custom `lookup` function when you want to customize + * the cache behavior of the default function. + * + * @default require('dns').lookup + */ + dnsLookup?: DNSLookupFunction } export const DEFAULT_CLUSTER_OPTIONS: IClusterOptions = { @@ -108,5 +128,6 @@ export const DEFAULT_CLUSTER_OPTIONS: IClusterOptions = { retryDelayOnClusterDown: 100, retryDelayOnTryAgain: 100, slotsRefreshTimeout: 1000, - slotsRefreshInterval: 5000 + slotsRefreshInterval: 5000, + dnsLookup: lookup } diff --git a/lib/cluster/index.ts b/lib/cluster/index.ts index d6453d94..234821ac 100644 --- a/lib/cluster/index.ts +++ b/lib/cluster/index.ts @@ -2,16 +2,16 @@ import {EventEmitter} from 'events' import ClusterAllFailedError from '../errors/ClusterAllFailedError' import {defaults, noop} from '../utils/lodash' import ConnectionPool from './ConnectionPool' -import {NodeKey, IRedisOptions, normalizeNodeOptions, NodeRole} from './util' +import {NodeKey, IRedisOptions, normalizeNodeOptions, NodeRole, getUniqueHostnamesFromOptions} from './util' import ClusterSubscriber from './ClusterSubscriber' import DelayQueue from './DelayQueue' import ScanStream from '../ScanStream' -import {AbortError} from 'redis-errors' +import {AbortError, RedisError} from 'redis-errors' import * as asCallback from 'standard-as-callback' import * as PromiseContainer from '../promiseContainer' import {CallbackFunction} from '../types'; import {IClusterOptions, DEFAULT_CLUSTER_OPTIONS} from './ClusterOptions' -import {sample, CONNECTION_CLOSED_ERROR_MSG, shuffle, timeout} from '../utils' +import {sample, CONNECTION_CLOSED_ERROR_MSG, shuffle, timeout, zipMap} from '../utils' import * as commands from 'redis-commands' const Deque = require('denque') @@ -30,7 +30,7 @@ type ClusterStatus = 'end' | 'close' | 'wait' | 'connecting' | 'connect' | 'read */ class Cluster extends EventEmitter { private options: IClusterOptions - private startupNodes: IRedisOptions[] + private startupNodes: Array private connectionPool: ConnectionPool private slots: Array = [] private manuallyClosing: boolean @@ -43,6 +43,18 @@ class Cluster extends EventEmitter { private status: ClusterStatus private isRefreshing: boolean = false + /** + * Every time Cluster#connect() is called, this value will be + * auto-incrementing. The purpose of this value is used for + * discarding previous connect attampts when creating a new + * connection. + * + * @private + * @type {number} + * @memberof Cluster + */ + private connectionEpoch: number = 0 + /** * Creates an instance of Cluster. * @@ -54,7 +66,7 @@ class Cluster extends EventEmitter { super() Commander.call(this) - this.startupNodes = normalizeNodeOptions(startupNodes) + this.startupNodes = startupNodes this.options = defaults(this.options, options, DEFAULT_CLUSTER_OPTIONS) // validate options @@ -117,59 +129,68 @@ class Cluster extends EventEmitter { reject(new Error('Redis is already connecting/connected')) return } + const epoch = ++this.connectionEpoch this.setStatus('connecting') - if (!Array.isArray(this.startupNodes) || this.startupNodes.length === 0) { - throw new Error('`startupNodes` should contain at least one node.') - } - - this.connectionPool.reset(this.startupNodes) - - function readyHandler() { - this.setStatus('ready') - this.retryAttempts = 0 - this.executeOfflineCommands() - this.resetNodesRefreshInterval() - resolve() - } + this.resolveStartupNodeHostnames().then((nodes) => { + if (this.connectionEpoch !== epoch) { + debug('discard connecting after resolving startup nodes because epoch not match: %d != %d', epoch, this.connectionEpoch) + reject(new RedisError('Connection is discarded because a new connection is made')) + return + } + if (this.status !== 'connecting') { + debug('discard connecting after resolving startup nodes because the status changed to %s', this.status) + reject(new RedisError('Connection is aborted')) + return + } + this.connectionPool.reset(nodes) + + function readyHandler() { + this.setStatus('ready') + this.retryAttempts = 0 + this.executeOfflineCommands() + this.resetNodesRefreshInterval() + resolve() + } - let closeListener: () => void - const refreshListener = () => { - this.removeListener('close', closeListener) - this.manuallyClosing = false - this.setStatus('connect') - if (this.options.enableReadyCheck) { - this.readyCheck((err, fail) => { - if (err || fail) { - debug('Ready check failed (%s). Reconnecting...', err || fail) - if (this.status === 'connect') { - this.disconnect(true) + let closeListener: () => void + const refreshListener = () => { + this.removeListener('close', closeListener) + this.manuallyClosing = false + this.setStatus('connect') + if (this.options.enableReadyCheck) { + this.readyCheck((err, fail) => { + if (err || fail) { + debug('Ready check failed (%s). Reconnecting...', err || fail) + if (this.status === 'connect') { + this.disconnect(true) + } + } else { + readyHandler.call(this) } - } else { - readyHandler.call(this) - } - }) - } else { - readyHandler.call(this) + }) + } else { + readyHandler.call(this) + } } - } - closeListener = function () { - this.removeListener('refresh', refreshListener) - reject(new Error('None of startup nodes is available')) - } + closeListener = function () { + this.removeListener('refresh', refreshListener) + reject(new Error('None of startup nodes is available')) + } - this.once('refresh', refreshListener) - this.once('close', closeListener) - this.once('close', this.handleCloseEvent.bind(this)) + this.once('refresh', refreshListener) + this.once('close', closeListener) + this.once('close', this.handleCloseEvent.bind(this)) - this.refreshSlotsCache(function (err) { - if (err && err.message === 'Failed to refresh slots cache.') { - Redis.prototype.silentEmit.call(this, 'error', err) - this.connectionPool.reset([]) - } - }.bind(this)) - this.subscriber.start() + this.refreshSlotsCache(function (err) { + if (err && err.message === 'Failed to refresh slots cache.') { + Redis.prototype.silentEmit.call(this, 'error', err) + this.connectionPool.reset([]) + } + }.bind(this)) + this.subscriber.start() + }).catch(reject) }) } @@ -639,6 +660,51 @@ class Cluster extends EventEmitter { } }) } + + private dnsLookup (hostname: string): Promise { + return new Promise((resolve, reject) => { + this.options.dnsLookup(hostname, (err, address) => { + if (err) { + debug('failed to resolve hostname %s to IP: %s', hostname, err.message) + reject(err) + } else { + debug('resolved hostname %s to IP %s', hostname, address) + resolve(address) + } + }) + }); + } + + /** + * Normalize startup nodes, and resolving hostnames to IPs. + * + * This process happens every time when #connect() is called since + * #startupNodes and DNS records may chanage. + * + * @private + * @returns {Promise} + */ + private resolveStartupNodeHostnames(): Promise { + if (!Array.isArray(this.startupNodes) || this.startupNodes.length === 0) { + return Promise.reject(new Error('`startupNodes` should contain at least one node.')) + } + const startupNodes = normalizeNodeOptions(this.startupNodes) + + const hostnames = getUniqueHostnamesFromOptions(startupNodes) + if (hostnames.length === 0) { + return Promise.resolve(startupNodes) + } + + return Promise.all(hostnames.map((hostname) => this.dnsLookup(hostname))).then((ips) => { + const hostnameToIP = zipMap(hostnames, ips) + + return startupNodes.map((node) => ( + hostnameToIP.has(node.host) + ? Object.assign({}, node, {host: hostnameToIP.get(node.host)}) + : node + )) + }) + } } Object.getOwnPropertyNames(Commander.prototype).forEach(name => { diff --git a/lib/cluster/util.ts b/lib/cluster/util.ts index 33b24ae4..e40a73cb 100644 --- a/lib/cluster/util.ts +++ b/lib/cluster/util.ts @@ -1,11 +1,15 @@ import {parseURL} from '../utils' +import {isIP} from 'net' +import {DNSLookupFunction} from './ClusterOptions'; export type NodeKey = string export type NodeRole = 'master' | 'slave' | 'all' export interface IRedisOptions { port: number, - host: string + host: string, + password?: string, + [key: string]: any } export function getNodeKey(node: IRedisOptions): NodeKey { @@ -43,3 +47,12 @@ export function normalizeNodeOptions(nodes: Array): IR return options }) } + +export function getUniqueHostnamesFromOptions (nodes: IRedisOptions[]): string[] { + const uniqueHostsMap = {} + nodes.forEach((node) => { + uniqueHostsMap[node.host] = true + }) + + return Object.keys(uniqueHostsMap).filter(host => !isIP(host)) +} diff --git a/lib/types.ts b/lib/types.ts index 939370c3..0df54d8b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -1 +1 @@ -export type CallbackFunction = (err?: Error | null, result?: T) => void +export type CallbackFunction = (err?: NodeJS.ErrnoException | null, result?: T) => void diff --git a/lib/utils/index.ts b/lib/utils/index.ts index f3f91486..4ee809b2 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -325,3 +325,11 @@ export function shuffle (array: T[]): T[] { * Error message for connection being disconnected */ export const CONNECTION_CLOSED_ERROR_MSG = 'Connection is closed.' + +export function zipMap (keys: K[], values: V[]): Map { + const map = new Map() + keys.forEach((key, index) => { + map.set(key, values[index]) + }) + return map +} diff --git a/test/functional/cluster/connect.js b/test/functional/cluster/connect.js index 49714610..29a8826e 100644 --- a/test/functional/cluster/connect.js +++ b/test/functional/cluster/connect.js @@ -353,4 +353,13 @@ describe('cluster:connect', function () { { host: '127.0.0.1', port: '30001' } ], { slotsRefreshInterval: 100, redisOptions: { lazyConnect: false } }); }); + + it('throws when startupNodes is empty', (done) => { + const cluster = new Redis.Cluster(null, {lazyConnect: true}) + cluster.connect().catch(err => { + expect(err.message).to.eql('`startupNodes` should contain at least one node.') + cluster.disconnect() + done() + }) + }) }); diff --git a/test/functional/cluster/dnsLookup.js b/test/functional/cluster/dnsLookup.js new file mode 100644 index 00000000..da493598 --- /dev/null +++ b/test/functional/cluster/dnsLookup.js @@ -0,0 +1,58 @@ +describe('cluster:dnsLookup', () => { + it('resolve hostnames to IPs', (done) => { + const slotTable = [ + [0, 1000, ['127.0.0.1', 30001]], + [1001, 16383, ['127.0.0.1', 30002]] + ] + new MockServer(30001, (argv, c) => { + }, slotTable) + new MockServer(30002, (argv, c) => { + }, slotTable) + + const cluster = new Redis.Cluster([ + { host: 'localhost', port: '30001' } + ]) + cluster.on('ready', () => { + const nodes = cluster.nodes('master') + expect(nodes.length).to.eql(2) + expect(nodes[0].options.host).to.eql('127.0.0.1') + expect(nodes[1].options.host).to.eql('127.0.0.1') + cluster.disconnect() + done() + }) + }) + + it('support customize dnsLookup function', (done) => { + let dnsLookupCalledCount = 0 + const slotTable = [ + [0, 1000, ['127.0.0.1', 30001]], + [1001, 16383, ['127.0.0.1', 30002]] + ] + new MockServer(30001, (argv, c) => { + }, slotTable) + new MockServer(30002, (argv, c) => { + }, slotTable) + + const cluster = new Redis.Cluster([ + { host: 'a.com', port: '30001' } + ], { + dnsLookup (hostname, callback) { + dnsLookupCalledCount += 1 + if (hostname === 'a.com') { + callback(null, '127.0.0.1') + } else { + callback(new Error('Unknown hostname')) + } + } + }) + cluster.on('ready', () => { + const nodes = cluster.nodes('master') + expect(nodes.length).to.eql(2) + expect(nodes[0].options.host).to.eql('127.0.0.1') + expect(nodes[1].options.host).to.eql('127.0.0.1') + expect(dnsLookupCalledCount).to.eql(1) + cluster.disconnect() + done() + }) + }) +}) diff --git a/test/functional/cluster/quit.js b/test/functional/cluster/quit.js index ebd2d3f1..fdc6d98e 100644 --- a/test/functional/cluster/quit.js +++ b/test/functional/cluster/quit.js @@ -23,6 +23,7 @@ describe('cluster:quit', () => { cluster.quit((err, res) => { expect(err).to.eql(null) expect(res).to.eql('OK') + cluster.disconnect() done() }) }) @@ -51,6 +52,7 @@ describe('cluster:quit', () => { cluster.on('ready', () => { cluster.quit((err) => { expect(err.message).to.eql(ERROR_MESSAGE) + cluster.disconnect() done() }) }) diff --git a/test/functional/connection.js b/test/functional/connection.js index fb033d03..dc691c25 100644 --- a/test/functional/connection.js +++ b/test/functional/connection.js @@ -27,6 +27,7 @@ describe('connection', function () { expect(command.name).to.eql('auth'); } else if (times === 2) { expect(command.name).to.eql('info'); + redis.disconnect(); done(); } }); @@ -39,6 +40,7 @@ describe('connection', function () { }); redis.get('foo', function (err, res) { expect(res).to.eql('bar'); + redis.disconnect(); done(); }); }); @@ -58,6 +60,7 @@ describe('connection', function () { redis.get('foo', function (err) { expect(err.message).to.match(/Connection is closed/); if (!--pending) { + redis.disconnect(); done(); } }); @@ -69,6 +72,7 @@ describe('connection', function () { stub(redis.stream, 'setTimeout', function (timeout) { expect(timeout).to.eql(0); redis.stream.setTimeout.restore(); + redis.disconnect(); done(); }); }); @@ -115,6 +119,7 @@ describe('connection', function () { var redis = new Redis(); redis.connect().catch(function (err) { expect(err.message).to.match(/Redis is already connecting/); + redis.disconnect() done(); }); }); @@ -123,6 +128,7 @@ describe('connection', function () { var redis = new Redis({ lazyConnect: true }); redis.connect().then(function () { expect(redis.status).to.eql('ready'); + redis.disconnect() done(); }); }); @@ -138,6 +144,7 @@ describe('connection', function () { redis.connect().catch(function () { expect(redis.status).to.eql('reconnecting'); + redis.disconnect() done(); }); }); @@ -151,6 +158,7 @@ describe('connection', function () { redis.connect().catch(function () { expect(redis.status).to.eql('end'); + redis.disconnect() done(); }); }); @@ -178,6 +186,7 @@ describe('connection', function () { retryStrategy: function () { process.nextTick(function () { expect(redis.status).to.eql('end'); + redis.disconnect() done(); }); return null; @@ -197,12 +206,12 @@ describe('connection', function () { redis.quit().then(function (result) { expect(result).to.eql('OK'); expect(count).to.eql(0); + redis.disconnect() done(); }); }); it('should skip reconnecting if quitting before connecting (buffer)', function (done) { - var count = 0; var redis = new Redis({ port: 8999, retryStrategy: function () { @@ -213,6 +222,7 @@ describe('connection', function () { redis.quitBuffer().then(function (result) { expect(result).to.be.instanceof(Buffer); expect(result.toString()).to.eql('OK'); + redis.disconnect() done(); }); }); @@ -224,6 +234,7 @@ describe('connection', function () { redis.once('ready', function () { redis.client('getname', function (err, res) { expect(res).to.eql('niceName'); + redis.disconnect() done(); }); }); @@ -238,6 +249,7 @@ describe('connection', function () { redis.unsubscribe('l', function () { redis.client('getname', function (err, res) { expect(res).to.eql('niceName'); + redis.disconnect() done(); }); }); @@ -275,6 +287,7 @@ describe('connection', function () { expect(res[0]).to.eql('l'); expect(res[1]).to.eql('1'); if (!--pending) { + redis.disconnect() done(); } }); @@ -304,6 +317,7 @@ describe('connection', function () { redis.unsubscribe('l', function() { pub.pubsub('channels', function(err, channels){ expect(channels.length).to.eql(channelsBefore.length); + redis.disconnect() done(); }); }); diff --git a/test/functional/maxRetriesPerRequest.js b/test/functional/maxRetriesPerRequest.js index 842d7bbc..850b3b42 100644 --- a/test/functional/maxRetriesPerRequest.js +++ b/test/functional/maxRetriesPerRequest.js @@ -11,6 +11,7 @@ describe('maxRetriesPerRequest', function () { }); redis.get('foo', (err) => { expect(err).instanceOf(MaxRetriesPerRequestError) + redis.disconnect() done() }) }) @@ -26,6 +27,7 @@ describe('maxRetriesPerRequest', function () { expect(redis.retryAttempts).to.eql(21) redis.get('foo', () => { expect(redis.retryAttempts).to.eql(42) + redis.disconnect() done() }) }) @@ -42,6 +44,7 @@ describe('maxRetriesPerRequest', function () { expect(redis.retryAttempts).to.eql(2) redis.get('foo', () => { expect(redis.retryAttempts).to.eql(4) + redis.disconnect() done() }) }) @@ -58,6 +61,7 @@ describe('maxRetriesPerRequest', function () { expect(redis.retryAttempts).to.eql(1) redis.get('foo', () => { expect(redis.retryAttempts).to.eql(2) + redis.disconnect() done() }) }) diff --git a/test/functional/pipeline.js b/test/functional/pipeline.js index bce003ea..b06a480a 100644 --- a/test/functional/pipeline.js +++ b/test/functional/pipeline.js @@ -12,6 +12,7 @@ describe('pipeline', function () { [null, 3], [null, '3'] ]); + redis.disconnect() done(); }); }); @@ -21,6 +22,7 @@ describe('pipeline', function () { redis.pipeline().exec(function (err, results) { expect(err).to.eql(null); expect(results).to.eql([]); + redis.disconnect() done(); }); }); @@ -38,6 +40,7 @@ describe('pipeline', function () { [null, Buffer.from('bar')], [null, 'bar'] ]); + redis.disconnect() done(); }); }); @@ -49,6 +52,7 @@ describe('pipeline', function () { expect(results.length).to.eql(1); expect(results[0].length).to.eql(1); expect(results[0][0].toString()).to.match(/wrong number of arguments/); + redis.disconnect() done(); }); }); @@ -62,6 +66,7 @@ describe('pipeline', function () { }).exec(function (err, results) { expect(pending).to.eql(0); expect(results[1][1]).to.eql('bar'); + redis.disconnect() done(); }); }); @@ -74,6 +79,7 @@ describe('pipeline', function () { expect(result[1][1]).to.eql('QUEUED'); expect(result[2][1]).to.eql('QUEUED'); expect(result[3][1]).to.eql(['OK', 'bar']); + redis.disconnect() done(); }); }); @@ -82,6 +88,7 @@ describe('pipeline', function () { var redis = new Redis({ showFriendlyErrorStack: true }); var pipeline = redis.pipeline(); expect(pipeline.options).to.have.property('showFriendlyErrorStack', true); + redis.disconnect() }); it('should support key prefixing', function (done) { @@ -96,6 +103,7 @@ describe('pipeline', function () { [null, 'test1'], [null, ['foo:bar']] ]); + redis.disconnect() done(); }); }); @@ -111,6 +119,10 @@ describe('pipeline', function () { }); }); + after(function () { + redis.disconnect() + }) + it('should work', function (done) { redis.pipeline().echo('foo', 'bar', '123', 'abc').exec(function (err, results) { expect(err).to.eql(null); @@ -168,6 +180,7 @@ describe('pipeline', function () { ]).exec(function (err, results) { expect(pending).to.eql(0); expect(results[1][1]).to.eql('bar'); + redis.disconnect() done(); }); }); @@ -180,6 +193,7 @@ describe('pipeline', function () { redis.set('foo', 'bar'); redis.get('foo'); redis.exec().then(function () { + redis.disconnect() done(); }); }); @@ -188,6 +202,7 @@ describe('pipeline', function () { var redis = new Redis(); redis.exec().catch(function (err) { expect(err.message).to.eql('ERR EXEC without MULTI'); + redis.disconnect() done(); }); }); @@ -202,6 +217,7 @@ describe('pipeline', function () { expect(typeof res[0][1]).to.eql('string'); expect(res[1][0]).to.eql(null); expect(Array.isArray(res[1][1])).to.eql(true); + redis.disconnect() done(); }); }); @@ -220,6 +236,7 @@ describe('pipeline', function () { ['get', 'foo'] ]); expect(pipeline2.length).to.eql(2); + redis.disconnect() }); }); }); diff --git a/test/functional/scan_stream.js b/test/functional/scan_stream.js index 9b1a9dda..cf5be588 100644 --- a/test/functional/scan_stream.js +++ b/test/functional/scan_stream.js @@ -20,6 +20,7 @@ describe('*scanStream', function () { }); stream.on('end', function () { expect(keys.sort()).to.eql(['foo1', 'foo10', 'foo2', 'foo3', 'foo4']); + redis.disconnect(); done(); }); }); @@ -37,6 +38,7 @@ describe('*scanStream', function () { }); stream.on('end', function () { expect(keys).to.eql(['foo10']); + redis.disconnect(); done(); }); }); @@ -66,6 +68,7 @@ describe('*scanStream', function () { }); stream.on('end', function () { expect(keys.sort()).to.eql(['foo1', 'foo10', 'foo2', 'foo3', 'foo4']); + redis.disconnect(); done(); }); }); @@ -100,6 +103,7 @@ describe('*scanStream', function () { stream.on('end', function () { expect(keys.sort()).to.eql([Buffer.from('foo1'), Buffer.from('foo10'), Buffer.from('foo2'), Buffer.from('foo3'), Buffer.from('foo4')]); + redis.disconnect(); done(); }); }); @@ -117,6 +121,7 @@ describe('*scanStream', function () { }); stream.on('end', function () { expect(keys).to.eql(['foo10']); + redis.disconnect(); done(); }); }); @@ -160,25 +165,10 @@ describe('*scanStream', function () { stream.on('end', function () { expect(keys).to.eql(serverKeys); cluster.disconnect(); - disconnect([node1, node2, node3], done); + done(); }); }); }); }); }); - -function disconnect(clients, callback) { - var pending = 0; - - for (var i = 0; i < clients.length; ++i) { - pending += 1; - clients[i].disconnect(check); - } - - function check() { - if (!--pending && callback) { - callback(); - } - } -} diff --git a/test/functional/scripting.js b/test/functional/scripting.js index 2ec805b5..2cbcb502 100644 --- a/test/functional/scripting.js +++ b/test/functional/scripting.js @@ -12,6 +12,7 @@ describe('scripting', function () { redis.test('k1', 'k2', 'a1', 'a2', function (err, result) { expect(result).to.eql(['k1', 'k2', 'a1', 'a2']); + redis.disconnect(); done(); }); }); @@ -25,6 +26,7 @@ describe('scripting', function () { redis.test(2, 'k1', 'k2', 'a1', 'a2', function (err, result) { expect(result).to.eql(['k1', 'k2', 'a1', 'a2']); + redis.disconnect(); done(); }); }); @@ -39,6 +41,7 @@ describe('scripting', function () { redis.test('2', 'a2', function (err, result) { expect(result).to.eql(['2', 'a2']); + redis.disconnect(); done(); }); }); @@ -53,6 +56,7 @@ describe('scripting', function () { redis.test('k1', 'k2', 'a1', 'a2', function (err, result) { expect(err).to.be.instanceof(Error); expect(err.toString()).to.match(/value is not an integer/); + redis.disconnect(); done(); }); }); @@ -68,6 +72,7 @@ describe('scripting', function () { redis.testBuffer('k1', 'k2', 'a1', 'a2', function (err, result) { expect(result).to.eql([Buffer.from('k1'), Buffer.from('k2'), Buffer.from('a1'), Buffer.from('a2')]); + redis.disconnect(); done(); }); }); @@ -82,6 +87,7 @@ describe('scripting', function () { redis.pipeline().set('test', 'pipeline').test('test').exec(function (err, results) { expect(results).to.eql([[null, 'OK'], [null, 'pipeline']]); + redis.disconnect(); done(); }); }); @@ -97,6 +103,7 @@ describe('scripting', function () { expect(err).to.eql(null); expect(results[1][0]).to.be.instanceof(Error); expect(results[1][0].toString()).to.match(/value is not an integer/); + redis.disconnect(); done(); }); }); @@ -143,6 +150,7 @@ describe('scripting', function () { expect(name).to.eql(command[0]); if (!expectedComands.length) { monitor.disconnect(); + redis.disconnect(); done(); } }); @@ -174,6 +182,7 @@ describe('scripting', function () { expect(name).to.eql(command[0]); if (!expectedComands.length) { monitor.disconnect(); + redis.disconnect(); done(); } }); @@ -193,6 +202,7 @@ describe('scripting', function () { redis.echo('k1', 'k2', 'a1', 'a2', function (err, result) { expect(result).to.eql(['foo:k1', 'foo:k2', 'a1', 'a2']); + redis.disconnect(); done(); }); }); diff --git a/test/functional/select.js b/test/functional/select.js index 83510b03..6f02338b 100644 --- a/test/functional/select.js +++ b/test/functional/select.js @@ -7,6 +7,7 @@ describe('select', function () { redis.select('2'); redis.get('foo', function (err, res) { expect(res).to.eql('2'); + redis.disconnect(); done(); }); }); @@ -24,6 +25,7 @@ describe('select', function () { redis.select('3'); redis.get('foo', function (err, res) { expect(res).to.eql('3'); + redis.disconnect(); done(); }); }); @@ -41,6 +43,7 @@ describe('select', function () { redis.stream.destroy(); redis.get('foo', function (err, res) { expect(res).to.eql('2'); + redis.disconnect(); done(); }); }); @@ -59,6 +62,7 @@ describe('select', function () { expect(changes).to.eql([2, 4]); redis.select('4', function () { expect(changes).to.eql([2, 4]); + redis.disconnect(); done(); }); });