WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit bd4913f

Browse files
committed
implement nodeAddressMap for sentinel
1 parent a64134c commit bd4913f

File tree

4 files changed

+194
-8
lines changed

4 files changed

+194
-8
lines changed

docs/sentinel.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,51 @@ await sentinel.close();
2424

2525
In the above example, we configure the sentinel object to fetch the configuration for the database Redis Sentinel is monitoring as "sentinel-db" with one of the sentinels being located at `example:1234`, then using it like a regular Redis client.
2626

27+
## Node Address Map
28+
29+
A mapping between the addresses returned by sentinel and the addresses the client should connect to.
30+
Useful when the sentinel nodes are running on a different network to the client.
31+
32+
```javascript
33+
import { createSentinel } from 'redis';
34+
35+
// Use either a static mapping:
36+
const sentinel = await createSentinel({
37+
name: 'sentinel-db',
38+
sentinelRootNodes: [{
39+
host: 'example',
40+
port: 1234
41+
}],
42+
nodeAddressMap: {
43+
'10.0.0.1:6379': {
44+
host: 'external-host.io',
45+
port: 6379
46+
},
47+
'10.0.0.2:6379': {
48+
host: 'external-host.io',
49+
port: 6380
50+
}
51+
}
52+
}).connect();
53+
54+
// or create the mapping dynamically, as a function:
55+
const sentinel = await createSentinel({
56+
name: 'sentinel-db',
57+
sentinelRootNodes: [{
58+
host: 'example',
59+
port: 1234
60+
}],
61+
nodeAddressMap(address) {
62+
const [host, port] = address.split(':');
63+
64+
return {
65+
host: `external-${host}.io`,
66+
port: Number(port)
67+
};
68+
}
69+
}).connect();
70+
```
71+
2772
## `createSentinel` configuration
2873

2974
| Property | Default | Description |
@@ -35,6 +80,7 @@ In the above example, we configure the sentinel object to fetch the configuratio
3580
| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with |
3681
| masterPoolSize | `1` | The number of clients connected to the master node |
3782
| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. |
83+
| nodeAddressMap | | Defines the [node address mapping](#node-address-map) |
3884
| scanInterval | `10000` | Interval in milliseconds to periodically scan for changes in the sentinel topology. The client will query the sentinel for changes at this interval. |
3985
| passthroughClientErrorEvents | `false` | When `true`, error events from client instances inside the sentinel will be propagated to the sentinel instance. This allows handling all client errors through a single error handler on the sentinel instance. |
4086
| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. |

packages/client/lib/sentinel/index.ts

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import RedisClient, { RedisClientOptions, RedisClientType } from '../client';
44
import { CommandOptions } from '../client/commands-queue';
55
import { attachConfig } from '../commander';
66
import COMMANDS from '../commands';
7-
import { ClientErrorEvent, NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelOptions, RedisSentinelType, SentinelCommander } from './types';
7+
import { ClientErrorEvent, NamespaceProxySentinel, NamespaceProxySentinelClient, NodeAddressMap, ProxySentinel, ProxySentinelClient, RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelOptions, RedisSentinelType, SentinelCommander } from './types';
88
import { clientSocketToNode, createCommand, createFunctionCommand, createModuleCommand, createNodeList, createScriptCommand, parseNode } from './utils';
99
import { RedisMultiQueuedCommand } from '../multi-command';
1010
import RedisSentinelMultiCommand, { RedisSentinelMultiCommandType } from './multi-commands';
@@ -623,6 +623,7 @@ class RedisSentinelInternal<
623623
readonly #name: string;
624624
readonly #nodeClientOptions: RedisClientOptions<M, F, S, RESP, TYPE_MAPPING, RedisTcpSocketOptions>;
625625
readonly #sentinelClientOptions: RedisClientOptions<typeof RedisSentinelModule, RedisFunctions, RedisScripts, RespVersions, TypeMapping, RedisTcpSocketOptions>;
626+
readonly #nodeAddressMap?: NodeAddressMap;
626627
readonly #scanInterval: number;
627628
readonly #passthroughClientErrorEvents: boolean;
628629
readonly #RESP?: RespVersions;
@@ -679,6 +680,7 @@ class RedisSentinelInternal<
679680
this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16;
680681
this.#masterPoolSize = options.masterPoolSize ?? 1;
681682
this.#replicaPoolSize = options.replicaPoolSize ?? 0;
683+
this.#nodeAddressMap = options.nodeAddressMap;
682684
this.#scanInterval = options.scanInterval ?? 0;
683685
this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false;
684686

@@ -716,16 +718,30 @@ class RedisSentinelInternal<
716718
);
717719
}
718720

721+
#getNodeAddress(address: string): RedisNode | undefined {
722+
switch (typeof this.#nodeAddressMap) {
723+
case 'object':
724+
return this.#nodeAddressMap[address];
725+
726+
case 'function':
727+
return this.#nodeAddressMap(address);
728+
}
729+
}
730+
719731
#createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: false) {
732+
const address = `${node.host}:${node.port}`;
733+
const socket =
734+
this.#getNodeAddress(address) ??
735+
{ host: node.host, port: node.port };
720736
return RedisClient.create({
721737
//first take the globally set RESP
722738
RESP: this.#RESP,
723739
//then take the client options, which can in theory overwrite it
724740
...clientOptions,
725741
socket: {
726742
...clientOptions.socket,
727-
host: node.host,
728-
port: node.port,
743+
host: socket.host,
744+
port: socket.port,
729745
...(reconnectStrategy !== undefined && { reconnectStrategy })
730746
}
731747
});
@@ -1426,6 +1442,16 @@ export class RedisSentinelFactory extends EventEmitter {
14261442
this.#sentinelRootNodes = options.sentinelRootNodes;
14271443
}
14281444

