davsclaus commented on code in PR #26309:
URL: https://github.com/apache/camel/pull/26309#discussion_r3988738733
##########
components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java:
##########
@@ -114,15 +227,86 @@ private static GuestFunction evalFunction() {
return new GuestFunction(FUNCTION_NAME, List.of(Object.class,
String.class), Object.class);
}
+ /**
+ * The JavaScript library for one route expression: the script is
embedded, so QuickJS compiles it once per engine
+ * instead of on every evaluation. An expression is compiled as {@code
return (script)}; a script that is not a
+ * single expression (statements, a trailing semicolon) falls back to
{@code eval} so it keeps its completion value,
+ * exactly as before.
+ */
+ static String scriptLibrary(String script, boolean expressionForm) {
+ StringBuilder sb = new StringBuilder(CAMEL_FACADE);
+ sb.append("function __camelScript(").append(String.join(", ",
EXCHANGE_BINDINGS))
+ .append(", camel, java_invoke, quickjs4j_engine) {\n \"use
strict\";\n");
+ if (expressionForm) {
+ sb.append(" return (\n").append(script).append("\n );\n}\n");
+ } else {
+ sb.append(" return eval(__camelSource);\n}\n");
+ sb.append("const __camelSource =
").append(jsStringLiteral(script)).append(";\n");
+ }
+ sb.append("export function camelEval(b, s) {\n");
+ sb.append(indent(SANDBOX_ENTER, 1));
+ sb.append(" return __camelScript(");
+ for (String name : EXCHANGE_BINDINGS) {
+ sb.append("b.").append(name).append(", ");
+ }
+ sb.append("__camelFacade, __camel_stub_invoke, undefined);\n");
+ sb.append(indent(SANDBOX_EXIT, 1));
+ sb.append("}\n");
+ return sb.toString();
+ }
+
+ static byte[] compileScript(Engine engine, ByteArrayOutputStream stderr,
String script) {
+ stderr.reset();
+ try {
+ return engine.compilePortableGuestFunction(scriptLibrary(script,
true));
+ } catch (RuntimeException e) {
+ if (!isSyntaxError(e)) {
Review Comment:
Minor, but it made the probing confusing enough to be worth raising:
`isSyntaxError` matches any message containing `"Failed to compile JS code"`,
so a compile that fails for a reason *other* than bad syntax is reported to the
user as a syntax error in their script. With an engine left in a bad state I
saw a perfectly valid expression come back as:
```
org.apache.camel.ExpressionIllegalSyntaxException: Illegal syntax: body + 103
```
Narrowing the match to `SyntaxError` (the QuickJS-reported class), or
keeping the generic compile failure as an `ExpressionEvaluationException`,
would stop a runtime problem from being blamed on the route's script.
##########
components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsLanguage.java:
##########
@@ -104,40 +128,65 @@ private QuickjsExpression createQuickjsExpression(String
expression) {
* Evaluates {@code script} with optional {@code bindings} as JavaScript
function parameters. Binding names must be
* valid JavaScript identifiers; invalid names fail with {@link
ExpressionEvaluationException} rather than a raw
* JavaScript {@code SyntaxError}. Route expressions do not use this map —
they always bind {@code body},
- * {@code headers}, {@code properties}, and {@code exchangeId}.
+ * {@code headers}, {@code properties}, {@code exchangeId}, {@code
variables} and {@code exception}. There is no
+ * current exchange, so the {@code camel} API is not usable from this
entry point.
*/
@Override
public <T> T evaluate(String script, Map<String, Object> bindings,
Class<T> resultType) {
script = loadResource(script);
+ Map<String, Object> jsonBindings =
QuickjsHelper.toJsonCompatibleBindings(bindings, null);
+ EngineState state = currentEngine();
+ state.stderr.reset();
+ Object result;
try {
- Object result = eval(script,
QuickjsHelper.toJsonCompatibleBindings(bindings, null));
- return convert(result, resultType, getCamelContext(), null);
+ result = state.engine.invokeGuestFunction(
+ QuickjsHelper.MODULE_NAME,
+ QuickjsHelper.FUNCTION_NAME,
+ List.of(jsonBindings, script),
+ QuickjsHelper.EVAL_WRAPPER);
} catch (Exception e) {
+ discard(state);
throw QuickjsHelper.wrapFailure(script, null, e);
+ } finally {
+ state.stderr.reset();
}
+ return convert(result, resultType, getCamelContext(), null);
}
Object evaluateExpression(String script, Exchange exchange) {
+ Exchange previous = currentExchange.get();
+ currentExchange.set(exchange);
try {
- Object result = eval(script,
QuickjsHelper.exchangeBindings(exchange));
+ Map<String, Object> bindings =
QuickjsHelper.exchangeBindings(exchange);
+ EngineState state = currentEngine();
+ Object result;
+ try {
+ byte[] compiled = state.compiled(script);
+ state.stderr.reset();
+ result = state.engine.invokePrecompiledGuestFunction(
+ QuickjsHelper.MODULE_NAME,
+ QuickjsHelper.FUNCTION_NAME,
+ List.of(bindings, ""),
+ compiled);
+ if (state.exhausted(engineMaxMemory, engineMaxEvaluations)) {
+ discard(state);
+ }
+ } catch (Exception e) {
+ // a script that throws leaves the QuickJS runtime in an
undefined state (its next compile
+ // panics with "RefCell already borrowed"), so this thread
gets a fresh engine on its next use
+ discard(state);
+ throw QuickjsHelper.wrapFailure(script, exchange, e);
Review Comment:
**Only a host-side trap poisons the runtime — a guest exception does not.**
I removed this `discard(state)` on your branch and re-probed: after `throw
new Error(...)`, after a `TypeError`, and after a statement-form `throw`, the
same engine happily re-executed a cached script *and* compiled a new one. Only
`camel.getHeader(null)` (host function throwing) and `call stack exhausted`
left it unusable.
Measured on this branch — failing evaluations of `body.nope.deeper`:
```
as written: fail 4,659 us (214 ops/s) 16x the
success path
discard only for non-GuestException: fail 357 us (2,794 ops/s) 1.3x
```
All 55 tests in the module pass with the narrower condition.
Note that narrowing it also means `exhausted(...)` is no longer reached when
the script throws — and a failed evaluation still grows the engine (~5.6 KB
each, measured on #26308). So the check needs to move into the `finally` at the
same time, which is the fix I suggested on #26308 as well:
```java
EngineState state = currentEngine();
Object result;
boolean discarded = false;
try {
byte[] compiled = state.compiled(script);
state.stderr.reset();
result = state.engine.invokePrecompiledGuestFunction(
QuickjsHelper.MODULE_NAME,
QuickjsHelper.FUNCTION_NAME,
List.of(bindings, ""),
compiled);
} catch (Exception e) {
// a guest exception leaves the QuickJS runtime usable; a
host-side trap does not
if (!(e instanceof GuestException)) {
discard(state);
discarded = true;
}
throw QuickjsHelper.wrapFailure(script, exchange, e);
} finally {
// Drop this evaluation's WASI stderr so a reused Engine
cannot accumulate it.
state.stderr.reset();
if (!discarded && state.exhausted(engineMaxMemory,
engineMaxEvaluations)) {
discard(state);
}
}
```
A test that drives a throwing script past `engineMaxEvaluations` would cover
both halves at once.
##########
components/camel-quickjs/src/main/java/org/apache/camel/language/quickjs/QuickjsHelper.java:
##########
@@ -96,13 +86,136 @@ export function camelEval(bindings, script) {
"protected", "public", "return", "static", "super", "switch",
"this", "throw", "true", "try", "typeof",
"var", "void", "while", "with", "yield");
+ /**
+ * The controlled Camel API. Each entry is one host function: its
JavaScript name, its parameter names (the arity is
+ * fixed so the JSON argument array always has every slot) and the Java
implementation, which receives the current
+ * {@link Exchange} and the JSON-decoded arguments.
+ */
+ private static final List<CamelFunction> CAMEL_API = List.of(
+ new CamelFunction(
+ "getBody", List.of(), Object.class,
+ (exchange, args) ->
toJsonCompatible(exchange.getMessage().getBody(), exchange, false)),
Review Comment:
`camel.getBody()` passes `failOnUnsupported = false`, so a `StreamCache` or
`InputStream` body silently comes back as `null`. The `body` binding on the
very same exchange goes through `exchangeBindings(...)` with `true` and raises
the explicit "Streaming type ... cannot be exposed to camel-quickjs without
consuming the message body".
Same exchange, two different answers for the same value — a script using
`camel.getBody()` on a streamed body just sees `null` and has no idea why.
Worth making the two paths agree, or calling it out in the `camel` API table in
the docs.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]