diff --git a/CHANGELOG.md b/CHANGELOG.md index 94f678dbe9..1b2a4b4025 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +#### Changes + +- Node: Allow routing Cluster requests by address. ([#1021](https://github.com/aws/glide-for-redis/pull/1021)) + ## 0.2.0 (2024-02-11) #### Changes diff --git a/node/src/RedisClusterClient.ts b/node/src/RedisClusterClient.ts index b9a1fdb90f..f35b2bfd85 100644 --- a/node/src/RedisClusterClient.ts +++ b/node/src/RedisClusterClient.ts @@ -52,6 +52,19 @@ export type SlotKeyTypes = { key: string; }; +/// Route command to specific node. +export type RouteByAddress = { + type: "routeByAddress"; + /** + * DNS name of the host. + */ + host: string; + /** + * The port to access on the node. If port is not provided, `host` is assumed to be in the format `{hostname}:{port}`. + */ + port?: number; +}; + export type Routes = | SingleNodeRoute /** @@ -75,7 +88,8 @@ export type SingleNodeRoute = /** * Route request to the node that contains the slot that the given key matches. */ - | SlotKeyTypes; + | SlotKeyTypes + | RouteByAddress; function toProtobufRoute( route: Routes | undefined @@ -124,6 +138,27 @@ function toProtobufRoute( slotId: route.id, }), }); + } else if (route.type === "routeByAddress") { + let port = route.port; + let host = route.host; + + if (port === undefined) { + const split = host.split(":"); + + if (split.length !== 2) { + throw new Error( + "No port provided, expected host to be formatted as `{hostname}:{port}`. Received " + + host + ); + } + + host = split[0]; + port = Number(split[1]); + } + + return redis_request.Routes.create({ + byAddressRoute: { host, port }, + }); } } diff --git a/node/tests/RedisClusterClient.test.ts b/node/tests/RedisClusterClient.test.ts index 1ea93d589a..875e4390fd 100644 --- a/node/tests/RedisClusterClient.test.ts +++ b/node/tests/RedisClusterClient.test.ts @@ -135,6 +135,74 @@ describe("RedisClusterClient", () => { TIMEOUT ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `route by address reaches correct node_%p`, + async (protocol) => { + const client = await RedisClusterClient.createClient( + getOptions(cluster.ports(), protocol) + ); + const result = (await client.customCommand( + ["cluster", "nodes"], + "randomNode" + )) as string; + + // check that routing without explicit port works + const host = + result + .split("\n") + .find((line) => line.includes("myself")) + ?.split(" ")[1] + .split("@")[0] ?? ""; + if (!host) { + throw new Error("No host could be parsed"); + } + + const secondResult = (await client.customCommand( + ["cluster", "nodes"], + { + type: "routeByAddress", + host, + } + )) as string; + + expect(result).toEqual(secondResult); + + const [host2, port] = host.split(":"); + + // check that routing with explicit port works + const thirdResult = (await client.customCommand( + ["cluster", "nodes"], + { + type: "routeByAddress", + host: host2, + port: Number(port), + } + )) as string; + + expect(result).toEqual(thirdResult); + + client.close(); + }, + TIMEOUT + ); + + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( + `fail routing by address if no port is provided_%p`, + async (protocol) => { + const client = await RedisClusterClient.createClient( + getOptions(cluster.ports(), protocol) + ); + expect(() => + client.info(undefined, { + type: "routeByAddress", + host: "foo", + }) + ).toThrowError(); + client.close(); + }, + TIMEOUT + ); + it.each([ProtocolVersion.RESP2, ProtocolVersion.RESP3])( `config get and config set transactions test_%p`, async (protocol) => {