javascript
javascript copied to clipboard
Add optional timeoutMs parameter to makeApiClient
Fixes https://github.com/kubernetes-client/javascript/issues/1613
I followed @brendandburns comments from the linked issue:
I think it is totally fine to add an optional typed parameter to the makeApiClient signature. Thanks for checking!
And I'm pretty sure that it will work. That's the Configuration object that flows down through to fetch ultimately (if I tracked the code right
The committers listed above are authorized under a signed CLA.
- :white_check_mark: login: jannikschaper (688f40149fe20c20f1fa93330491dab550e1301a)
[APPROVALNOTIFIER] This PR is NOT APPROVED
This pull-request has been approved by: jannikschaper Once this PR has been reviewed and has the lgtm label, please assign mstruebing for approval. For more information see the Code Review Process.
The full list of commands accepted by this bot can be found here.
Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment
Welcome @jannikschaper!
It looks like this is your first PR to kubernetes-client/javascript 🎉. Please refer to our pull request process documentation to help your PR have a smooth ride to approval.
You will be prompted by a bot to use commands during the review process. Do not be afraid to follow the prompts! It is okay to experiment. Here is the bot commands documentation.
You can also check if kubernetes-client/javascript has its own contribution guidelines.
You may want to refer to our testing guide if you run into trouble with your tests not passing.
If you are having difficulty getting your pull request seen, please follow the recommended escalation practices. Also, for tips and tricks in the contribution process you may want to read the Kubernetes contributor cheat sheet. We want to make sure your contribution gets all the attention it needs!
Thank you, and welcome to Kubernetes. :smiley:
i'm testing out a PR on the generator for passing through the signal property on the RequestContext type
middleware has access to the RequestContext which would allow adding a middleware that sets the abortsignal
another alternative is to add a signal input to the Configuration Type to expose it directly in the options. this would be a more accessible call, but it would add more complexity
https://github.com/OpenAPITools/openapi-generator/pull/21323
@jannikschaper how do you feel about the middleware injection approach?
something like this:
/**
* Middleware that adds an abort signal to the request context.
* This can be used to abort requests using an AbortController.
* @param signal AbortSignal to use for the request
* @returns Middleware that sets the signal in the request context
*/
function abortSignalMiddleware(signal: AbortSignal): Middleware {
return {
pre: async (c: RequestContext) => {
c.setSignal(signal)
return c
},
post: async (c: ResponseContext) => {
return c
},
}
}
The Kubernetes project currently lacks enough contributors to adequately respond to all PRs.
This bot triages PRs according to the following rules:
- After 90d of inactivity,
lifecycle/staleis applied - After 30d of inactivity since
lifecycle/stalewas applied,lifecycle/rottenis applied - After 30d of inactivity since
lifecycle/rottenwas applied, the PR is closed
You can:
- Mark this PR as fresh with
/remove-lifecycle stale - Close this PR with
/close - Offer to help out with Issue Triage
Please send feedback to sig-contributor-experience at kubernetes/community.
/lifecycle stale
It works!
const makeApiClient = <T extends ApiType>(
kubeConfig: KubeConfig,
ApiClientType: ApiConstructor<T>,
requestTimeoutInSeconds: number,
): T => {
// Copied from KubeConfig.makeApiClient and adapted to add timeout
// Suggested by @kubernetes/client-node maintainer (https://github.com/kubernetes-client/javascript/pull/2430#issuecomment-2918109624)
const cluster = kubeConfig.getCurrentCluster();
if (!cluster) {
throw new Error('No active cluster!');
}
return new ApiClientType(createConfiguration({
baseServer: new ServerConfiguration(cluster.server, {}),
authMethods: {
default: kubeConfig,
},
middleware: [{
pre: (context) => {
const controller = new AbortController();
setTimeout(() => controller.abort(), requestTimeoutInSeconds * 1000);
context.setSignal(controller.signal);
return new Observable(Promise.resolve(context));
},
post: (context) => new Observable(Promise.resolve(context)),
}],
}));
};
Thank you so much @davidgamero for going out of your way and getting PRs merged both here and even in OpenAPI to fix my use case. Using a signal is better than a hardcoded timeout value, because you can implement a timeout and more on top of it, good idea. Thanks!
@davidgamero I have a follow-up question to the timeout approach you suggested, if you don't mind
How to clean up dangling timeout instances when requests complete normally? For example, with a 120s timeout and on average 10 successful Kubernetes API requests per second, there would be over a thousand timeout promises still hanging around in the NodeJS event loop.
Each of them also eventually calling .abort() on the request, which is additional mental load to make sure these delayed false-positive aborts don't cause troubles.
A naive implementation might look like this:
function timeoutMiddleware(milliseconds: number): Middleware {
let timeoutId: NodeJS.Timeout;
return {
pre: async (c: RequestContext) => {
const abortController = new AbortController();
timeoutId = setTimeout(() => abortController.abort(), milliseconds);
c.setSignal(abortController.signal);
return c;
},
post: async (c: ResponseContext) => {
clearTimeout(timeoutId);
return c;
},
};
}
But this is wrong, because parallel requests would overwrite each other's timeoutId. You'd need to attach the timeoutId to the request context, but as far as I can tell, there is no way to inject a value into a RequestContext and get it out reliably in the ResponseContext. The only possibility I can think of is to inject a cookie with a timeout ID in the request and hope that the Kubernetes API server returns the cookie in its response. But this is obviously a terrible solution.
Do you have an idea how to solve the problem of dangling timeouts with this library? Is there simply no way, and the best solution is to just deal with dangling timeouts aborting each not-anymore-existent request long after it finished?
Thank you for your help!
@jannikschaper We have found a solution for that:
promiseMiddleware: [{
pre: async (context) => {
context.setSignal(AbortSignal.timeout(requestTimeoutInSeconds * 1000));
return context;
},
post: async (context) => context,
}],
This way, we don't have to cleanup the timeout ourself because NodeJs internally calls Timeout.unref() to prevent the process from exiting if there is still such a pending timeout: https://github.com/nodejs/node/blob/3a33ee3cf09a36f6c2456b053efa8d7f20728956/lib/internal/abort_controller.js#L139-L161