diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/one/DepAClass.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/one/DepAClass.java new file mode 100644 index 000000000000..2bb75c387e59 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/one/DepAClass.java @@ -0,0 +1,9 @@ +package org.enso.example.deps.one; + +import org.enso.example.deps.two.DepBClass; + +public class DepAClass { + public static double expToNeg(double x) { + return DepBClass.expPow(-x); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/two/DepBClass.java b/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/two/DepBClass.java new file mode 100644 index 000000000000..1c961785faa1 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/example/deps/two/DepBClass.java @@ -0,0 +1,13 @@ +package org.enso.example.deps.two; + +import org.enso.example.deps.one.DepAClass; + +public class DepBClass { + public static double sigmoid(double x) { + return 1 / (1 + DepAClass.expToNeg(x)); + } + + public static double expPow(double x) { + return Math.exp(x); + } +} diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java index 0a1cc7c6e384..1ec8ce5e8acb 100644 --- a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/test/interop/JavaInteropTest.java @@ -5,7 +5,10 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ForkJoinPool; import org.enso.example.TestClass; import org.enso.test.utils.ContextUtils; import org.graalvm.polyglot.PolyglotException; @@ -503,6 +506,32 @@ public void catchCheckedSubExceptionThrownInJava() { assertEquals(result.asInt(), -1); } + @Test + public void multiThreadedAccess() throws Exception { + var code1 = + """ + from Standard.Base import all + polyglot java import org.enso.example.deps.one.DepAClass + + main = + DepAClass.expToNeg 2 + """; + var code2 = + """ + from Standard.Base import all + polyglot java import org.enso.example.deps.two.DepBClass + + main = + DepBClass.sigmoid 2 + """; + List> cases = new ArrayList<>(); + cases.add(() -> ctx().evalModule(code1).asDouble()); + cases.add(() -> ctx().evalModule(code2).asDouble()); + var results = ForkJoinPool.commonPool().invokeAll(cases); + assertEquals(results.get(0).get().doubleValue(), 0.1353352832366127, 0); + assertEquals(results.get(1).get().doubleValue(), 0.8807970779778823, 0); + } + private Value checkedException(int t) { var code = """ diff --git a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java index 49b25c376387..9138c9ffe458 100644 --- a/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java +++ b/engine/runtime-language-epb/src/main/java/org/enso/interpreter/epb/HostClassLoader.java @@ -18,8 +18,10 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; -import java.util.Map; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; import org.graalvm.polyglot.Context; /** @@ -30,7 +32,8 @@ @ExportLibrary(InteropLibrary.class) final class HostClassLoader extends URLClassLoader implements AutoCloseable, TruffleObject { - private final Map> loadedClasses = new ConcurrentHashMap<>(); + private final ConcurrentHashMap>> loadedClasses = + new ConcurrentHashMap<>(); private static final Logger logger = System.getLogger(HostClassLoader.class.getName()); // Classes from "org.graalvm" packages are loaded either by a class loader for the boot // module layer, or by a specific class loader, depending on how enso is run. For example, @@ -63,49 +66,66 @@ public Class loadClass(String name) throws ClassNotFoundException { @Override @CompilerDirectives.TruffleBoundary + @SuppressWarnings("unchecked") protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { logger.log(Logger.Level.TRACE, "Loading class {0}", name); - var l = loadedClasses.get(name); - if (l != null) { - logger.log(Logger.Level.TRACE, "Class {0} found in cache", name); - return l; - } - synchronized (this) { - l = loadedClasses.get(name); - if (l != null) { - logger.log(Logger.Level.TRACE, "Class {0} found in cache", name); - return l; + var placeholder = new CompletableFuture[1]; + var pendingClass = + loadedClasses.computeIfAbsent( + name, + t -> { + logger.log(Logger.Level.TRACE, "Class {0} found in cache", name); + var f = new CompletableFuture>(); + placeholder[0] = f; + return f; + }); + if (placeholder[0] != null) { + try { + placeholder[0].complete(loadClassUnsafe(name, resolve)); + } catch (ClassNotFoundException e) { + placeholder[0].completeExceptionally(e); } - if (!isRuntimeModInBootLayer && name.startsWith("org.graalvm")) { - return polyglotClassLoader.loadClass(name); + } + try { + return pendingClass.get(); + } catch (InterruptedException | ExecutionException e) { + if (e.getCause() instanceof ClassCastException cce) { + throw cce; } - if (name.startsWith("org.slf4j")) { - // Delegating to system class loader ensures that log classes are not loaded again - // and do not require special setup. In other words, it is using log configuration that - // has been setup by the runner that started the process. See #11641. - return polyglotClassLoader.loadClass(name); + throw new ClassNotFoundException("Unable to find class " + name, e); + } + } + + /** Find a class with a given name without giving any Thread-safety guarantees. */ + private Class loadClassUnsafe(String name, boolean resolve) throws ClassNotFoundException { + if (!isRuntimeModInBootLayer && name.startsWith("org.graalvm")) { + return polyglotClassLoader.loadClass(name); + } + if (name.startsWith("org.slf4j")) { + // Delegating to system class loader ensures that log classes are not loaded again + // and do not require special setup. In other words, it is using log configuration that + // has been setup by the runner that started the process. See #11641. + return polyglotClassLoader.loadClass(name); + } + try { + var l = findClass(name); + if (resolve) { + l.getMethods(); } - try { - l = findClass(name); - if (resolve) { - l.getMethods(); - } - logger.log(Logger.Level.TRACE, "Class {0} found, putting in cache", name); - loadedClasses.put(name, l); - return l; - } catch (ClassNotFoundException ex) { - logger.log(Logger.Level.TRACE, "Class {0} not found, delegating to super", name); + logger.log(Logger.Level.TRACE, "Class {0} found, putting in cache", name); + return l; + } catch (ClassNotFoundException ex) { + logger.log(Logger.Level.TRACE, "Class {0} not found, delegating to super", name); + return super.loadClass(name, resolve); + } catch (Throwable e) { + if (isAttemptToLoadBytecodeInNI(e)) { + logger.log( + Logger.Level.TRACE, + "Attempt to load bytecode for class {0}, delegating to super" + name); return super.loadClass(name, resolve); - } catch (Throwable e) { - if (isAttemptToLoadBytecodeInNI(e)) { - logger.log( - Logger.Level.TRACE, - "Attempt to load bytecode for class {0}, delegating to super" + name); - return super.loadClass(name, resolve); - } else { - logger.log(Logger.Level.TRACE, "Failure while loading a class: " + e.getMessage(), e); - throw e; - } + } else { + logger.log(Logger.Level.TRACE, "Failure while loading a class: " + e.getMessage(), e); + throw e; } } }