diff --git a/packages/memcache-client/README.md b/packages/memcache-client/README.md index 2e860ec..a179b6b 100644 --- a/packages/memcache-client/README.md +++ b/packages/memcache-client/README.md @@ -166,6 +166,7 @@ const options = { lifetime: 100, // TTL 100 seconds cmdTimeout: 3000, // command timeout in milliseconds connectTimeout: 8000, // connect to server timeout in ms + keepAlive: 120000, // keepalive initial delay in ms, or `false` to disable noDelay: true, // whether to enable TCP_NODELAY on connections compressor: require("custom-compressor"), logger: require("./custom-logger"), @@ -191,6 +192,7 @@ const client = new MemcacheClient(options); - If a command didn't receive response before this timeout value, then it will cause the connection to shutdown and returns Error. - `connectTimeout` - **_optional_** Custom self connect to server timeout in milliseconds. It's disabled if set to 0. DEFAULT: 0 - The error object from this will have `connecting` set to `true` +- `keepAlive` - **_optional_** Initial delay (in milliseconds) between the last data packet received on a connection and when a keepalive probe should be sent, or `false` to disable the `SO_KEEPALIVE` socket option entirely. DEFAULT: 1 minute (60000 milliseconds) - `keepDangleSocket` - **_optional_** After `connectTimeout` trigger, do not destroy the socket but keep listening for errors on it. DEFAULT: false - `dangleSocketWaitTimeout` - **_optional_** How long to wait for errors on dangle socket before destroying it. DEFAULT: 5 minutes (30000 milliseconds) - `compressor` - **_optional_** a custom compressor for compressing the data. See [data compression](#data-compression) for more details. diff --git a/packages/memcache-client/src/lib/connection.ts b/packages/memcache-client/src/lib/connection.ts index f8e7644..a3588bd 100644 --- a/packages/memcache-client/src/lib/connection.ts +++ b/packages/memcache-client/src/lib/connection.ts @@ -342,6 +342,16 @@ export class MemcacheConnection extends MemcacheParser { } _setupConnection(socket: Socket): void { + const keepAlive = this.client?.options?.keepAlive; + + if (keepAlive !== false) { + const initialDelay = typeof keepAlive === "number" && Number.isFinite(keepAlive) + ? keepAlive + : 60000; + + socket.setKeepAlive(true, initialDelay); + } + if (this.client?.options?.noDelay) { socket.setNoDelay(true); } diff --git a/packages/memcache-client/src/test/spec/client.spec.ts b/packages/memcache-client/src/test/spec/client.spec.ts index d82ff02..f349d20 100644 --- a/packages/memcache-client/src/test/spec/client.spec.ts +++ b/packages/memcache-client/src/test/spec/client.spec.ts @@ -279,35 +279,81 @@ describe("memcache client", function () { }); }); - it("should not enable TCP_NODELAY by default", async () => { - await startSingleServer(); + it("should enable SO_KEEPALIVE with an initial delay of 60000 ms by default", async () => { + const _setKeepAlive = Socket.prototype.setKeepAlive; + const mockKeepAlive = jest.fn(); + const x = new MemcacheClient({ server }); + + try { + Socket.prototype.setKeepAlive = mockKeepAlive; + await x.set("foo", "bar"); + } finally { + Socket.prototype.setKeepAlive = _setKeepAlive; + x.shutdown(); + } + + expect(mockKeepAlive).toHaveBeenCalledWith(true, 60000); + }); + + it("should enable SO_KEEPALIVE with a custom initial delay when the keepAlive client option is a number", async () => { + const _setKeepAlive = Socket.prototype.setKeepAlive; + const mockKeepAlive = jest.fn(); + const x = new MemcacheClient({ server, keepAlive: 10000 }); + + try { + Socket.prototype.setKeepAlive = mockKeepAlive; + await x.set("foo", "bar"); + } finally { + Socket.prototype.setKeepAlive = _setKeepAlive; + x.shutdown(); + } + + expect(mockKeepAlive).toHaveBeenCalledWith(true, 10000); + }); + + it("should not enable SO_KEEPALIVE when the keepAlive client option is `false`", async () => { + const _setKeepAlive = Socket.prototype.setKeepAlive; + const mockKeepAlive = jest.fn(); + const x = new MemcacheClient({ server, keepAlive: false }); + + try { + Socket.prototype.setKeepAlive = mockKeepAlive; + await x.set("foo", "bar"); + } finally { + Socket.prototype.setKeepAlive = _setKeepAlive; + x.shutdown(); + } + + expect(mockKeepAlive).not.toHaveBeenCalled(); + }); + it("should not enable TCP_NODELAY by default", async () => { const _setNoDelay = Socket.prototype.setNoDelay; const mockNoDelay = jest.fn(); + const x = new MemcacheClient({ server }); try { Socket.prototype.setNoDelay = mockNoDelay; - const x = new MemcacheClient({ server: server }); await x.set("foo", "bar"); } finally { Socket.prototype.setNoDelay = _setNoDelay; + x.shutdown(); } expect(mockNoDelay).not.toHaveBeenCalled(); }); it("should enable TCP_NODELAY when options.noDelay is true", async () => { - await startSingleServer(); - const _setNoDelay = Socket.prototype.setNoDelay; const mockNoDelay = jest.fn(); + const x = new MemcacheClient({ server, noDelay: true }); try { Socket.prototype.setNoDelay = mockNoDelay; - const x = new MemcacheClient({ server: server, noDelay: true }); await x.set("foo", "bar"); } finally { Socket.prototype.setNoDelay = _setNoDelay; + x.shutdown(); } expect(mockNoDelay).toHaveBeenCalled(); diff --git a/packages/memcache-client/src/types.ts b/packages/memcache-client/src/types.ts index 068a2bd..f8a92e3 100644 --- a/packages/memcache-client/src/types.ts +++ b/packages/memcache-client/src/types.ts @@ -46,6 +46,7 @@ export type MemcacheClientOptions = { noDelay?: boolean; cmdTimeout?: number; connectTimeout?: number; + keepAlive?: number | false; keepDangleSocket?: boolean; dangleSocketWaitTimeout?: number; compressor?: CompressorLibrary;