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);