Skip to content

Commit 8f3a276

Browse files
sjpotterleibale
andauthored
Sentinel Support (#2664)
* redis client socket changes needed for sentinel * Sentinel Implementation [EXPERIMENTAL] * add pooling * improve typing with SENTINEL_ client members * cleanup - remove unused comments / commented code * small sendCommand change + revert change to tsconfig * add more sentinel commands needed for testing. * lots of fixups and a reasonable first pass test suite * add a timer option to update topology in background + don't need both sentinel client and pubsubclient + nits * format all the things * more progress * small cleanup * try to group promises together to minimize the internal await points * redo events, to keep a single topology event to listen on * nits + readme * add RedisSentinelFactory to provide lower level access to sentinel * nit * update * add RedisSentinelClient/Type for leased clients returned by aquire() used by function passed to use() * add self for private access + improve emitting * nit * nits * improve testing - improve steady state waiting between tests - get masternode from client, not from sentinels themselves (not consistent and then client isn't changing as we expect - provide extensive logging/tracing on test errors - provide a very low impact tracing mechanism withinthe code that only really impacts code when tracing is in use. * ismall nit for typing * bunch of changes - harden testing - don't use sentinel[0] for debug error dump as could be downed by a test - increase time for sentinel down test to 30s (caused a long taking failover) - add client-error even / don't pass throuh client errors as errors option for pubsub proxy - when passing through cient errors as error events, dont pass the event, but the Error object, as only Error objects are supposed to be on 'error' - * improve pub sub proxy. save the refference to all channel/pattern listeners up front on creation, dont hve to fetch the object each time, as it doesn't change. removes race condition between setting up the listener and the pub sub node going down and being recreated. * wrap the passed through RedisClient error to make clear where its coming from. * refactor sentinel object / factory tests apart * harden tests a little bit more * add pipeline test * add scripts/function tests + fixups / cleanups to get them to work * change to use redis-stack-server for redis nodes to enable module testing * fix test, forgot to return in use function with module * rename test * improve tests to test with redis/sentinel nodes with and withput passwords this tests that we are handling the nodeClientOptions and sentinelClientOptions correctly * cleanup for RedisSentinel type generic typing in tests * remove debugLog, just rely on traace mechanism * added multi tests for script/function/modules * don't emit errors on lease object, only on main object * improve testing * extract out common code to reduce duplication * nit * nits * nit * remove SENTINEL_... commands from main client, load them via module interface * missed adding RedisSentinelModule to correct places in RedisSentinelFactory * nits * fix test logging on error 1) it takes a lot of time now, so needs larger timeout 2) docker logs can be large, so need to increase maxBuffer size so doesn't error (and break test clean up) * invalidate watches when client reconnects + provide API for other wrapper clients to also create invalid watch states programatically. Reasoning: if a user does a WATCH and then the client reconnects, the watch is no longer active, but if a user does a MULTI/EXEC after that, they wont know, and since the WATCH is no longer active, the request has no protection. The API is needed for when a wrapper client (say sentinel, cluster) might close the underlying client and reopen a new one transparently to the user. Just like in the reconnection case, this should result in an error, but its up to the wrapping client to provide the appropriate error * remove WATCH and UNWATCH command files, fix WATCH and UNWATCH return type, some more cleanups * missing file in last commit :P * support for custom message in `WatchError` * setDirtyWatch * update watch docs * fixes needed * wip * get functions/modules to work again self -> _self change * reuse leased client on pipelined commands. though I realize this implementation, really only works after the first write command. unsure this is worth it. * test tweaks * nit * change how "sentinel" object client works, allow it to be reserved no more semaphore type counting * review * fixes to get more tests to pass * handle dirtyWatch and watchEpoch in reset and resetIfDirty * "fix", but not correct, needs more work * fix pubsub proxy * remove timeout from steadyState function in test, caused problems * improve restarting nodes * fix pubsub proxy and test --------- Co-authored-by: Leibale Eidelman <[email protected]>
1 parent 0cd6915 commit 8f3a276

34 files changed

+6806
-1557
lines changed

docs/clustering.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ await cluster.close();
3737
| modules | | Included [Redis Modules](../README.md#packages) |
3838
| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) |
3939
| functions | | Function definitions (see [Functions](../README.md#functions)) |
40+
4041
## Auth with password and username
4142

4243
Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option.

docs/sentinel.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Redis Sentinel
2+
3+
The [Redis Sentinel](https://redis.io/docs/management/sentinel/) object of node-redis provides a high level object that provides access to a high availability redis installation managed by Redis Sentinel to provide enumeration of master and replica nodes belonging to an installation as well as reconfigure itself on demand for failover and topology changes.
4+
5+
## Basic Example
6+
7+
```javascript
8+
import { createSentinel } from 'redis';
9+
10+
const sentinel = await createSentinel({
11+
name: 'sentinel-db',
12+
sentinelRootNodes: [{
13+
host: 'example',
14+
port: 1234
15+
}]
16+
})
17+
.on('error', err => console.error('Redis Sentinel Error', err));
18+
.connect();
19+
20+
await sentinel.set('key', 'value');
21+
const value = await sentinel.get('key');
22+
await sentinel.close();
23+
```
24+
25+
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.
26+
27+
## `createSentinel` configuration
28+
29+
| Property | Default | Description |
30+
|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
31+
| name | | The sentinel identifier for a particular database cluster |
32+
| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server |
33+
| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. |
34+
| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with |
35+
| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with |
36+
| masterPoolSize | `1` | The number of clients connected to the master node |
37+
| 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. |
38+
| 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. |
39+
## PubSub
40+
41+
It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down.
42+
43+
```javascript
44+
await sentinel.subscribe('channel', message => {
45+
// ...
46+
});
47+
await sentinel.unsubscribe('channel');
48+
```
49+
50+
see [the PubSub guide](./pub-sub.md) for more details.
51+
52+
## Sentinel as a pool
53+
54+
The sentinel object provides the ability to manage a pool of clients for the master node:
55+
56+
```javascript
57+
createSentinel({
58+
// ...
59+
masterPoolSize: 10
60+
});
61+
```
62+
63+
In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them:
64+
65+
```javascript
66+
createSentinel({
67+
// ...
68+
replicaPoolSize: 10
69+
});
70+
```
71+
72+
## Master client lease
73+
74+
Sometimes multiple commands needs to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
75+
76+
There are 2 ways to get a client lease:
77+
78+
`.use()`
79+
```javascript
80+
const result = await sentinel.use(async client => {
81+
await client.watch('key');
82+
return client.multi()
83+
.get('key')
84+
.exec();
85+
});
86+
```
87+
88+
`.getMasterClientLease()`
89+
```javascript
90+
const clientLease = await sentinel.getMasterClientLease();
91+
92+
try {
93+
await clientLease.watch('key');
94+
const resp = await clientLease.multi()
95+
.get('key')
96+
.exec();
97+
} finally {
98+
clientLease.release();
99+
}
100+
```

docs/transactions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ await multi.execTyped(); // [string]
2424
2525
## [`WATCH`](https://redis.io/commands/watch/)
2626

27-
You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change.
27+
You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change or if the client reconnected between the `watch` and `exec` calls.
2828

2929
The `WATCH` state is stored on the connection (by the server). In case you need to run multiple `WATCH` & `MULTI` in parallel you'll need to use a [pool](./pool.md).
3030

0 commit comments

Comments
 (0)