Skip to content

feat: Add command timeout #2981

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

florian-schunk
Copy link

@florian-schunk florian-schunk commented May 28, 2025

Description

This PR introduces a timeout for commands sent to redis.
Our use-case is that we are using node-rate-limiter-flexible with node-redis, but sometimes the connection to redis is slow. In these cases we want to fall back the the insurance limiter of node-rate-limiter-flexible.

This feature was also requested in #2175


Checklist

  • Does npm test pass with this change (including linting)?
  • Is the new or changed code fully tested?
  • Is a documentation update included (if this change modifies existing APIs, or introduces new ones)?

@florian-schunk florian-schunk force-pushed the addCommandTimeout branch 3 times, most recently from 4ad6a4a to 90f8fe9 Compare June 18, 2025 15:20
@florian-schunk
Copy link
Author

Hi @nkaradzhov sorry for pinging you directly, but I couldn't find information on how to contact maintainers for this project.
What should we do for getting this PR reviewed and merged?

@nkaradzhov
Copy link
Collaborator

@florian-schunk hi, sorry for the delay, been a bit busy. I will take a look at this.

Comment on lines +896 to +910
let controller: AbortController;
if (this._self.#options?.commandTimeout) {
controller = new AbortController()
let abortSignal = controller.signal;
if (options?.abortSignal) {
abortSignal = AbortSignal.any([
abortSignal,
options.abortSignal
]);
}
options = {
...options,
abortSignal
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this by using the AbortSignal.timeout static method:

    if (this._self.#options?.commandTimeout) {
      let abortSignal = AbortSignal.timeout(
        this._self.#options?.commandTimeout
      );
      if (options?.abortSignal) {
        abortSignal = AbortSignal.any([abortSignal, options.abortSignal]);
      }
      options = {
        ...options,
        abortSignal
      };
    }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this, also together with the change in the test that you proposed, but somehow for me the test keeps failing with this approach. I am not quite understanding, why.

Comment on lines 912 to 930

this._self.#scheduleWrite();
return promise;
if (!this._self.#options?.commandTimeout) {
return promise;
}

return new Promise<T>((resolve, reject) => {
const timeoutId = setTimeout(() => {
controller.abort();
reject(new CommandTimeoutError());
}, this._self.#options?.commandTimeout)
promise.then(result => {
clearInterval(timeoutId);
resolve(result)
}).catch(error => {
clearInterval(timeoutId);
reject(error)
});
})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, this change becomes obsolete

Comment on lines 25 to 30
export class CommandTimeoutError extends Error {
constructor() {
super('Command timeout');
}
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to introduce another error, we can use the AbortError

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I changed it to use AbortError, but couldn't there be situation where it makes sense to distinguish between a timeout and having the command manually aborted? Wouldn't it then make more sense to have a different error?

Comment on lines 285 to 300
testUtils.testWithClient('CommandTimeoutError', async client => {
const promise = assert.rejects(client.sendCommand(['PING']), CommandTimeoutError),
start = process.hrtime.bigint();

while (process.hrtime.bigint() - start < 50_000_000) {
// block the event loop for 1ms, to make sure the connection will timeout
}

await promise;
}, {
...GLOBAL.SERVERS.OPEN,
clientOptions: {
commandTimeout: 50,
}
});

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test would need to change like this:

  testUtils.testWithClient('CommandTimeoutError', async client => {
    const promise = client.sendCommand(['PING'])
    const start = process.hrtime.bigint();
    while (process.hrtime.bigint() - start < 50_000_000) {
      // block the event loop for 50ms, to make sure the connection will timeout
    }
    assert.rejects(promise, AbortError);
  }, {
    ...GLOBAL.SERVERS.OPEN,
    clientOptions: {
      commandTimeout: 50,
    }
  });

@nkaradzhov
Copy link
Collaborator

nkaradzhov commented Jun 20, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants