This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new c199b40742 [ZEPPELIN-6638] Time out the React remote entry load
c199b40742 is described below

commit c199b4074278316c99e5cc53304aeca3992b5f73
Author: 김예나 <[email protected]>
AuthorDate: Thu Aug 13 01:05:56 2026 +0900

    [ZEPPELIN-6638] Time out the React remote entry load
    
    ### What is this PR for?
    
    `ReactRemoteLoaderService` settles `loadContainer()` only from the script 
tag's `onload` and `onerror`. Neither fires while a request is merely pending, 
and there is no timer. A `remoteEntry.js` request that the server accepts and 
never answers therefore leaves the promise pending for as long as the browser 
keeps the connection open, which is minutes.
    
    `onError` is never called, so the hosts that depend on it never fall back. 
The paragraph footer keeps an empty mount div instead of restoring 
`zeppelin-notebook-paragraph-footer`, and the published paragraph renders 
nothing. Both look like a slow page rather than a failed load.
    
    This bounds the script load with `environment.reactRemoteLoadTimeoutMs` (10 
s, or 0 to disable) and reuses the existing `fail()` path on expiry, which 
removes the tag and leaves the caches drained so a later mount can retry.
    
    The chunks that `container.get()` pulls are left alone. They are fetched by 
the remote's own webpack runtime, which already bounds them with 
`output.chunkLoadTimeout` (120 s by default). A second, shorter timer over that 
path would cut off a multi-megabyte chunk on a slow connection, which is a 
worse failure than the one being fixed.
    
    ### What type of PR is it?
    
    Bug Fix
    
    ### Todos
    
    None
    
    ### What is the Jira issue?
    
    https://issues.apache.org/jira/browse/ZEPPELIN-6638
    
    ### How should this be tested?
    
    * Two new Playwright cases in 
`e2e/tests/notebook/paragraph/react-footer.spec.ts`: one holds `remoteEntry.js` 
open without ever answering and asserts the Angular footer comes back, one 
answers after a delay well inside the budget and asserts the React footer still 
renders. The first fails on master and passes here; verified by setting the 
budget to 0, which reproduces the current behaviour and makes it fail.
    * The existing abort and delay cases in the same suite, and 
`e2e/tests/notebook/published/published-paragraph.spec.ts`, for regressions. 
Chromium, all green.
    * A production build (`npm run build`).
    * A unit spec for the service would be the better home for the timer, but 
the Angular shell has no test harness on master yet (ZEPPELIN-6566, 
ZEPPELIN-6567, ZEPPELIN-6637) and this file carries an `<at>Injectable` 
decorator, which the current setup cannot compile. Left to a follow-up once 
that lands.
    
    ### Screenshots (if appropriate)
    
    No
    
    ### Questions:
    
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? Yes, the loader section of 
`projects/zeppelin-react/README.md` is updated in this PR
    
    Closes #5409 from kimyenac/ZEPPELIN-6638.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../tests/notebook/paragraph/react-footer.spec.ts  | 42 ++++++++++++++++++++++
 .../projects/zeppelin-react/README.md              |  4 ++-
 .../react-mount/react-remote-loader.service.ts     | 20 +++++++++--
 .../src/environments/environment.prod.ts           |  3 +-
 .../src/environments/environment.ts                |  5 ++-
 5 files changed, 69 insertions(+), 5 deletions(-)

diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
index fd102b36d4..b82065f9bc 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
@@ -83,6 +83,48 @@ test.describe('React Paragraph Footer', () => {
     await 
expect(page.locator('[data-testid="react-paragraph-footer"]')).toHaveCount(0);
   });
 
