From fdfd4dd5e3cc4b73a4498b7ccef0bea6a7ffc851 Mon Sep 17 00:00:00 2001 From: NathanGrand Date: Thu, 5 Mar 2026 10:07:32 +0000 Subject: [PATCH 1/3] Add timeout for invokeGuestFunction; base64 encode in cache --- .../quickjs4j/core/BasicScriptCache.java | 15 ++++---- .../io/roastedroot/quickjs4j/core/Runner.java | 5 ++- .../quickjs4j/core/RunnerTest.java | 37 +++++++++++++++++++ 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java b/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java index 9af0196..a8a73ec 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java @@ -1,8 +1,8 @@ package io.roastedroot.quickjs4j.core; -import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Base64; import java.util.HashMap; public class BasicScriptCache implements ScriptCache, AutoCloseable { @@ -24,19 +24,20 @@ public BasicScriptCache(String messageDigestAlgorithm) { } } + private String cacheKey(byte[] code) { + return Base64.getEncoder().encodeToString(messageDigest.digest(code)); + } + public boolean exists(byte[] code) { - var key = messageDigest.digest(code); - return cache.containsKey(new String(key, StandardCharsets.UTF_8)); + return cache.containsKey(cacheKey(code)); } public void set(byte[] code, byte[] compiled) { - var key = messageDigest.digest(code); - cache.put(new String(key, StandardCharsets.UTF_8), compiled); + cache.put(cacheKey(code), compiled); } public byte[] get(byte[] code) { - var key = messageDigest.digest(code); - return cache.get(new String(key, StandardCharsets.UTF_8)); + return cache.get(cacheKey(code)); } public void close() { diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java index 8afa242..59b01c5 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java @@ -60,7 +60,10 @@ public void compileAndExec(String code) { public Object invokeGuestFunction( String moduleName, String name, List args, String libraryCode) { - return engine.invokeGuestFunction(moduleName, name, args, libraryCode); + return submitWithTimeout( + () -> engine.invokeGuestFunction(moduleName, name, args, libraryCode), + this.timeoutMs, + "Timeout while invoking guest function"); } public String stdout() { diff --git a/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java b/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java index 7d11aba..2a4adaa 100644 --- a/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java +++ b/core/src/test/java/io/roastedroot/quickjs4j/core/RunnerTest.java @@ -339,6 +339,43 @@ public void compileTimeout() throws Exception { runner.close(); } + @Test + public void invokeGuestFunctionTimeout() throws Exception { + var invokables = + Invokables.builder("from_js") + .add(new GuestFunction("hang", List.of(), Integer.class)) + .build(); + + var libraryCode = "function hang() { while(true) {} return 1; };"; + + var es = Executors.newSingleThreadExecutor(); + var jsEngine = Engine.builder().addInvokables(invokables).build(); + var runner = + Runner.builder() + .withEngine(jsEngine) + .withTimeoutMs(500) + .withExecutorService(es) + .build(); + + var ex = + assertThrows( + RuntimeException.class, + () -> + runner.invokeGuestFunction( + "from_js", "hang", List.of(), libraryCode)); + + assertTrue(ex.getCause() instanceof TimeoutException); + assertTrue( + ex.getMessage().contains("Timeout while invoking guest function"), + "Expected execution timeout, got: " + ex.getMessage()); + + // Verify the executor thread is free after timeout + var probe = es.submit(() -> "ok"); + assertEquals("ok", probe.get(5, java.util.concurrent.TimeUnit.SECONDS)); + + runner.close(); + } + @Test public void handleExceptionsThrownInJava() { var builtins = From 59834cbfe3fd34afde1ea1e1fc77a6aa0943c267 Mon Sep 17 00:00:00 2001 From: NathanGrand Date: Thu, 5 Mar 2026 11:16:52 +0000 Subject: [PATCH 2/3] sneakyThrow to preserve existing exceptions --- .../java/io/roastedroot/quickjs4j/core/Runner.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java index 59b01c5..81f0605 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java @@ -101,17 +101,17 @@ private T submitWithTimeout(Callable task, int timeout, String timeoutMes throw new RuntimeException("Thread interrupted", e); } catch (ExecutionException e) { if (e.getCause() != null) { - if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } else { - throw new RuntimeException(e.getCause()); - } - } else { - throw new RuntimeException(e); + sneakyThrow(e.getCause()); } + throw new RuntimeException(e); } } + @SuppressWarnings("unchecked") + private static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } + public static Builder builder() { return new Builder(); } From 794d5b8accc5a996601f9b38934939343b0a9103 Mon Sep 17 00:00:00 2001 From: NathanGrand Date: Thu, 5 Mar 2026 13:19:04 +0000 Subject: [PATCH 3/3] Drop cache changes in favour of https://github.com/roastedroot/quickjs4j/pull/145 --- .../quickjs4j/core/BasicScriptCache.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java b/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java index a8a73ec..9af0196 100644 --- a/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java +++ b/core/src/main/java/io/roastedroot/quickjs4j/core/BasicScriptCache.java @@ -1,8 +1,8 @@ package io.roastedroot.quickjs4j.core; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Base64; import java.util.HashMap; public class BasicScriptCache implements ScriptCache, AutoCloseable { @@ -24,20 +24,19 @@ public BasicScriptCache(String messageDigestAlgorithm) { } } - private String cacheKey(byte[] code) { - return Base64.getEncoder().encodeToString(messageDigest.digest(code)); - } - public boolean exists(byte[] code) { - return cache.containsKey(cacheKey(code)); + var key = messageDigest.digest(code); + return cache.containsKey(new String(key, StandardCharsets.UTF_8)); } public void set(byte[] code, byte[] compiled) { - cache.put(cacheKey(code), compiled); + var key = messageDigest.digest(code); + cache.put(new String(key, StandardCharsets.UTF_8), compiled); } public byte[] get(byte[] code) { - return cache.get(cacheKey(code)); + var key = messageDigest.digest(code); + return cache.get(new String(key, StandardCharsets.UTF_8)); } public void close() {