diff --git a/lib/command.ts b/lib/command.ts index 40159213..8b0c59ab 100644 --- a/lib/command.ts +++ b/lib/command.ts @@ -156,6 +156,7 @@ export default class Command implements ICommand { public isCustomCommand = false; public inTransaction = false; public pipelineIndex?: number; + private _commandTimeoutTimer?: NodeJS.Timeout; private slot?: number | null; private keys?: Array; @@ -342,6 +343,12 @@ export default class Command implements ICommand { private _convertValue(resolve: Function): (result: any) => void { return (value) => { try { + const existingTimer = this._commandTimeoutTimer; + if (existingTimer) { + clearTimeout(existingTimer); + delete this._commandTimeoutTimer; + } + resolve(this.transformReply(value)); this.isResolved = true; } catch (err) { @@ -370,6 +377,20 @@ export default class Command implements ICommand { return result; } + + /** + * Set the wait time before terminating the attempt to execute a command + * and generating an error. + */ + public setTimeout(ms: number) { + if (!this._commandTimeoutTimer) { + this._commandTimeoutTimer = setTimeout(() => { + if (!this.isResolved) { + this.reject(new Error("Command timed out")); + } + }, ms); + } + } } const msetArgumentTransformer = function (args) { diff --git a/lib/redis/index.ts b/lib/redis/index.ts index 88828e7b..7860de7a 100644 --- a/lib/redis/index.ts +++ b/lib/redis/index.ts @@ -693,7 +693,7 @@ addTransactionSupport(Redis.prototype); * ``` * @private */ -Redis.prototype.sendCommand = function (command, stream) { +Redis.prototype.sendCommand = function (command: Command, stream) { if (this.status === "wait") { this.connect().catch(noop); } @@ -714,11 +714,7 @@ Redis.prototype.sendCommand = function (command, stream) { } if (typeof this.options.commandTimeout === "number") { - setTimeout(() => { - if (!command.isResolved) { - command.reject(new Error("Command timed out")); - } - }, this.options.commandTimeout); + command.setTimeout(this.options.commandTimeout); } if (command.name === "quit") { diff --git a/lib/utils/index.ts b/lib/utils/index.ts index d9533fcd..6ba975f5 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -111,7 +111,7 @@ export function wrapMultiResult(arr) { * ``` * @private */ -export function isInt(value) { +export function isInt(value): value is string { const x = parseFloat(value); return !isNaN(value) && (x | 0) === x; } diff --git a/test/functional/commandTimeout.ts b/test/functional/commandTimeout.ts index 1b0a7fec..6258bbbe 100644 --- a/test/functional/commandTimeout.ts +++ b/test/functional/commandTimeout.ts @@ -22,4 +22,16 @@ describe("commandTimeout", function () { }); clock.tick(1000); }); + + it("does not leak timers for commands in offline queue", async function () { + const server = new MockServer(30001); + + const redis = new Redis({ port: 30001, commandTimeout: 1000 }); + const clock = sinon.useFakeTimers(); + await redis.hget("foo"); + expect(clock.countTimers()).to.eql(0); + clock.restore(); + redis.disconnect(); + await server.disconnectPromise(); + }); });