+  test('when the remote never answers, paragraphs fall back to the Angular 
footer', async ({ page }) => {
+    const { noteId } = testNotebook;
+
+    await test.step('Given a remote that accepts the request and never 
answers', async () => {
+      // The handler settles nothing on purpose: the request is left open.
+      await page.route('**/remoteEntry.js', () => {});
+    });
+
+    await test.step('When the notebook opens with the React footer enabled', 
async () => {
+      await page.goto(`/#/notebook/${noteId}?reactFooter=true`);
+      await waitForZeppelinReady(page);
+    });
+
+    await test.step('Then the Angular footer takes over once the load budget 
expires', async () => {
+      await 
expect(page.locator('[data-testid="angular-paragraph-footer"]').first()).toBeAttached({
 timeout: 30000 });
+      await 
expect(page.locator('[data-testid="react-paragraph-footer"]')).toHaveCount(0);
+    });
+  });
+
+  test('a remote that answers within the budget still renders the React 
footer', async ({ page }) => {
+    const { noteId } = testNotebook;
+
+    await test.step('Given a remote that answers slowly but well inside the 
budget', async () => {
+      await page.route('**/remoteEntry.js', async route => {
+        await new Promise(r => setTimeout(r, 2000));
+        await route.continue();
+      });
+    });
+
+    await test.step('When the notebook opens with the React footer enabled', 
async () => {
+      await page.goto(`/#/notebook/${noteId}?reactFooter=true`);
+      await waitForZeppelinReady(page);
+    });
+
+    await test.step('Then the React footer renders and no fallback happens', 
async () => {
+      await 
expect(page.locator('[data-testid="react-paragraph-footer-content"]').first()).toBeAttached({
+        timeout: 20000
+      });
+      await 
expect(page.locator('[data-testid="angular-paragraph-footer"]')).toHaveCount(0);
+    });
+  });
+
   test('navigating away during remoteEntry load does not throw', async ({ page 
}) => {
     const { noteId } = testNotebook;
 
diff --git a/zeppelin-web-angular/projects/zeppelin-react/README.md 
b/zeppelin-web-angular/projects/zeppelin-react/README.md
index f452ee1455..a32e90643f 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/README.md
+++ b/zeppelin-web-angular/projects/zeppelin-react/README.md
@@ -21,7 +21,9 @@ React micro-frontend that runs alongside the Angular host via 
[Webpack Module Fe
 The Angular host's `src/app/share/react-mount/` exports two pieces:
 
 - `ReactRemoteLoaderService` — loads `remoteEntry.js` once per page,
-  caches per-module promises, evicts on error.
+  caches per-module promises, evicts on error. The load is bounded by
+  `environment.reactRemoteLoadTimeoutMs`, so a remote that stalls instead
+  of failing still reaches the host's `onError` and its fallback.
 - `ReactMountDirective` — owns the host element, mounts outside the
   Angular zone, forwards `[reactProps]` changes through
   `handle.update(...)`, and unmounts on destroy. Re-checks `destroyed`
diff --git 
a/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts 
b/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
index c3f45911ea..2c903f529a 100644
--- 
a/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
+++ 
b/zeppelin-web-angular/src/app/share/react-mount/react-remote-loader.service.ts
@@ -45,14 +45,20 @@ export class ReactRemoteLoaderService {
       script.src = environment.reactRemoteEntryUrl;
       script.async = true;
 
-      // Remove the tag on *any* failure (network error or 
loaded-but-unregistered):
-      // containerPromise resets on rejection, so each retry would otherwise 
leak a tag.
+      const timeoutMs = environment.reactRemoteLoadTimeoutMs;
+      let timer: ReturnType<typeof setTimeout> | undefined;
+
+      // Remove the tag on *any* failure (network error, timeout, or
+      // loaded-but-unregistered): containerPromise resets on rejection, so 
each
+      // retry would otherwise leak a tag.
       const fail = (message: string) => {
+        clearTimeout(timer);
         script.remove();
         reject(new Error(message));
       };
 
       script.onload = () => {
+        clearTimeout(timer);
         if (!window.reactApp) {
           fail('window.reactApp not registered after script load');
           return;
@@ -60,6 +66,16 @@ export class ReactRemoteLoaderService {
         resolve(window.reactApp);
       };
       script.onerror = () => fail(`Failed to load React remote at 
${script.src}`);
+
+      // A request the server accepts but never answers fires neither onload 
nor
+      // onerror, so without this the promise stays pending for minutes.
+      if (timeoutMs > 0) {
+        timer = setTimeout(
+          () => fail(`Timed out after ${timeoutMs} ms loading the React remote 
at ${script.src}`),
+          timeoutMs
+        );
+      }
+
       document.head.appendChild(script);
     });
 
diff --git a/zeppelin-web-angular/src/environments/environment.prod.ts 
b/zeppelin-web-angular/src/environments/environment.prod.ts
index 8613a332bc..606214bc64 100644
--- a/zeppelin-web-angular/src/environments/environment.prod.ts
+++ b/zeppelin-web-angular/src/environments/environment.prod.ts
@@ -12,5 +12,6 @@
 
 export const environment = {
   production: true,
-  reactRemoteEntryUrl: '/assets/react/remoteEntry.js'
+  reactRemoteEntryUrl: '/assets/react/remoteEntry.js',
+  reactRemoteLoadTimeoutMs: 10000
 };
diff --git a/zeppelin-web-angular/src/environments/environment.ts 
b/zeppelin-web-angular/src/environments/environment.ts
index c20bf371d2..aab3beca1b 100644
--- a/zeppelin-web-angular/src/environments/environment.ts
+++ b/zeppelin-web-angular/src/environments/environment.ts
@@ -16,7 +16,10 @@
 
 export const environment = {
   production: false,
-  reactRemoteEntryUrl: 'http://localhost:3001/remoteEntry.js'
+  reactRemoteEntryUrl: 'http://localhost:3001/remoteEntry.js',
+  // Budget for fetching remoteEntry.js, after which the host falls back.
+  // Set to 0 to disable the timer.
+  reactRemoteLoadTimeoutMs: 10000
 };
 
 /*

Reply via email to