Skip to content

Commit 3b09db1

Browse files
authored
fix: no timeout handling in invokeGuestFunction (#147)
1 parent 9f8998d commit 3b09db1

2 files changed

Lines changed: 151 additions & 5 deletions

File tree

core/src/main/java/io/roastedroot/quickjs4j/core/Runner.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,18 @@ public void close() {
9090
}
9191

9292
private <T> T submitWithTimeout(Callable<T> task, int timeout, String timeoutMessage) {
93+
if (timeout == -1) {
94+
try {
95+
return task.call();
96+
} catch (RuntimeException e) {
97+
throw e;
98+
} catch (Throwable e) {
99+
sneakyThrow(e);
100+
}
101+
}
93102
Future<T> fut = es.submit(task);
94103
try {
95-
if (timeout != -1) {
96-
return fut.get(timeout, TimeUnit.MILLISECONDS);
97-
} else {
98-
return fut.get();
99-
}
104+
return fut.get(timeout, TimeUnit.MILLISECONDS);
100105
} catch (TimeoutException e) {
101106
fut.cancel(true);
102107
throw new RuntimeException(timeoutMessage, e);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package io.roastedroot.quickjs4j.core;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotNull;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import com.fasterxml.jackson.databind.JsonNode;
8+
import com.fasterxml.jackson.databind.ObjectMapper;
9+
import com.fasterxml.jackson.databind.node.ObjectNode;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.List;
12+
import org.junit.jupiter.api.Test;
13+
14+
/**
15+
* Reproduces the Apicurio Registry failure pattern: the ScriptingService caches the
16+
* ArtifactTypeScriptProvider proxy (which wraps a Runner), but each operation calls
17+
* closeScriptProvider() which closes the Runner. Subsequent operations reuse the
18+
* cached (closed) proxy. In quickjs4j 0.0.15 this worked because
19+
* Runner.invokeGuestFunction bypassed the executor. In 0.0.16 it fails because
20+
* invokeGuestFunction now goes through submitWithTimeout, and the
21+
* executor is shut down by close().
22+
*/
23+
public class ApicurioArtifactTypesTest {
24+
25+
private static final ObjectMapper mapper = new ObjectMapper();
26+
27+
private static final String RAML_CONTENT =
28+
"#%RAML 1.0\n"
29+
+ "title: Mobile Order API\n"
30+
+ "baseUri: http://localhost:8081/api\n"
31+
+ "version: \"1.0\"\n"
32+
+ "\n"
33+
+ "uses:\n"
34+
+ " assets: assets.lib.raml\n"
35+
+ "\n"
36+
+ "annotationTypes:\n"
37+
+ " monitoringInterval:\n"
38+
+ " type: integer\n"
39+
+ "\n"
40+
+ "/orders:\n"
41+
+ " displayName: Orders\n"
42+
+ " get:\n"
43+
+ " is: [ assets.paging ]\n"
44+
+ " (monitoringInterval): 30\n"
45+
+ " description: Lists all orders of a specific user\n"
46+
+ " queryParameters:\n"
47+
+ " userId:\n"
48+
+ " type: string\n"
49+
+ " description: use to query all orders of a user\n"
50+
+ " post:\n"
51+
+ " /{orderId}:\n"
52+
+ " get:\n"
53+
+ " responses:\n"
54+
+ " 200:\n"
55+
+ " body:\n"
56+
+ " application/json:\n"
57+
+ " type: assets.Order\n"
58+
+ " application/xml:\n"
59+
+ " type: ~include schemas/order.xsd\n";
60+
61+
private String loadJsLibrary() throws Exception {
62+
var bytes =
63+
ApicurioArtifactTypesTest.class
64+
.getResourceAsStream("/apicurio-numbers/js-artifact-types-test.js")
65+
.readAllBytes();
66+
return new String(bytes, StandardCharsets.UTF_8);
67+
}
68+
69+
private Engine createEngine(Invokables invokables) {
70+
var builtins =
71+
Builtins.builder("ArtifactTypeScriptProvider_Builtins")
72+
.add(
73+
new HostFunction(
74+
"info",
75+
List.of(String.class),
76+
Void.class,
77+
(args) -> {
78+
return null;
79+
}),
80+
new HostFunction(
81+
"debug",
82+
List.of(String.class),
83+
Void.class,
84+
(args) -> {
85+
return null;
86+
}))
87+
.build();
88+
return Engine.builder().addBuiltins(builtins).addInvokables(invokables).build();
89+
}
90+
91+
@Test
92+
public void testInvokeAfterClose() throws Exception {
93+
var invokables =
94+
Invokables.builder("ArtifactTypeScriptProvider_Invokables")
95+
.add(
96+
new GuestFunction(
97+
"acceptsContent", List.of(JsonNode.class), Boolean.class))
98+
.add(new GuestFunction("validate", List.of(JsonNode.class), JsonNode.class))
99+
.build();
100+
var engine = createEngine(invokables);
101+
var runner = Runner.builder().withEngine(engine).build();
102+
var jsLibrary = loadJsLibrary();
103+
104+
// First call: acceptsContent - should work
105+
ObjectNode ramlRequest = mapper.createObjectNode();
106+
ObjectNode typedContent = mapper.createObjectNode();
107+
typedContent.put("contentType", "application/x-yaml");
108+
typedContent.put("content", RAML_CONTENT);
109+
ramlRequest.set("typedContent", typedContent);
110+
111+
var accepted =
112+
(Boolean)
113+
runner.invokeGuestFunction(
114+
"ArtifactTypeScriptProvider_Invokables",
115+
"acceptsContent",
116+
List.of(ramlRequest),
117+
jsLibrary);
118+
assertTrue(accepted);
119+
120+
// Close the runner (as Apicurio's closeScriptProvider does after each operation)
121+
runner.close();
122+
123+
// Second call: validate - reusing the closed runner (as Apicurio does via cache)
124+
ObjectNode validRequest = mapper.createObjectNode();
125+
ObjectNode validContent = mapper.createObjectNode();
126+
validContent.put("contentType", "application/x-yaml");
127+
validContent.put("content", RAML_CONTENT);
128+
validRequest.set("content", validContent);
129+
130+
var validResult =
131+
(JsonNode)
132+
runner.invokeGuestFunction(
133+
"ArtifactTypeScriptProvider_Invokables",
134+
"validate",
135+
List.of(validRequest),
136+
jsLibrary);
137+
assertNotNull(validResult);
138+
assertTrue(validResult.has("ruleViolations"));
139+
assertEquals(0, validResult.get("ruleViolations").size());
140+
}
141+
}

0 commit comments

Comments
 (0)