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

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 0e2528e284 correctly release the namespace when switching projects, 
fixes #8972 (#8973)
0e2528e284 is described below

commit 0e2528e2848b55a9a71ac15d99f1196e2818ae69
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Mon Oct 19 10:37:04 2026 +0200

    correctly release the namespace when switching projects, fixes #8972 (#8973)
    
    * correctly release the namespace when switching projects, fixes #8972
    
    * enable code disabled to test
---
 .../main/java/org/apache/hop/core/vfs/HopVfs.java  |  3 +
 .../org/apache/hop/core/vfs/HopVfsNamespaces.java  | 13 ++++
 .../hop/core/vfs/HopVfsClosedNamespaceTest.java    | 70 ++++++++++++++++++++++
 .../main/java/org/apache/hop/ui/hopgui/HopGui.java | 10 +++-
 4 files changed, 95 insertions(+), 1 deletion(-)

diff --git a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java 
b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
index 705ff3736b..7337ba450f 100644
--- a/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
+++ b/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java
@@ -254,6 +254,9 @@ public class HopVfs {
               + namespace.getDescription()
               + " is closed. Resolving with the process wide file system 
manager instead.");
     }
+    // A thread that inherited the namespace keeps it for as long as it lives 
- the linter of Hop
+    // GUI does. Let go of it here, or every file it resolves from now on 
comes through this detour.
+    HopVfsNamespaces.unbindThreadIfCurrent(namespace);
     return null;
   }
 
diff --git a/core/src/main/java/org/apache/hop/core/vfs/HopVfsNamespaces.java 
b/core/src/main/java/org/apache/hop/core/vfs/HopVfsNamespaces.java
index 59c79b6ced..02364d19ca 100644
--- a/core/src/main/java/org/apache/hop/core/vfs/HopVfsNamespaces.java
+++ b/core/src/main/java/org/apache/hop/core/vfs/HopVfsNamespaces.java
@@ -303,6 +303,19 @@ public class HopVfsNamespaces {
     }
   }
 
+  /**
+   * Stop binding a namespace to this thread, if it is the one bound. Used 
once a closed namespace
+   * is found on a thread that may fall back to the process wide manager: 
every later file would
+   * only take the same detour again.
+   *
+   * @param namespace the namespace to unbind
+   */
+  static void unbindThreadIfCurrent(HopVfsNamespace namespace) {
+    if (namespace != null && scope.get() == namespace) {
+      scope.remove();
+    }
+  }
+
   /**
    * Is this the metadata the process wide file system manager already holds 
the connections of?
    *
diff --git 
a/core/src/test/java/org/apache/hop/core/vfs/HopVfsClosedNamespaceTest.java 
b/core/src/test/java/org/apache/hop/core/vfs/HopVfsClosedNamespaceTest.java
index fb55ae80c9..e1199f3204 100644
--- a/core/src/test/java/org/apache/hop/core/vfs/HopVfsClosedNamespaceTest.java
+++ b/core/src/test/java/org/apache/hop/core/vfs/HopVfsClosedNamespaceTest.java
@@ -20,10 +20,12 @@ package org.apache.hop.core.vfs;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.File;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.commons.vfs2.FileObject;
 import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
 import org.apache.hop.core.exception.HopException;
@@ -133,4 +135,72 @@ class HopVfsClosedNamespaceTest {
         HopVfs.startsWithScheme("file:///tmp/test.hwf"),
         "The scheme must still be recognized using the fallback file system 
manager");
   }
+
+  /**
+   * Every file a thread resolves through a closed namespace takes the 
fallback and says so. A
+   * linter thread that inherited the namespace resolves a great many files, 
and at debug level the
+   * log flood alone made Hop GUI unresponsive. Once the namespace is found 
closed, the thread lets
+   * go of it.
+   */
+  @Test
+  @DisplayName("A thread lets go of a closed namespace once it finds it 
closed")
+  void aClosedNamespaceIsUnboundFromTheThread(@TempDir File tempDir) throws 
Exception {
+    File file = new File(tempDir, "run-retail-update.hwf");
+
+    HopVfsNamespace namespace = new HopVfsNamespace("project being closed");
+    HopVfsNamespaces.bindThread(namespace);
+    namespace.close();
+
+    assertNotNull(HopVfs.getFileObject(file.getAbsolutePath()));
+    assertNull(
+        HopVfsNamespaces.getCurrent(),
+        "The thread kept the closed namespace and will take the fallback for 
every file");
+  }
+
+  @Test
+  @DisplayName("A thread that inherited a closed namespace lets go of its own 
binding only")
+  void anInheritedClosedNamespaceIsUnboundFromThatThreadOnly(@TempDir File 
tempDir)
+      throws Exception {
+    File file = new File(tempDir, "run-retail-update.hwf");
+
+    HopVfsNamespace namespace = new HopVfsNamespace("project being closed");
+    HopVfsNamespaces.bindThread(namespace);
+
+    AtomicReference<Object> afterResolve = new AtomicReference<>("not run");
+    AtomicReference<Throwable> failure = new AtomicReference<>();
+    namespace.close();
+    Thread linter =
+        new Thread(
+            () -> {
+              try {
+                HopVfs.getFileObject(file.getAbsolutePath());
+                afterResolve.set(HopVfsNamespaces.getCurrent());
+              } catch (Throwable t) {
+                failure.set(t);
+              }
+            });
+    linter.start();
+    linter.join();
+
+    assertNull(failure.get());
+    assertNull(afterResolve.get(), "The background thread kept the closed 
namespace");
+    assertSame(
+        namespace,
+        HopVfsNamespaces.getCurrent(),
+        "Another thread's resolve must not change what this thread has bound");
+  }
+
+  @Test
+  @DisplayName("With several tenants a closed namespace stays bound, as it is 
rebuilt")
+  void aClosedNamespaceStaysBoundWhenTenantsShareTheJvm(@TempDir File tempDir) 
throws Exception {
+    File file = new File(tempDir, "run-retail-update.hwf");
+
+    HopVfsNamespaces.setScope(IHopScope.process());
+    HopVfsNamespace namespace = new HopVfsNamespace("one session of several");
+    HopVfsNamespaces.bindThread(namespace);
+    namespace.close();
+
+    assertNotNull(HopVfs.getFileObject(file.getAbsolutePath()));
+    assertSame(namespace, HopVfsNamespaces.getCurrent());
+  }
 }
diff --git a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java 
b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
index 1f7e92803f..eb6ca942d1 100644
--- a/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
+++ b/ui/src/main/java/org/apache/hop/ui/hopgui/HopGui.java
@@ -529,7 +529,15 @@ public class HopGui
 
     // Bind it for this session, so the call sites that resolve a file with no 
variables in hand
     // land in it too. Deliberately not restored afterwards: it stays for as 
long as the session.
-    HopVfsNamespaces.bindThread(namespace);
+    // With no namespace of its own the binding is cleared rather than left 
alone: what is bound now
+    // is the previous project's namespace, which is about to be released and 
closed. Leaving it
+    // would send every file this thread - and every thread it starts - 
resolves through a closed
+    // manager.
+    if (namespace == null) {
+      HopVfsNamespaces.restoreThread(null);
+    } else {
+      HopVfsNamespaces.bindThread(namespace);
+    }
 
     if (previousProvider != null) {
       HopVfsNamespaces.release(previousProvider);

Reply via email to