Skip to content
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

When a remote is offline, it takes multiple seconds on Windows before a remote is considered as unavailable. #2367

Closed
5 tasks done
patricklafrance opened this issue Apr 23, 2024 · 8 comments
Labels

Comments

@patricklafrance
Copy link

patricklafrance commented Apr 23, 2024

Describe the bug

When a remote is offline, it takes multiple seconds on Windows before a remote is considered as unavailable, leaving the users with a blank page until the shared dependencies negotiation is done and the application is rendered.

According to my POC, a production build hosted on Netlify, when a remote is offline on Windows, it takes approximately 2500ms until the application is rendered.

On macOS, it takes about 20ms, which is a lot faster.

This delay doesn't seems to be related to any custom code but rather on how Windows behave when a connection is refused. It seems to retry 3 times before failing with ERR_CONNECTION_REFUSED.

image

Similar issues has been observed for other projects:

@ryok90 and I have been extensively discussing about this issue in the past few days on the Module Federation Discord's server and came to the conclusion that there is currently nothing offered by Module Federation to actually help with this issue: https://discord.com/channels/1055442562959290389/1060923312043212920/threads/1232010715381108929

👉🏻 We believe that the solution would be for Module Federation to include a mechanism allowing the authors to specify a timeout delay to fetch a remote, rather the relying on the OS defaults.

When the host is configuring a remote with a mf-manifest.json file, it seems like the manifest is fetched with fetch, a timeout could be introduced with an AbortSignal.

When the host is configuring a remote with a remoteEntry.js file, it seems like the remote is fetched with a script element. Something similar to the following code could be added to eagerly reject a remote when it is offline. This is how we used to do it with Module Federation 1.0:

function loadRemoteScript(url: string, { timeoutDelay = 500 }: LoadRemoteScriptOptions = {}) {
    return new Promise((resolve, reject) => {
        const element = document.createElement("script");

        // Adding a timestamp to make sure the remote entry points are never cached.
        // View: https://github.com/module-federation/module-federation-examples/issues/566.
        element.src = `${url}?t=${Date.now()}`;
        element.type = "text/javascript";
        element.async = true;

        let timeoutId: number | undefined = undefined;
        let hasCanceled = false;

        function cancel(error: Error) {
            hasCanceled = true;

            element?.parentElement?.removeChild(element);

            reject({
                error,
                hasCanceled: true
            });
        }

        element.onload = () => {
            window.clearTimeout(timeoutId);

            element?.parentElement?.removeChild(element);
            resolve({});
        };

        element.onerror = (error: unknown) => {
            if (!hasCanceled) {
                window.clearTimeout(timeoutId);

                element?.parentElement?.removeChild(element);

                reject({
                    error,
                    hasCanceled: false
                });
            }
        };

        document.head.appendChild(element);

        // Eagerly reject the loading of a script, it's too long when a remote is unavailable.
        timeoutId = window.setTimeout(() => {
            cancel(new Error(`[squide] Remote script "${url}" time-outed.`));
        }, timeoutDelay);
    });
}

Thank you,

Patrick

Reproduction

https://pat-mf-enhanced-poc.netlify.app/

Used Package Manager

pnpm

System Info

System:
    OS: Windows 11 10.0.22631
    CPU: (12) x64 Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
    Memory: 13.30 GB / 31.70 GB
  Binaries:
    Node: 21.7.1 - C:\Program Files\nodejs\node.EXE
    Yarn: 1.22.19 - C:\Program Files (x86)\Yarn\bin\yarn.CMD
    npm: 10.5.0 - C:\Program Files\nodejs\npm.CMD
    pnpm: 8.15.4 - ~\AppData\Roaming\npm\pnpm.CMD
  Browsers:
    Chrome: 124.0.6367.61
    Edge: Chromium (123.0.2420.97)

Validations

@ScriptedAlchemy
Copy link
Member

createScript hook is probbably what you are after: https://module-federation.io/plugin/dev/index.html#createscript

@patricklafrance
Copy link
Author

patricklafrance commented Apr 23, 2024

@ScriptedAlchemy I have been on this road, the issue with the createScript hook is that I don't have any control over when Module Federation choose to stop waiting for the remote to be available, it's basically only a script element factory.

@ScriptedAlchemy
Copy link
Member

cant you throw the script after timeout?

@ScriptedAlchemy ScriptedAlchemy added the ✨ enhancement New feature or request label Apr 24, 2024
@patricklafrance
Copy link
Author

patricklafrance commented Apr 24, 2024

@ScriptedAlchemy not exactly.

If I always throw an Error in the createScript hook:

createScript({ url }) {
    throw new Error(`Remote script "${url}" time-outed.`);
}

Then, it kinds of work as expected since the catch handler of my loadRemote function catch the error:

await loadRemote("remote1/HelloWorld.jsx")
    .then(mod => {
        console.log("Loaded remote 1", mod);
    })
    .catch((error) => console.log("Failed to load remote 1", error));

But the error that is catched, is not the exception I thrown:

TypeError: remoteEntryExports.get is not a function

Then, if I delay the throw of the Error by 500ms for the timeout:

createScript({ url }) {
    const element = document.createElement("script");

    // Adding a timestamp to make sure the remote entry points are never cached.
    // View: https://github.com/module-federation/module-federation-examples/issues/566.
    element.src = `${url}?t=${Date.now()}`;
    element.type = "text/javascript";
    element.async = true;

    let timeoutId = undefined;

    element.onload = () => {
        window.clearTimeout(timeoutId);
    }

    // Eagerly reject the loading of a script, it's too long when a remote is unavailable.
    timeoutId = window.setTimeout(() => {
        throw new Error(`Remote script "${url}" time-outed.`);
    }, 500);

    return element;
}

It doesn't work anymore as the error is not catched anymore by the catch handler of the loadRemote function and it crashes the application. I don't feel like the code around the createScript hook has been written to handle a throw.

@patricklafrance
Copy link
Author

I also tried hustling something with the beforeRequest hook, but I haven't been to because of #2371.

@ScriptedAlchemy
Copy link
Member

ScriptedAlchemy commented May 1, 2024

@patricklafrance https://github.com/module-federation/core/pull/2433/files

hows that?

This would change the return types of createScript

return htmlElement | {script?: htmlElement, timeout?:number}

return {script: myScript}
return {timeout: 1000}
return {script,timeout}
return scriptElement

@patricklafrance
Copy link
Author

patricklafrance commented May 1, 2024

@ScriptedAlchemy LGTM. One question thought, as a consumer, how do I handle:

onScriptComplete(null, new Error(`Remote script "${url}" time-outed.`)); ?

Is it with either the errorLoadRemote hook or loadRemote.catch ?

Copy link
Contributor

github-actions bot commented Jul 1, 2024

Stale issue message

@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Jul 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants