Skip to content

Fix typo and improve Sentinel docs #2931

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 17 additions & 14 deletions docs/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand All @@ -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({
Expand All @@ -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');
Expand Down
38 changes: 19 additions & 19 deletions packages/client/lib/sentinel/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -148,7 +148,7 @@ describe(`test with scripts`, () => {
);
}, GLOBAL.SENTINEL.WITH_SCRIPT)
});


describe(`test with functions`, () => {
testUtils.testWithClientSentinel('with function', async sentinel => {
Expand Down Expand Up @@ -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);
});
Expand Down Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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", () => { });
Expand Down Expand Up @@ -313,26 +313,26 @@ 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);

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;
Expand Down Expand Up @@ -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:");
Expand All @@ -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')}`);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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;
})
});
});



75 changes: 74 additions & 1 deletion packages/client/lib/sentinel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,30 @@ export class RedisSentinelClient<
#internal: RedisSentinelInternal<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() {
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;
}
Expand Down Expand Up @@ -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');
Expand All @@ -245,10 +271,20 @@ export default class RedisSentinel<
#internal: RedisSentinelInternal<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() {
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;
}
Expand Down Expand Up @@ -511,7 +547,28 @@ export default class RedisSentinel<

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();
return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions);
}
Expand Down Expand Up @@ -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> {
const id = this.#masterClientQueue.shift();
if (id !== undefined) {
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions packages/client/lib/sentinel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/**
Expand Down
Loading