1445+
#getNodeAddress(address: string): RedisNode | undefined {
1446+
switch (typeof this.options.nodeAddressMap) {
1447+
case 'object':
1448+
return this.options.nodeAddressMap[address];
1449+
1450+
case 'function':
1451+
return this.options.nodeAddressMap(address);
1452+
}
1453+
}
1454+
14291455
async updateSentinelRootNodes() {
14301456
for (const node of this.#sentinelRootNodes) {
14311457
const client = RedisClient.create({
@@ -1508,12 +1534,16 @@ export class RedisSentinelFactory extends EventEmitter {
15081534

15091535
async getMasterClient() {
15101536
const master = await this.getMasterNode();
1537+
const address = `${master.host}:${master.port}`;
1538+
const socket =
1539+
this.#getNodeAddress(address) ??
1540+
{ host: master.host, port: master.port };
15111541
return RedisClient.create({
15121542
...this.options.nodeClientOptions,
15131543
socket: {
15141544
...this.options.nodeClientOptions?.socket,
1515-
host: master.host,
1516-
port: master.port
1545+
host: socket.host,
1546+
port: socket.port
15171547
}
15181548
});
15191549
}
@@ -1576,12 +1606,17 @@ export class RedisSentinelFactory extends EventEmitter {
15761606
this.#replicaIdx = 0;
15771607
}
15781608

1609+
const replica = replicas[this.#replicaIdx];
1610+
const address = `${replica.host}:${replica.port}`;
1611+
const socket =
1612+
this.#getNodeAddress(address) ??
1613+
{ host: replica.host, port: replica.port };
15791614
return RedisClient.create({
15801615
...this.options.nodeClientOptions,
15811616
socket: {
15821617
...this.options.nodeClientOptions?.socket,
1583-
host: replicas[this.#replicaIdx].host,
1584-
port: replicas[this.#replicaIdx].port
1618+
host: socket.host,
1619+
port: socket.port
15851620
}
15861621
});
15871622
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { strict as assert } from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
import { NodeAddressMap } from './types';
4+
5+
describe('NodeAddressMap', () => {
6+
describe('type checking', () => {
7+
it('should accept object mapping', () => {
8+
const map: NodeAddressMap = {
9+
'10.0.0.1:6379': {
10+
host: 'external-host.io',
11+
port: 6379
12+
}
13+
};
14+
15+
assert.ok(map);
16+
});
17+
18+
it('should accept function mapping', () => {
19+
const map: NodeAddressMap = (address: string) => {
20+
const [host, port] = address.split(':');
21+
return {
22+
host: `external-${host}.io`,
23+
port: Number(port)
24+
};
25+
};
26+
27+
assert.ok(map);
28+
});
29+
});
30+
31+
describe('object mapping', () => {
32+
it('should map addresses correctly', () => {
33+
const map: NodeAddressMap = {
34+
'10.0.0.1:6379': {
35+
host: 'external-host.io',
36+
port: 6379
37+
},
38+
'10.0.0.2:6379': {
39+
host: 'external-host.io',
40+
port: 6380
41+
}
42+
};
43+
44+
assert.deepEqual(map['10.0.0.1:6379'], {
45+
host: 'external-host.io',
46+
port: 6379
47+
});
48+
49+
assert.deepEqual(map['10.0.0.2:6379'], {
50+
host: 'external-host.io',
51+
port: 6380
52+
});
53+
});
54+
});
55+
56+
describe('function mapping', () => {
57+
it('should map addresses dynamically', () => {
58+
const map: NodeAddressMap = (address: string) => {
59+
const [host, port] = address.split(':');
60+
return {
61+
host: `external-${host}.io`,
62+
port: Number(port)
63+
};
64+
};
65+
66+
const result1 = map('10.0.0.1:6379');
67+
assert.deepEqual(result1, {
68+
host: 'external-10.0.0.1.io',
69+
port: 6379
70+
});
71+
72+
const result2 = map('10.0.0.2:6380');
73+
assert.deepEqual(result2, {
74+
host: 'external-10.0.0.2.io',
75+
port: 6380
76+
});
77+
});
78+
79+
it('should return undefined for unmapped addresses', () => {
80+
const map: NodeAddressMap = (address: string) => {
81+
if (address.startsWith('10.0.0.')) {
82+
const [host, port] = address.split(':');
83+
return {
84+
host: `external-${host}.io`,
85+
port: Number(port)
86+
};
87+
}
88+
return undefined;
89+
};
90+
91+
const result = map('192.168.1.1:6379');
92+
assert.equal(result, undefined);
93+
});
94+
});
95+
});
96+

packages/client/lib/sentinel/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export interface RedisNode {
1111
port: number;
1212
}
1313

14+
export type NodeAddressMap = {
15+
[address: string]: RedisNode;
16+
} | ((address: string) => RedisNode | undefined);
17+
1418
export interface RedisSentinelOptions<
1519
M extends RedisModules = RedisModules,
1620
F extends RedisFunctions = RedisFunctions,
@@ -49,10 +53,15 @@ export interface RedisSentinelOptions<
4953
* When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes.
5054
*/
5155
replicaPoolSize?: number;
56+
/**
57+
* Mapping between the addresses returned by sentinel and the addresses the client should connect to
58+
* Useful when the sentinel nodes are running on another network
59+
*/
60+
nodeAddressMap?: NodeAddressMap;
5261
/**
5362
* Interval in milliseconds to periodically scan for changes in the sentinel topology.
5463
* The client will query the sentinel for changes at this interval.
55-
*
64+
*
5665
* Default: 10000 (10 seconds)
5766
*/
5867
scanInterval?: number;

0 commit comments

Comments
 (0)