javascript icon indicating copy to clipboard operation
javascript copied to clipboard

Add optional timeoutMs parameter to makeApiClient

Open jannikschaper opened this issue 6 months ago • 5 comments

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

jannikschaper avatar May 12 '25 17:05 jannikschaper

CLA Signed

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.

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment Approvers can cancel approval by writing /approve cancel in a comment

k8s-ci-robot avatar May 12 '25 17:05 k8s-ci-robot

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:

k8s-ci-robot avatar May 12 '25 17:05 k8s-ci-robot

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

davidgamero avatar May 28 '25 18:05 davidgamero

@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
    },
  }
}

davidgamero avatar May 29 '25 03:05 davidgamero

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/stale is applied
  • After 30d of inactivity since lifecycle/stale was applied, lifecycle/rotten is applied
  • After 30d of inactivity since lifecycle/rotten was 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

k8s-triage-robot avatar Aug 27 '25 03:08 k8s-triage-robot

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!

jannikschaper avatar Aug 28 '25 15:08 jannikschaper

@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 avatar Oct 30 '25 11:10 jannikschaper

@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

DrauzJu avatar Nov 04 '25 11:11 DrauzJu