Skip to content
Open
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
68 changes: 67 additions & 1 deletion packages/client/lib/sentinel/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RESP_TYPES } from '../RESP/decoder';
import { WatchError } from "../errors";
import { RedisSentinelConfig, SentinelFramework } from "./test-util";
import { RedisSentinelEvent, RedisSentinelType, RedisSentinelClientType, RedisNode } from "./types";
import RedisSentinel from "./index";
import RedisSentinel, { areSentinelListsEqual } from "./index";
import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types';
import { promisify } from 'node:util';
import { exec } from 'node:child_process';
Expand All @@ -30,6 +30,72 @@ describe('RedisSentinel', () => {
assert.equal((sentinel as any).HOTKEYS_RESET, undefined);
});

describe('areSentinelListsEqual', () => {
it('should return true for identical lists', () => {
const a = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26380 }];
const b = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26380 }];
assert.equal(areSentinelListsEqual(a, b), true);
});

it('should return true for empty lists', () => {
assert.equal(areSentinelListsEqual([], []), true);
});

it('should return false for different lengths', () => {
const a = [{ host: '127.0.0.1', port: 26379 }];
const b = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26380 }];
assert.equal(areSentinelListsEqual(a, b), false);
});

it('should return false when hosts differ', () => {
const a = [{ host: '127.0.0.1', port: 26379 }];
const b = [{ host: '10.0.0.1', port: 26379 }];
assert.equal(areSentinelListsEqual(a, b), false);
});

it('should return false when ports differ', () => {
const a = [{ host: '127.0.0.1', port: 26379 }];
const b = [{ host: '127.0.0.1', port: 26380 }];
assert.equal(areSentinelListsEqual(a, b), false);
});

it('should return false when order differs (same elements)', () => {
const a = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26380 }];
const b = [{ host: '127.0.0.1', port: 26380 }, { host: '127.0.0.1', port: 26379 }];
assert.equal(areSentinelListsEqual(a, b), false);
});

it('should detect change when same length but different content (regression: was only checking length)', () => {
const a = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26380 }];
const b = [{ host: '127.0.0.1', port: 26379 }, { host: '127.0.0.1', port: 26381 }];
assert.equal(areSentinelListsEqual(a, b), false);
});
});

describe('sentinel root nodes preservation on failure', () => {
it('should preserve all sentinel root nodes after create (sentinels are not removed from config)', () => {
const sentinelRootNodes = [
{ host: '127.0.0.1', port: 26379 },
{ host: '127.0.0.1', port: 26380 },
{ host: '127.0.0.1', port: 26381 }
];

const sentinel = RedisSentinel.create({
name: 'mymaster',
sentinelRootNodes
});

// Verify all sentinel root nodes are preserved in the config
// (regression: #handleSentinelFailure used to splice nodes out of sentinelRootNodes)
assert.equal(sentinelRootNodes.length, 3, 'Original sentinelRootNodes array should not be modified');
assert.deepEqual(sentinelRootNodes, [
{ host: '127.0.0.1', port: 26379 },
{ host: '127.0.0.1', port: 26380 },
{ host: '127.0.0.1', port: 26381 }
]);
});
});

describe('initialization', () => {
describe('clientSideCache validation', () => {
const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
Expand Down
15 changes: 7 additions & 8 deletions packages/client/lib/sentinel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import { TcpNetConnectOpts } from 'node:net';
import { RedisTcpSocketOptions } from '../client/socket';
import { BasicPooledClientSideCache, PooledClientSideCacheProvider } from '../client/cache';

export function areSentinelListsEqual(a: ReadonlyArray<RedisNode>, b: ReadonlyArray<RedisNode>): boolean {
if (a.length !== b.length) return false;
return a.every((nodeA, i) => nodeA.host === b[i].host && nodeA.port === b[i].port);
}

interface ClientInfo {
id: number;
}
Expand Down Expand Up @@ -931,13 +936,7 @@ class RedisSentinelInternal<
}
}

#handleSentinelFailure(node: RedisNode) {
const found = this.#sentinelRootNodes.findIndex(
(rootNode) => rootNode.host === node.host && rootNode.port === node.port
);
if (found !== -1) {
this.#sentinelRootNodes.splice(found, 1);
}
#handleSentinelFailure(_node: RedisNode) {
this.#reset();
}

Expand Down Expand Up @@ -1347,7 +1346,7 @@ class RedisSentinelInternal<
}
}

if (analyzed.sentinelList.length != this.#sentinelRootNodes.length) {
if (!areSentinelListsEqual(analyzed.sentinelList, this.#sentinelRootNodes)) {
this.#sentinelRootNodes = analyzed.sentinelList;
const event: RedisSentinelEvent = {
type: "SENTINE_LIST_CHANGE",
Expand Down