diff --git a/README.md b/README.md index 667e671..0c79ad1 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ The `@socket.io/redis-adapter` package allows broadcasting packets between multi - [With the `ioredis` package](#with-the-ioredis-package) - [With the `ioredis` package and a Redis cluster](#with-the-ioredis-package-and-a-redis-cluster) - [With Redis sharded Pub/Sub](#with-redis-sharded-pubsub) + - [With the `ioredis` package and a Redis cluster](#with-the-ioredis-package-and-a-redis-cluster-1) - [Options](#options) - [Default adapter](#default-adapter) - [Sharded adapter](#sharded-adapter) @@ -186,7 +187,44 @@ Minimum requirements: - Redis 7.0 - [`redis@4.6.0`](https://github.com/redis/node-redis/commit/3b1bad229674b421b2bc6424155b20d4d3e45bd1) -Note: it is not currently possible to use the sharded adapter with the `ioredis` package and a Redis cluster ([reference](https://github.com/luin/ioredis/issues/1759)). +#### With the `ioredis` package and a Redis cluster + +Starting with `ioredis@5.9.0`, you can use the sharded adapter with an ioredis Cluster by enabling the `shardedSubscribers` option: + +```js +import { Cluster } from "ioredis"; +import { Server } from "socket.io"; +import { createShardedAdapter } from "@socket.io/redis-adapter"; + +const pubClient = new Cluster( + [ + { + host: "localhost", + port: 7000, + }, + { + host: "localhost", + port: 7001, + }, + { + host: "localhost", + port: 7002, + }, + ], + { + shardedSubscribers: true, + } +); +const subClient = pubClient.duplicate(); + +const io = new Server({ + adapter: createShardedAdapter(pubClient, subClient) +}); + +io.listen(3000); +``` + +Reference: https://github.com/redis/ioredis/pull/1956 ## Options diff --git a/lib/sharded-adapter.ts b/lib/sharded-adapter.ts index ea3f37a..80fb108 100644 --- a/lib/sharded-adapter.ts +++ b/lib/sharded-adapter.ts @@ -6,7 +6,15 @@ import { Offset, } from "socket.io-adapter"; import { decode, encode } from "notepack.io"; -import { hasBinary, PUBSUB, SPUBLISH, SSUBSCRIBE, SUNSUBSCRIBE } from "./util"; +import { + hasBinary, + hasShardedSubscribers, + isIoRedisCluster, + PUBSUB, + SPUBLISH, + SSUBSCRIBE, + SUNSUBSCRIBE, +} from "./util"; import debugModule from "debug"; const debug = debugModule("socket.io-redis"); @@ -85,6 +93,15 @@ class ShardedRedisAdapter extends ClusterAdapter { opts ); + // Validate ioredis Cluster configuration + if (isIoRedisCluster(subClient) && !hasShardedSubscribers(subClient)) { + throw new Error( + "When using the sharded adapter with an ioredis Cluster, " + + "you must enable the 'shardedSubscribers' option. " + + "See https://github.com/redis/ioredis/pull/1956" + ); + } + this.channel = `${this.opts.channelPrefix}#${nsp.name}#`; this.responseChannel = `${this.opts.channelPrefix}#${nsp.name}#${this.uid}#`; diff --git a/lib/util.ts b/lib/util.ts index cc4f74b..829ba20 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -52,6 +52,29 @@ function isRedisV4Client(redisClient: any) { return typeof redisClient.sSubscribe === "function"; } +/** + * Whether the client is an ioredis Cluster instance + * + * @param redisClient + */ +export function isIoRedisCluster(redisClient: any) { + return redisClient.constructor.name === "Cluster" || redisClient.isCluster; +} + +/** + * Whether the ioredis Cluster has shardedSubscribers enabled + * + * @param redisClient + * + * @see https://github.com/redis/ioredis/pull/1956 + */ +export function hasShardedSubscribers(redisClient: any) { + return ( + isIoRedisCluster(redisClient) && + redisClient.options?.shardedSubscribers === true + ); +} + const kHandlers = Symbol("handlers"); export function SSUBSCRIBE( diff --git a/package-lock.json b/package-lock.json index c580cf5..ebece32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@types/mocha": "^8.2.1", "@types/node": "^14.14.7", "expect.js": "0.3.1", - "ioredis": "^5.3.2", + "ioredis": "^5.9.1", "mocha": "^10.1.0", "nyc": "^15.1.0", "prettier": "^2.8.7", @@ -283,10 +283,11 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "dev": true, + "license": "MIT" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -458,6 +459,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz", "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==", "dev": true, + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -564,7 +566,8 @@ "version": "14.14.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", - "dev": true + "dev": true, + "peer": true }, "node_modules/accepts": { "version": "1.3.8", @@ -1376,12 +1379,13 @@ "dev": true }, "node_modules/ioredis": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", - "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.1.tgz", + "integrity": "sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==", "dev": true, + "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -2738,6 +2742,7 @@ "version": "2.5.4", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "peer": true, "dependencies": { "debug": "~4.3.4", "ws": "~8.11.0" @@ -3024,6 +3029,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3575,9 +3581,9 @@ } }, "@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", "dev": true }, "@isaacs/cliui": { @@ -3705,6 +3711,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz", "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==", "dev": true, + "peer": true, "requires": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -3800,7 +3807,8 @@ "version": "14.14.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.7.tgz", "integrity": "sha512-Zw1vhUSQZYw+7u5dAwNbIA9TuTotpzY/OF7sJM9FqPOF3SPjKnxrjoTktXDZgUjybf4cWVBP7O8wvKdSaGHweg==", - "dev": true + "dev": true, + "peer": true }, "accepts": { "version": "1.3.8", @@ -4410,12 +4418,12 @@ "dev": true }, "ioredis": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", - "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.1.tgz", + "integrity": "sha512-BXNqFQ66oOsR82g9ajFFsR8ZKrjVvYCLyeML9IvSMAsP56XH2VXBdZjmI11p65nXXJxTEt1hie3J2QeFJVgrtQ==", "dev": true, "requires": { - "@ioredis/commands": "^1.1.1", + "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -5416,6 +5424,7 @@ "version": "2.5.4", "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "peer": true, "requires": { "debug": "~4.3.4", "ws": "~8.11.0" @@ -5621,7 +5630,8 @@ "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true + "dev": true, + "peer": true }, "uid2": { "version": "1.0.0", diff --git a/package.json b/package.json index 869e82d..90f7cb2 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@types/mocha": "^8.2.1", "@types/node": "^14.14.7", "expect.js": "0.3.1", - "ioredis": "^5.3.2", + "ioredis": "^5.9.1", "mocha": "^10.1.0", "nyc": "^15.1.0", "prettier": "^2.8.7", diff --git a/test/test-runner.ts b/test/test-runner.ts index d80c1d5..585b74f 100644 --- a/test/test-runner.ts +++ b/test/test-runner.ts @@ -38,6 +38,16 @@ const clusterNodes = [ }, ]; +// NAT mapping for ioredis Cluster (Docker container returns internal IP) +const ioredisNatMap = { + "172.20.0.3:7000": { host: "localhost", port: 7000 }, + "172.20.0.3:7001": { host: "localhost", port: 7001 }, + "172.20.0.3:7002": { host: "localhost", port: 7002 }, + "172.20.0.3:7003": { host: "localhost", port: 7003 }, + "172.20.0.3:7004": { host: "localhost", port: 7004 }, + "172.20.0.3:7005": { host: "localhost", port: 7005 }, +}; + function testSuite( createAdapter: any, redisPackage: string = "redis@4", @@ -139,7 +149,9 @@ describe("@socket.io/redis-adapter", () => { describe("ioredis cluster", () => testSuite(async () => { - const pubClient = new Cluster(clusterNodes); + const pubClient = new Cluster(clusterNodes, { + natMap: ioredisNatMap, + }); const subClient = pubClient.duplicate(); return [ @@ -259,11 +271,14 @@ describe("@socket.io/redis-adapter", () => { true )); - // FIXME see https://github.com/luin/ioredis/issues/1759 - describe.skip("[sharded] ioredis cluster", () => + // Fixed in ioredis 5.9.0, see https://github.com/redis/ioredis/pull/1956 + describe("[sharded] ioredis cluster", () => testSuite( async () => { - const pubClient = new Cluster(clusterNodes); + const pubClient = new Cluster(clusterNodes, { + shardedSubscribers: true, + natMap: ioredisNatMap, + }); const subClient = pubClient.duplicate(); return [