allthingssecurity opened a new pull request, #27793:
URL: https://github.com/apache/camel/pull/27793

   # Description
   
   [CAMEL-25606](https://issues.apache.org/jira/browse/CAMEL-25606)
   
   Part 2 of 4 of the camel-wasm redesign (part 1: #27678, merged): an optional 
runtime compiler for the `wasm` endpoint.
   
   Author: Shashank Mohan Jain (@allthingssecurity), working with Claude Code 
(Claude Opus 5.5).
   
   Scope: camel-wasm only. The Endive version alignment of camel-opa and 
camel-quickjs (both still resolve some Endive 1.0.0 artifacts through their 
libraries) is **not** in this PR. It will be proposed separately, once the 
container integration tests of camel-opa have run on Endive 1.1.0. camel-wasm 
itself already resolves Endive 1.1.0 for every Endive artifact, with and 
without this PR.
   
   ## What changes
   
   - **`compile` option** (boolean, default `false`) on the `wasm` endpoint. 
With `compile=true` the module is translated to JVM bytecode with the Endive 
runtime compiler (`MachineFactoryCompiler`) instead of being interpreted. 
`WasiCommand` compiles in its constructor and passes the machine factory to 
every per-exchange instance (`Instance.Builder.withMachineFactory`), so 
exchanges still run on separate instances and share only the generated classes.
   - **Loaded once per endpoint**: the endpoint loads the module, checks it and 
(with `compile=true`) compiles it when its first producer is initialized, and 
keeps that immutable `WasiCommand` for as long as the endpoint exists. Since 
#27678 every producer parsed the module itself, and a route start creates a new 
producer, so with a compiler the same module would be compiled at every route 
start. Consequences:
     - compilation happens when the route is first started, or at the first 
message with `lazyStartProducer=true` or a `toD`;
     - a route stop and start gets a new producer but the same command: the 
module is neither read nor compiled again (this is a change from #27678, where 
a restarted route read the module again; it is in the upgrade note);
     - the generated classes can only be unloaded when the endpoint is no 
longer reachable (route removed, context stopped), not when a route stops;
     - a dev-mode route reload removes all routes and clears the endpoint 
registry by default (`RouteWatcherReloadStrategy`), so it reads and compiles 
the module again;
     - with `toD`, every different URI is its own endpoint with its own 
compiled copy, so many different compiled URIs grow metaspace. The docs say so.
     - a failed load is not kept: the next producer that is initialized tries 
again.
   - **Functions too large for a JVM method**: Endive leaves such a function to 
the interpreter and compiles the rest. Its default (`InterpreterFallback.WARN`) 
reports that with `System.err.println`. `WasiCommand` instead compiles with 
`FAIL` first; if that fails it compiles again with `SILENT` and logs once per 
endpoint at WARN, naming the module. The module keeps working, nothing is 
written to `System.err`, and the second compilation only happens for a module 
that has such a function.
   - **A module the compiler cannot translate** (for example one with a vector 
instruction: Endive's parser accepts `v128.const`, its compiler does not) fails 
when the first producer is initialized, with an `IllegalStateException` that 
names the module by its program name (the file name only, as for argument zero 
in #27678) and has the Endive exception as cause.
   - **Missing compiler**: `WasiCommand` refers to the compiler through one 
nested class, so it loads and interprets without `run.endive:compiler` on the 
classpath; `compile=true` then fails with an `IllegalStateException` that names 
the artifact.
   - **Same behaviour with the compiler on**: result headers, exit codes, traps 
(`TrapException` as the cause, stderr in the message, the hint about a memory 
at its maximum), `maxMemoryPages`, `maxOutputSize`, environment, arguments, 
`SecureRandom` for `random_get`, `timeout`, and what #27678 gained in review 
(with a timeout the body is read into memory on the calling thread, a stream 
opened by the type converter is closed, a stream of the caller is not, the body 
after a failed run).
   - **Default off**: compiling costs start-up time and generates classes at 
run time, which a GraalVM native image cannot load.
   
   New public API: `WasmConfiguration.isCompile/setCompile`, 
`WasiCommand(WasmModule, List<String>, int, boolean)` and 
`WasiCommand.isCompiled()`. The three-argument `WasiCommand` constructor is 
kept. Package-private, for tests: `WasmProducer.command()`, 
`WasiCommand.usesInterpreterFallback()`.
   
   Not changed: the `wasm` language, instance handling (a new instance per 
exchange), all other options. No component-wide module cache and no instance 
pooling.
   
   ## Questions a reviewer may have
   
   - **Does `timeout` stop compiled code?** Yes. The Endive 1.1.0 compiler 
emits a call to `Shaded.checkInterruption` (which throws when the thread is 
interrupted) at calls, at indirect calls and at branches inside a function; I 
read this in the bytecode of the compiler. `timeoutInterruptsASpinningModule` 
(a module in an endless loop, stopped by `timeout=500`) passes with 
`compile=true`.
   - **Is the compiled code really used?** 
`compileOptionSelectsCompiledCodeOrTheInterpreter` asserts that the guest 
frames of a trap are only `run.endive.$gen.*` classes with `compile=true`, and 
only `InterpreterMachine` frames without.
   - **Is sharing the machine factory between threads safe?** The factory is a 
function from an `Instance` to a new `Machine`; every exchange builds its own 
instance and so gets its own machine. Evidence: `parallelExchanges` (8 threads 
x 50 exchanges on one endpoint) passes with `compile=true`. I did not review 
Endive's generated code for shared static state beyond that.
   - **Security model**: unchanged. A module is trusted route code; the option 
gives the guest no new capability (same streams, arguments and environment, no 
files, no sockets), relaxes no default and is not marked `insecure:*`. Endive's 
documentation recommends the interpreter for untrusted modules because the 
generated bytecode is not verified after compilation; in Camel the module is 
chosen by the route author, like a bean.
   - **Dependencies**: `run.endive:compiler` 1.1.0 (163 KB, Apache-2.0) as a 
normal dependency of camel-wasm, which brings ASM 9.10.1 (`asm`, `asm-commons`, 
`asm-tree`, `asm-util`, `asm-analysis`, 384 KB together, the version 
`parent/pom.xml` already manages). `dependency:tree` of camel-wasm on this 
commit: `runtime`, `wasm`, `wasi`, `log`, `compiler` all 1.1.0, `wabt` 1.1.0 in 
test scope. `camel-sbom` is not regenerated here.
   
   ## Docs and generated files
   
   - `wasm-component.adoc`, "Performance": describes `compile=true`, when the 
module is compiled and when it is not compiled again (see above), the `toD` 
note, when to keep the default (start-up time, native image), the fallback for 
functions that are too large, and a pointer to Endive's build-time compiler 
plugin with the statement that the endpoint does not use classes generated at 
build time. The section is qualitative: no numbers.
   - 4.24 upgrade guide: one paragraph inside the existing camel-wasm section 
(the `compile` option, the new `run.endive:compiler` and ASM dependency, the 
module loaded once per endpoint).
   - Regenerated: component metadata (`wasm.json`, configurers, URI factory) by 
the camel-wasm build; the endpoint DSL (`WasmEndpointBuilderFactory`) with the 
`generate-endpoint-dsl` mojo. The two catalog files (`components/wasm.json`, 
`docs/wasm-component.adoc`) are copies of the component files; I did not run 
`prepare-catalog` for them.
   
   ## Tests
   
   All on the commit of this PR, on top of `main` at d05541613fc7.
   
   camel-wasm with `verify` (formatter, import sort, generation): **132 tests, 
0 failures** (`WasmComponentTest` 58, `WasmCompiledComponentTest` 58, 
`WasmLanguageTest` 6, `WasiCommandTest` 7, `WasmFunctionTest` 3).
   
   - `WasmCompiledComponentTest` extends `WasmComponentTest` and runs every 
test of it with `compile=true`: body and headers, exit codes, traps with and 
without a timeout, environment and arguments, `random_get`, the spinning module 
stopped by `timeout`, the sleeping module ended by the interrupt, the tests 
#27678 added in review, the route restart with a timeout, `maxMemoryPages`, 
`maxOutputSize`, a fresh instance per exchange, 8 threads x 50 exchanges, and 
the start failures. The test modules are written in WAT and translated when the 
tests run; no binary is added.
   - `moduleIsLoadedOncePerEndpoint` (both classes): after the first exchange 
the module file is deleted; a second producer of the same endpoint starts, 
processes an exchange and has the same `WasiCommand` instance as the endpoint 
returned before.
   - `routeStopAndStartKeepsTheLoadedModule` (both classes): the route is 
stopped, the module file deleted, the route started; the next exchange works 
and the endpoint still returns the same `WasiCommand` instance.
   - `moduleIsLoadedAgainAfterAFailedLoad` (both classes): a producer of an 
endpoint whose module file does not exist fails to start; after the file is 
written, the next producer of the same endpoint starts and runs it.
   - `compileOptionSelectsCompiledCodeOrTheInterpreter` (both classes): see 
above.
   - `WasiCommandTest.functionTooLargeForAJvmMethodIsInterpreted`: a module 
whose `_start` has 70000 times `i32.const 1; drop` (210 KB, built in the test) 
is compiled with the fallback, runs and exits with 0, and nothing about it is 
printed to `System.err` during the compilation. The WARN line itself is not 
asserted (I saw it in the test log).
   - `WasiCommandTest.moduleThatCannotBeCompiledFailsWithItsName`: a module 
with a `v128.const` instruction fails with the message that starts with `The 
Wasm module simd.wasm cannot be compiled to JVM bytecode (compile=true): ` and 
a `WasmEngineException` as cause.
   - `WasiCommandTest.compiledCodeIsGeneratedOnceAndRunsEveryInstance`, 
`interpretedByDefault`, `compileWithoutTheCompilerOnTheClasspathFails`: as 
before (same generated classes for two runs; no generated class when 
interpreted; with a class loader that hides `run.endive.compiler.*` the 
interpreter works and `compile=true` fails with the message that names 
`run.endive:compiler`).
   
   Sensitivity, on this commit: with the endpoint not keeping the command, 
Endive's default compile call instead of the explicit fallback handling, and 
the wrapping of compile failures removed, 6 tests fail: 
`moduleIsLoadedOncePerEndpoint` and `routeStopAndStartKeepsTheLoadedModule` in 
both classes (`FileNotFoundException`), and the two new `WasiCommandTest` 
tests. `moduleIsLoadedAgainAfterAFailedLoad` pins the existing behaviour and 
does not fail under these changes.
   
   Not run:
   
   - A GraalVM native image (neither that `compile=true` fails there nor that 
the interpreter works there).
   - The full build from the root, and `prepare-catalog`.
   - A dev-mode route reload with a compiled module (the statement above comes 
from reading `RouteWatcherReloadStrategy`), and a measurement of metaspace with 
`toD`.
   - camel-opa and camel-quickjs: not touched by this PR, so not run.
   
   ## Performance
   
   Not measured on this commit. The numbers below were measured through a route 
(`ProducerTemplate.requestBody` -> `direct:` -> `wasm:`), one thread, median 
after a warm-up, two rounds in one JVM, Apple M4 Pro, JDK 21, Endive 1.1.0, 
with a probe that is not part of the PR, **on an earlier base** (an earlier 
head of #27678, before its final review changes and before the changes of this 
PR to the fallback handling). Indicative only; that is why the component page 
has no numbers.
   
   | Module, body | Interpreted | `compile=true` | First route start, 
interpreted / compiled |
   |---|---|---|---|
   | WAT upper-case module (538 B), 1 KiB (n=2000 after 300) | 225-275 us | 
32-35 us | 3-45 ms / 5-50 ms |
   | same, 1 MiB (n=30 after 5) | 210-269 ms | 1.4 ms in round 2, 17 ms in 
round 1 (before the JIT compiled the loop) | 3-4 ms / 8 ms |
   | Rust `no_std` WASI command (3.9 KB), 1 KiB (n=2000 after 300) | 230-237 us 
| 32-45 us | 3-10 ms / 7-12 ms |
   | standard Go 1.26 `GOOS=wasip1` program (2.3 MB), 1 KiB (n=50 after 5 / 
n=200 after 20) | 30-31 ms | 3.9-4.1 ms | 0.2-0.3 s / 0.6-1.1 s |
   
   The first round of each row includes class loading of Camel and Endive in 
the start time. None of these modules has a function that is too large for a 
JVM method, so none was compiled twice.
   
   ## Questions
   
   1. `run.endive:compiler` as a normal dependency (this PR) or optional? 
Normal adds about 0.55 MB of jars (compiler + ASM) for every camel-wasm user; 
optional would need `--dep` with `camel run`.
   2. Build-time compiled modules: Endive's Maven plugin generates a class with 
a machine factory, which is the native-image friendly way. The endpoint has no 
option to use such a class, so the docs only point to the plugin and say so. 
Should a later PR add a hook (for example a `#bean` reference to a 
`run.endive.runtime.CompiledModule`)?
   3. `InterpreterFallback`: this PR keeps a module with a function that is too 
large working (compile with `FAIL`, then with `SILENT`, and a WARN in the log), 
at the price of a second compilation for such modules. The alternatives are 
`SILENT` only (one compilation, but no way to tell the user through the public 
API of Endive) or `FAIL` (the route does not start). Which do you prefer?
   4. Module loading moved from the producer to the endpoint, and the command 
is kept for as long as the endpoint exists: a module file that changes is only 
read again by a new endpoint (in #27678: at the next route start). Is that the 
lifecycle you want, or should the endpoint drop the command when it stops, 
which would compile again at every restart of the context but not of a route?
   
   ## Plan
   
   1. #27678 (merged): WASI runner, endpoint and limits replacing the custom 
ABI, docs and upgrade note.
   2. **This PR**: optional runtime compiler (`compile=true`, off by default 
because of native images), docs pointer to the Endive build-time compiler 
plugin.
   3. The `wasm` language: rebase it on `WasiCommand`, or drop it.
   4. Rust and Go examples in camel-examples.
   
   Separately: one Endive version for camel-opa and camel-quickjs.
   
   @davsclaus please review.
   
   # Target
   
   - [x] I checked that the commit is targeting the correct branch (Camel 4 
uses the `main` branch)
   
   # Tracking
   - [x] If this is a large change, bug fix, or code improvement, I checked 
there is a [JIRA issue](https://issues.apache.org/jira/browse/CAMEL) filed for 
the change (usually before you start working on it).
   
   # Apache Camel coding standards and style
   
   - [x] I checked that each commit in the pull request has a meaningful 
subject line and body.
   - [ ] I have run `mvn clean install -DskipTests` locally from root folder 
and I have committed all auto-generated changes.
     (I built and tested camel-wasm with `verify`, including the formatter and 
import-sort plugins, and regenerated the endpoint DSL with the 
camel-package-maven-plugin mojo. I did not run the full root build.)
   
   # AI-assisted contributions
   
   - [x] If this PR includes AI-generated code, commits have proper 
co-authorship attribution (e.g., `Co-authored-by` trailers) and the PR 
description identifies the AI tool used.
     Prepared by Shashank Mohan Jain (@allthingssecurity) with Claude Code 
(Claude Opus 5.5). The commit carries a `Co-Authored-By` trailer.
   
   _Claude Code on behalf of allthingssecurity_
   
   🤖 Generated with [Claude Code](https://claude.com/claude-code)
   


-- 
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]

Reply via email to