-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
base: master
Are you sure you want to change the base?
Conversation
4ad6a4a
to
90f8fe9
Compare
90f8fe9
to
e8eb95b
Compare
Hi @nkaradzhov sorry for pinging you directly, but I couldn't find information on how to contact maintainers for this project. |
@florian-schunk hi, sorry for the delay, been a bit busy. I will take a look at this. |
packages/client/lib/client/index.ts
Outdated
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 | ||
} | ||
} |
There was a problem hiding this comment.
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
};
}
There was a problem hiding this comment.
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.
packages/client/lib/client/index.ts
Outdated
|
||
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) | ||
}); | ||
}) |
There was a problem hiding this comment.
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
packages/client/lib/errors.ts
Outdated
export class CommandTimeoutError extends Error { | ||
constructor() { | ||
super('Command timeout'); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
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, | ||
} | ||
}); | ||
|
There was a problem hiding this comment.
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,
}
});
I will take a look, maybe will make a pr to your pr
…On Fri, Jun 20, 2025, 6:16 PM florian-schunk ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In packages/client/lib/client/index.ts
<#2981 (comment)>:
> + 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
+ }
+ }
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.
—
Reply to this email directly, view it on GitHub
<#2981 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AALIHLDULWEBI6J3YYY37E33EQQUVAVCNFSM6AAAAAB6CNSAFSVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHMZDSNBWGU2DKMRQGE>
.
You are receiving this because you were mentioned.Message ID:
***@***.***>
|
@florian-schunk i made a PR to your fork |
use AbortSignal utilities instead of setTimeout
|
||
testUtils.testWithClient('rejects with AbortError on commandTimeout timer', async client => { | ||
const start = process.hrtime.bigint(); | ||
const promise = client.ping(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just for my understanding: What is the difference between sendCommand(['PING'] and ping()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not much. sendCommand
is a catch-all command that can send any command. Usually, we would have all the commands available as dedicated methods, like client.ping()
, client.hset()
etc. But sometimes there are commands that are not yet implemented as a dedicated method. In those cases, users can fallback to using sendCommand
One significant difference is that sendCommand
takes additional options. In the options, there is the abortSignal
. So in practice, anyone who needs to be able to timeout a command can use sendCommand
with abortSignal
to achieve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CLGTM
@florian-schunk, I just spoke with our architect and he suggested we also test if this works properly with cluster and sentinel. While looking into this, i understood that we are not exposing this parameter in the cluster config and sentinel config. So it will need a bit more work. Let me take this for now and I will make a PR to your fork once im ready. It will take some time as I will not be available next week. |
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
npm test
pass with this change (including linting)?