From 46bfeaa94e8987216fcfd554c8ef7c5159480429 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Wed, 30 Apr 2025 16:30:16 +0300 Subject: [PATCH] Fix typo and improve Sentinel docs (#2931) --- docs/sentinel.md | 31 +++++---- packages/client/lib/sentinel/index.spec.ts | 38 +++++------ packages/client/lib/sentinel/index.ts | 75 +++++++++++++++++++++- packages/client/lib/sentinel/types.ts | 10 ++- 4 files changed, 118 insertions(+), 36 deletions(-) diff --git a/docs/sentinel.md b/docs/sentinel.md index 80e79c3f88..f10b2953df 100644 --- a/docs/sentinel.md +++ b/docs/sentinel.md @@ -14,7 +14,7 @@ const sentinel = await createSentinel({ port: 1234 }] }) - .on('error', err => console.error('Redis Sentinel Error', err)); + .on('error', err => console.error('Redis Sentinel Error', err)) .connect(); await sentinel.set('key', 'value'); @@ -26,16 +26,19 @@ In the above example, we configure the sentinel object to fetch the configuratio ## `createSentinel` configuration -| Property | Default | Description | -|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| 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 | -| 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 | -| 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 | -| 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. | +| Property | Default | Description | +|----------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 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 | +| 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 | +| 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 | +| 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. | +| 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 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 createSentinel({ @@ -85,9 +88,9 @@ const result = await sentinel.use(async client => { }); ``` -`.getMasterClientLease()` +`.acquire()` ```javascript -const clientLease = await sentinel.getMasterClientLease(); +const clientLease = await sentinel.acquire(); try { await clientLease.watch('key'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index 567da4b1a5..cf9228c261 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -134,7 +134,7 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT); 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]); }, GLOBAL.SENTINEL.WITH_SCRIPT); @@ -148,7 +148,7 @@ describe(`test with scripts`, () => { ); }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - + describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { @@ -178,14 +178,14 @@ describe(`test with functions`, () => { MATH_FUNCTION.code, { REPLACE: true } ); - + const reply = await sentinel.use( async (client: any) => { await client.set('key', '2'); return client.math.square('key'); } ); - + assert.equal(reply, 4); }, GLOBAL.SENTINEL.WITH_FUNCTION); }); @@ -216,7 +216,7 @@ describe(`test with replica pool size 1`, () => { testUtils.testWithClientSentinel('client lease', async sentinel => { sentinel.on("error", () => { }); - const clientLease = await sentinel.aquire(); + const clientLease = await sentinel.acquire(); clientLease.set('x', 456); let matched = false; @@ -243,7 +243,7 @@ describe(`test with replica pool size 1`, () => { return await client.get("x"); } ) - + await sentinel.set("x", 1); assert.equal(await promise, null); }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); @@ -276,7 +276,7 @@ describe(`test with masterPoolSize 2, reserve client true`, () => { assert.equal(await promise2, "2"); }, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true})); }); - + describe(`test with masterPoolSize 2`, () => { testUtils.testWithClientSentinel('multple clients', async sentinel => { sentinel.on("error", () => { }); @@ -313,14 +313,14 @@ describe(`test with masterPoolSize 2`, () => { }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => { - const leasedClient = await sentinel.aquire(); + const leasedClient = await sentinel.acquire(); await leasedClient.set('x', 1); await leasedClient.watch('x'); assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => { - const leasedClient = await sentinel.aquire(); + const leasedClient = await sentinel.acquire(); await leasedClient.set('x', 1); await leasedClient.watch('x'); await leasedClient.set('x', 2); @@ -328,11 +328,11 @@ describe(`test with masterPoolSize 2`, () => { await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }); - + // TODO: Figure out how to modify the test utils // so it would have fine grained controll over -// sentinel +// sentinel // it should somehow replicate the `SentinelFramework` object functionallities async function steadyState(frame: SentinelFramework) { let checkedMaster = false; @@ -439,7 +439,7 @@ describe.skip('legacy tests', () => { 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 in failing test: ${longestTestDelta}`); console.log("trace:"); @@ -454,7 +454,7 @@ describe.skip('legacy tests', () => { frame.sentinelMaster(), frame.sentinelReplicas() ]) - + 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 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 () { this.timeout(60000); @@ -538,8 +538,8 @@ describe.skip('legacy tests', () => { tracer.push("connected"); - const client = await sentinel.aquire(); - tracer.push("aquired lease"); + const client = await sentinel.acquire(); + tracer.push("acquired lease"); await client.set("x", 1); await client.watch("x"); @@ -586,7 +586,7 @@ describe.skip('legacy tests', () => { await sentinel.connect(); tracer.push("connected"); - const client = await sentinel.aquire(); + const client = await sentinel.acquire(); tracer.push("got leased client"); await client.set("x", 1); await client.watch("x"); @@ -965,10 +965,10 @@ describe.skip('legacy tests', () => { tracer.push("adding node"); await frame.addNode(); tracer.push("added node and waiting on added promise"); - await nodeAddedPromise; + await nodeAddedPromise; }) }); }); - + diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 73c4fffd07..3bf94abd81 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -32,14 +32,30 @@ export class RedisSentinelClient< #internal: RedisSentinelInternal; readonly _self: RedisSentinelClient; + /** + * Indicates if the client connection is open + * + * @returns `true` if the client connection is open, `false` otherwise + */ + get 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() { 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() { return this._self.#commandOptions; } @@ -222,6 +238,16 @@ export class RedisSentinelClient< 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() { if (this._self.#clientInfo === undefined) { throw new Error('RedisSentinelClient lease already released'); @@ -245,10 +271,20 @@ export default class RedisSentinel< #internal: RedisSentinelInternal; #options: RedisSentinelOptions; + /** + * Indicates if the sentinel connection is open + * + * @returns `true` if the sentinel connection is open, `false` otherwise + */ get 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() { return this._self.#internal.isReady; } @@ -511,7 +547,28 @@ export default class RedisSentinel< pUnsubscribe = this.PUNSUBSCRIBE; - async aquire(): Promise> { + /** + * 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> { const clientInfo = await this._self.#internal.getClientLease(); 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 { const id = this.#masterClientQueue.shift(); if (id !== undefined) { @@ -650,6 +713,16 @@ class RedisSentinelInternal< 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) { const client = this.#masterClients[clientInfo.id]; // client can be undefined if releasing in middle of a reconfigure diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index 428e7bccd6..28a5a91ddd 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -49,11 +49,17 @@ export interface RedisSentinelOptions< */ 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; /** - * 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; /**