1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Fix typo and improve Sentinel docs (#2931)

This commit is contained in:
Bobby I.
2025-04-30 16:30:16 +03:00
committed by GitHub
parent 49d6b2d465
commit 46bfeaa94e
4 changed files with 118 additions and 36 deletions

View File

@@ -14,7 +14,7 @@ const sentinel = await createSentinel({
port: 1234 port: 1234
}] }]
}) })
.on('error', err => console.error('Redis Sentinel Error', err)); .on('error', err => console.error('Redis Sentinel Error', err))
.connect(); .connect();
await sentinel.set('key', 'value'); await sentinel.set('key', 'value');
@@ -26,16 +26,19 @@ In the above example, we configure the sentinel object to fetch the configuratio
## `createSentinel` configuration ## `createSentinel` configuration
| Property | Default | Description | | Property | Default | Description |
|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |----------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | | The sentinel identifier for a particular database cluster | | name | | The sentinel identifier for a particular database cluster |
| 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 | | 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 |
| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. | | maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. |
| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with | | nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with |
| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with | | sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with |
| masterPoolSize | `1` | The number of clients connected to the master node | | masterPoolSize | `1` | The number of clients connected to the master node |
| 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. | | 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. |
| 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. | | 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. |
| 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. |
| 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. |
## PubSub ## PubSub
It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down. It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down.
@@ -60,7 +63,7 @@ createSentinel({
}); });
``` ```
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: 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:
```javascript ```javascript
createSentinel({ createSentinel({
@@ -85,9 +88,9 @@ const result = await sentinel.use(async client => {
}); });
``` ```
`.getMasterClientLease()` `.acquire()`
```javascript ```javascript
const clientLease = await sentinel.getMasterClientLease(); const clientLease = await sentinel.acquire();
try { try {
await clientLease.watch('key'); await clientLease.watch('key');

View File

@@ -134,7 +134,7 @@ describe(`test with scripts`, () => {
}, GLOBAL.SENTINEL.WITH_SCRIPT); }, GLOBAL.SENTINEL.WITH_SCRIPT);
testUtils.testWithClientSentinel('with script multi', async sentinel => { testUtils.testWithClientSentinel('with script multi', async sentinel => {
const reply = await sentinel.multi().set('key', 2).square('key').exec(); const reply = await sentinel.multi().set('key', 2).square('key').exec();
assert.deepEqual(reply, ['OK', 4]); assert.deepEqual(reply, ['OK', 4]);
}, GLOBAL.SENTINEL.WITH_SCRIPT); }, GLOBAL.SENTINEL.WITH_SCRIPT);
@@ -148,7 +148,7 @@ describe(`test with scripts`, () => {
); );
}, GLOBAL.SENTINEL.WITH_SCRIPT) }, GLOBAL.SENTINEL.WITH_SCRIPT)
}); });
describe(`test with functions`, () => { describe(`test with functions`, () => {
testUtils.testWithClientSentinel('with function', async sentinel => { testUtils.testWithClientSentinel('with function', async sentinel => {
@@ -178,14 +178,14 @@ describe(`test with functions`, () => {
MATH_FUNCTION.code, MATH_FUNCTION.code,
{ REPLACE: true } { REPLACE: true }
); );
const reply = await sentinel.use( const reply = await sentinel.use(
async (client: any) => { async (client: any) => {
await client.set('key', '2'); await client.set('key', '2');
return client.math.square('key'); return client.math.square('key');
} }
); );
assert.equal(reply, 4); assert.equal(reply, 4);
}, GLOBAL.SENTINEL.WITH_FUNCTION); }, GLOBAL.SENTINEL.WITH_FUNCTION);
}); });
@@ -216,7 +216,7 @@ describe(`test with replica pool size 1`, () => {
testUtils.testWithClientSentinel('client lease', async sentinel => { testUtils.testWithClientSentinel('client lease', async sentinel => {
sentinel.on("error", () => { }); sentinel.on("error", () => { });
const clientLease = await sentinel.aquire(); const clientLease = await sentinel.acquire();
clientLease.set('x', 456); clientLease.set('x', 456);
let matched = false; let matched = false;
@@ -243,7 +243,7 @@ describe(`test with replica pool size 1`, () => {
return await client.get("x"); return await client.get("x");
} }
) )
await sentinel.set("x", 1); await sentinel.set("x", 1);
assert.equal(await promise, null); assert.equal(await promise, null);
}, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1);
@@ -276,7 +276,7 @@ describe(`test with masterPoolSize 2, reserve client true`, () => {
assert.equal(await promise2, "2"); assert.equal(await promise2, "2");
}, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true})); }, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true}));
}); });
describe(`test with masterPoolSize 2`, () => { describe(`test with masterPoolSize 2`, () => {
testUtils.testWithClientSentinel('multple clients', async sentinel => { testUtils.testWithClientSentinel('multple clients', async sentinel => {
sentinel.on("error", () => { }); sentinel.on("error", () => { });
@@ -313,14 +313,14 @@ describe(`test with masterPoolSize 2`, () => {
}, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => { testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => {
const leasedClient = await sentinel.aquire(); const leasedClient = await sentinel.acquire();
await leasedClient.set('x', 1); await leasedClient.set('x', 1);
await leasedClient.watch('x'); await leasedClient.watch('x');
assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1'])
}, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => { testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => {
const leasedClient = await sentinel.aquire(); const leasedClient = await sentinel.acquire();
await leasedClient.set('x', 1); await leasedClient.set('x', 1);
await leasedClient.watch('x'); await leasedClient.watch('x');
await leasedClient.set('x', 2); await leasedClient.set('x', 2);
@@ -328,11 +328,11 @@ describe(`test with masterPoolSize 2`, () => {
await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError());
}, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2);
}); });
// TODO: Figure out how to modify the test utils // TODO: Figure out how to modify the test utils
// so it would have fine grained controll over // so it would have fine grained controll over
// sentinel // sentinel
// it should somehow replicate the `SentinelFramework` object functionallities // it should somehow replicate the `SentinelFramework` object functionallities
async function steadyState(frame: SentinelFramework) { async function steadyState(frame: SentinelFramework) {
let checkedMaster = false; let checkedMaster = false;
@@ -439,7 +439,7 @@ describe.skip('legacy tests', () => {
sentinel.on('error', () => { }); sentinel.on('error', () => { });
} }
if (this!.currentTest!.state === 'failed') { if (this!.currentTest!.state === 'failed') {
console.log(`longest event loop blocked delta: ${longestDelta}`); console.log(`longest event loop blocked delta: ${longestDelta}`);
console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); console.log(`longest event loop blocked in failing test: ${longestTestDelta}`);
console.log("trace:"); console.log("trace:");
@@ -454,7 +454,7 @@ describe.skip('legacy tests', () => {
frame.sentinelMaster(), frame.sentinelMaster(),
frame.sentinelReplicas() frame.sentinelReplicas()
]) ])
console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`);
console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`);
console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`);
@@ -492,7 +492,7 @@ describe.skip('legacy tests', () => {
); );
}); });
// stops master to force sentinel to update // stops master to force sentinel to update
it('stop master', async function () { it('stop master', async function () {
this.timeout(60000); this.timeout(60000);
@@ -538,8 +538,8 @@ describe.skip('legacy tests', () => {
tracer.push("connected"); tracer.push("connected");
const client = await sentinel.aquire(); const client = await sentinel.acquire();
tracer.push("aquired lease"); tracer.push("acquired lease");
await client.set("x", 1); await client.set("x", 1);
await client.watch("x"); await client.watch("x");
@@ -586,7 +586,7 @@ describe.skip('legacy tests', () => {
await sentinel.connect(); await sentinel.connect();
tracer.push("connected"); tracer.push("connected");
const client = await sentinel.aquire(); const client = await sentinel.acquire();
tracer.push("got leased client"); tracer.push("got leased client");
await client.set("x", 1); await client.set("x", 1);
await client.watch("x"); await client.watch("x");
@@ -965,10 +965,10 @@ describe.skip('legacy tests', () => {
tracer.push("adding node"); tracer.push("adding node");
await frame.addNode(); await frame.addNode();
tracer.push("added node and waiting on added promise"); tracer.push("added node and waiting on added promise");
await nodeAddedPromise; await nodeAddedPromise;
}) })
}); });
}); });

View File

@@ -32,14 +32,30 @@ export class RedisSentinelClient<
#internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>; #internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;
readonly _self: RedisSentinelClient<M, F, S, RESP, TYPE_MAPPING>; readonly _self: RedisSentinelClient<M, F, S, RESP, TYPE_MAPPING>;
/**
* Indicates if the client connection is open
*
* @returns `true` if the client connection is open, `false` otherwise
*/
get isOpen() { get isOpen() {
return this._self.#internal.isOpen; return this._self.#internal.isOpen;
} }
/**
* Indicates if the client connection is ready to accept commands
*
* @returns `true` if the client connection is ready, `false` otherwise
*/
get isReady() { get isReady() {
return this._self.#internal.isReady; return this._self.#internal.isReady;
} }
/**
* Gets the command options configured for this client
*
* @returns The command options for this client or `undefined` if none were set
*/
get commandOptions() { get commandOptions() {
return this._self.#commandOptions; return this._self.#commandOptions;
} }
@@ -222,6 +238,16 @@ export class RedisSentinelClient<
unwatch = this.UNWATCH; unwatch = this.UNWATCH;
/**
* Releases the client lease back to the pool
*
* After calling this method, the client instance should no longer be used as it
* will be returned to the client pool and may be given to other operations.
*
* @returns A promise that resolves when the client is ready to be reused, or undefined
* if the client was immediately ready
* @throws Error if the lease has already been released
*/
release() { release() {
if (this._self.#clientInfo === undefined) { if (this._self.#clientInfo === undefined) {
throw new Error('RedisSentinelClient lease already released'); throw new Error('RedisSentinelClient lease already released');
@@ -245,10 +271,20 @@ export default class RedisSentinel<
#internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>; #internal: RedisSentinelInternal<M, F, S, RESP, TYPE_MAPPING>;
#options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>; #options: RedisSentinelOptions<M, F, S, RESP, TYPE_MAPPING>;
/**
* Indicates if the sentinel connection is open
*
* @returns `true` if the sentinel connection is open, `false` otherwise
*/
get isOpen() { get isOpen() {
return this._self.#internal.isOpen; return this._self.#internal.isOpen;
} }
/**
* Indicates if the sentinel connection is ready to accept commands
*
* @returns `true` if the sentinel connection is ready, `false` otherwise
*/
get isReady() { get isReady() {
return this._self.#internal.isReady; return this._self.#internal.isReady;
} }
@@ -511,7 +547,28 @@ export default class RedisSentinel<
pUnsubscribe = this.PUNSUBSCRIBE; pUnsubscribe = this.PUNSUBSCRIBE;
async aquire(): Promise<RedisSentinelClientType<M, F, S, RESP, TYPE_MAPPING>> { /**
* Acquires a master client lease for exclusive operations
*
* Used when multiple commands need to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`).
* The returned client must be released after use with the `release()` method.
*
* @returns A promise that resolves to a Redis client connected to the master node
* @example
* ```javascript
* const clientLease = await sentinel.acquire();
*
* try {
* await clientLease.watch('key');
* const resp = await clientLease.multi()
* .get('key')
* .exec();
* } finally {
* clientLease.release();
* }
* ```
*/
async acquire(): Promise<RedisSentinelClientType<M, F, S, RESP, TYPE_MAPPING>> {
const clientInfo = await this._self.#internal.getClientLease(); const clientInfo = await this._self.#internal.getClientLease();
return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions); return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions);
} }
@@ -641,6 +698,12 @@ class RedisSentinelInternal<
}); });
} }
/**
* Gets a client lease from the master client pool
*
* @returns A client info object or a promise that resolves to a client info object
* when a client becomes available
*/
getClientLease(): ClientInfo | Promise<ClientInfo> { getClientLease(): ClientInfo | Promise<ClientInfo> {
const id = this.#masterClientQueue.shift(); const id = this.#masterClientQueue.shift();
if (id !== undefined) { if (id !== undefined) {
@@ -650,6 +713,16 @@ class RedisSentinelInternal<
return this.#masterClientQueue.wait().then(id => ({ id })); return this.#masterClientQueue.wait().then(id => ({ id }));
} }
/**
* Releases a client lease back to the pool
*
* If the client was used for a transaction that might have left it in a dirty state,
* it will be reset before being returned to the pool.
*
* @param clientInfo The client info object representing the client to release
* @returns A promise that resolves when the client is ready to be reused, or undefined
* if the client was immediately ready or no longer exists
*/
releaseClientLease(clientInfo: ClientInfo) { releaseClientLease(clientInfo: ClientInfo) {
const client = this.#masterClients[clientInfo.id]; const client = this.#masterClients[clientInfo.id];
// client can be undefined if releasing in middle of a reconfigure // client can be undefined if releasing in middle of a reconfigure

View File

@@ -49,11 +49,17 @@ export interface RedisSentinelOptions<
*/ */
replicaPoolSize?: number; replicaPoolSize?: number;
/** /**
* TODO * Interval in milliseconds to periodically scan for changes in the sentinel topology.
* The client will query the sentinel for changes at this interval.
*
* Default: 10000 (10 seconds)
*/ */
scanInterval?: number; scanInterval?: number;
/** /**
* TODO * 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.
*
* Default: false
*/ */
passthroughClientErrorEvents?: boolean; passthroughClientErrorEvents?: boolean;
/** /**