diff --git a/.gitignore b/.gitignore index f8b21dfd..5c0edfae 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules *.cpuprofile /test.js +/.idea built .vscode diff --git a/lib/connectors/SentinelConnector/index.ts b/lib/connectors/SentinelConnector/index.ts index 6f103cfa..a7105a6c 100644 --- a/lib/connectors/SentinelConnector/index.ts +++ b/lib/connectors/SentinelConnector/index.ts @@ -1,4 +1,5 @@ import {createConnection, Socket} from 'net' +import {NatMap} from '../../cluster/ClusterOptions'; import {CONNECTION_CLOSED_ERROR_MSG, packObject, sample} from '../../utils' import {connect as createTLSConnection, TLSSocket, SecureContextOptions} from 'tls' import {ITcpConnectionOptions, isIIpcConnectionOptions} from '../StandaloneConnector' @@ -30,6 +31,7 @@ interface ISentinelConnectionOptions extends ITcpConnectionOptions { connectTimeout?: number enableTLSForSentinelMode?: boolean sentinelTLS?: SecureContextOptions + natMap: NatMap updateSentinels?: boolean } @@ -152,7 +154,7 @@ export default class SentinelConnector extends AbstractConnector { result.map(packObject as (value: any) => IAddressFromResponse).forEach(sentinel => { const flags = sentinel.flags ? sentinel.flags.split(',') : [] if (flags.indexOf('disconnected') === -1 && sentinel.ip && sentinel.port) { - const endpoint = addressResponseToAddress(sentinel) + const endpoint = this.sentinelNatResolve(addressResponseToAddress(sentinel)) if (this.sentinelIterator.add(endpoint)) { debug('adding sentinel %s:%s', endpoint.host, endpoint.port) } @@ -174,7 +176,10 @@ export default class SentinelConnector extends AbstractConnector { if (err) { return callback(err) } - callback(null, Array.isArray(result) ? { host: result[0], port: Number(result[1]) } : null) + + callback(null, this.sentinelNatResolve( + Array.isArray(result) ? { host: result[0], port: Number(result[1]) } : null + )) }) }) } @@ -194,10 +199,19 @@ export default class SentinelConnector extends AbstractConnector { slave.flags && !slave.flags.match(/(disconnected|s_down|o_down)/) )) - callback(null, selectPreferredSentinel(availableSlaves, this.options.preferredSlaves)) + callback(null, this.sentinelNatResolve( + selectPreferredSentinel(availableSlaves, this.options.preferredSlaves) + )) }) } + sentinelNatResolve (item: ISentinelAddress) { + if (!item || !this.options.natMap) + return item; + + return this.options.natMap[`${item.host}:${item.port}`] || item + } + private resolve (endpoint, callback: NodeCallback): void { if (typeof Redis === 'undefined') { Redis = require('../../redis') diff --git a/lib/redis.js b/lib/redis.js index 504ac7ab..d88feb61 100644 --- a/lib/redis.js +++ b/lib/redis.js @@ -88,6 +88,7 @@ var PromiseContainer = require('./promiseContainer'); * strings. This option is necessary when dealing with big numbers (exceed the [-2^53, +2^53] range). * @param {boolean} [options.enableTLSForSentinelMode=false] - Whether to support the `tls` option * when connecting to Redis via sentinel mode. + * @param {NatMap} [options.natMap=null] NAT map for sentinel connector. * @param {boolean} [options.updateSentinels=true] - Update the given `sentinels` list with new IP * addresses when communicating with existing sentinels. * @extends [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) @@ -174,6 +175,7 @@ Redis.defaultOptions = { sentinelRetryStrategy: function (times) { return Math.min(times * 10, 1000); }, + natMap: null, enableTLSForSentinelMode: false, updateSentinels: true, // Status diff --git a/test/functional/sentinel_nat.js b/test/functional/sentinel_nat.js new file mode 100644 index 00000000..e956fab4 --- /dev/null +++ b/test/functional/sentinel_nat.js @@ -0,0 +1,72 @@ +describe('sentinel_nat', function() { + it('connects to server as expected', function(done) { + + var sentinel = new MockServer(27379, function (argv) { + if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') { + return ['127.0.0.1', '17380']; + } + }) + + var redis = new Redis({ + sentinels: [ + { host: '127.0.0.1', port: '27379' } + ], + natMap: { + '127.0.0.1:17380': { + host: 'localhost', + port: 6379, + } + }, + name: 'master', + lazyConnect: true, + }) + + redis.connect(function(err) { + if (err) { + sentinel.disconnect(function() {}) + return done(err) + } + sentinel.disconnect(done) + }) + }) + + it('rejects connection if host is not defined in map', function(done) { + var sentinel = new MockServer(27379, function (argv) { + if (argv[0] === 'sentinel' && argv[1] === 'get-master-addr-by-name') { + return ['127.0.0.1', '17380'] + } + + if (argv[0] === 'sentinel' && argv[1] === 'sentinels' &&argv[2] === 'master') { + return ['127.0.0.1', '27379'] + } + }) + + var redis = new Redis({ + sentinels: [ + { host: '127.0.0.1', port: '27379' } + ], + natMap: { + '127.0.0.1:17381': { + host: 'localhost', + port: 6379, + } + }, + maxRetriesPerRequest: 1, + name: 'master', + lazyConnect: true, + }) + + redis + .connect() + .then(function() { + throw new Error("Should not call") + }) + .catch(function(err) { + if (err.message === 'Connection is closed.') { + return done(null) + } + sentinel.disconnect(done) + }) + }) + +}) \ No newline at end of file