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]
