From 311c8f532229385c99703ca8a170d1d5a9c9f0b4 Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Mon, 2 Jun 2025 12:24:33 -0400 Subject: [PATCH] breaking: Move the builder from the Plugin to the PluginFactory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Plugins are now only created/started when they are first grabbed form the pool • Update test examples to use new PluginFactory.builder() API instead of Plugin.builder() • Switch from experimental AotMachine to MachineFactoryCompiler for bytecode compilation Signed-off-by: Hiram Chirino --- README.md | 2 +- .../java/io/roastedroot/proxywasm/Plugin.java | 344 +-------------- .../roastedroot/proxywasm/PluginFactory.java | 391 +++++++++++++++++- .../proxywasm/internal/Plugin.java | 7 - .../roastedroot/proxywasm/internal/Pool.java | 48 +-- .../internal/AbstractProxyWasmFeature.java | 15 +- .../proxywasm/jaxrs/example/App.java | 99 +++-- 7 files changed, 466 insertions(+), 440 deletions(-) diff --git a/README.md b/README.md index d584dcf..96d3f3e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The Java host implementation for Proxy-Wasm, enabling developers to run Proxy-Wa TODO: still need to document how to use in non-cdi containers. -If your using Quarkus, see the [Quarkus Proxy-Wasm Extension](https://github.com/roastedroot/quarkus-proxy-wasm) docs. +If your using Quarkus, see the [Quarkus Proxy-Wasm Extension](https://docs.quarkiverse.io/quarkus-proxy-wasm/dev/index.html) docs. ## Building diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Plugin.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Plugin.java index 73a5dc6..a102096 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Plugin.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Plugin.java @@ -1,23 +1,10 @@ package io.roastedroot.proxywasm; -import static io.roastedroot.proxywasm.internal.Helpers.bytes; - -import com.dylibso.chicory.runtime.ImportMemory; -import com.dylibso.chicory.runtime.Instance; -import com.dylibso.chicory.runtime.Machine; -import com.dylibso.chicory.wasi.WasiOptions; -import com.dylibso.chicory.wasm.WasmModule; -import io.roastedroot.proxywasm.internal.ProxyWasm; -import java.net.URI; -import java.util.HashMap; -import java.util.Map; -import java.util.function.Function; - /** * Represents a Proxy-WASM plugin, providing the bridge between the host * environment and the WASM module. * - *

Concrete Plugin instances are created using the {@link Builder}. + *

Concrete Plugin instances are created using a {@link PluginFactory}. * The actual WASM instance and interaction logic are managed internally. */ public interface Plugin { @@ -28,333 +15,4 @@ public interface Plugin { * @return the plugin name, which might be null if not explicitly set via the builder. */ String name(); - - /** - * Creates a new {@link Builder} to configure and construct a {@link Plugin} instance - * from the given WASM module. - * - * @param module the compiled {@link WasmModule} representing the plugin's code. - * @return a new {@link Plugin.Builder} instance. - */ - static Plugin.Builder builder(WasmModule module) { - return new Plugin.Builder(module); - } - - /** - * Builder for creating a Plugin instance. - */ - final class Builder { - - private final WasmModule module; - private final ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withStart(false); - private boolean shared; - private String name; - private HashMap foreignFunctions; - private HashMap upstreams; - private boolean strictUpstreams; - private int minTickPeriodMilliseconds; - private LogHandler logger; - private byte[] vmConfig; - private byte[] pluginConfig; - private MetricsHandler metricsHandler; - private SharedQueueHandler sharedQueueHandler; - private SharedDataHandler sharedDataHandler; - - /** - * Private constructor for the Builder. - * Initializes the builder with the essential WASM module. - * - * @param module The compiled {@link WasmModule} containing the plugin code. - */ - private Builder(WasmModule module) { - this.module = module; - } - - /** - * Sets the optional name for this plugin instance. - * This name can be used for identification and logging purposes. - * - * @param name the desired name for the plugin. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withName(String name) { - this.name = name; - return this; - } - - /** - * Registers foreign (host-provided) functions that can be called by the WASM plugin. - * These functions allow the plugin to interact with the host environment beyond the standard - * Proxy-WASM ABI calls. - * - * @param functions A map where keys are the function names expected by the WASM module, - * and values are {@link ForeignFunction} implementations provided by the host. - * @return this {@code Builder} instance for method chaining. - * @see ForeignFunction - */ - public Builder withForeignFunctions(Map functions) { - this.foreignFunctions = new HashMap<>(functions); - return this; - } - - /** - * Defines mappings from logical upstream names (used within the plugin) to actual network URIs. - * This allows the plugin to make network calls (e.g., HTTP, gRPC) to services known by name, - * without needing to hardcode addresses. - * - * @param upstreams A map where keys are the logical upstream names used by the plugin, - * and values are the corresponding {@link URI}s of the target services. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withUpstreams(Map upstreams) { - this.upstreams = new HashMap<>(upstreams); - return this; - } - - /** - * Configures the behavior when a plugin attempts to call an upstream that is not defined - * in the `upstreams` map provided via {@link #withUpstreams(Map)}. - * - *

If {@code strictUpstreams} is {@code true}, attempting to use an undefined upstream name - * will result in an error being reported back to the plugin. - * - *

If {@code strictUpstreams} is {@code false} (the default behavior if this method is not called), - * the host will try to parse the upstream name as URI. - * - * @param strictUpstreams {@code true} to enforce that all used upstream names must be explicitly mapped, - * {@code false} to allow fallback resolution. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withStrictUpstreams(boolean strictUpstreams) { - this.strictUpstreams = strictUpstreams; - return this; - } - - /** - * Sets a minimum interval for the plugin's periodic timer ticks ({@code proxy_on_tick}). - * The Proxy-WASM ABI allows plugins to request a timer tick period. This setting enforces - * a lower bound on that period to prevent plugins from requesting excessively frequent ticks, - * which could overload the host. - * - *

If the plugin requests a tick period shorter than this minimum, the host will use - * this minimum value instead. - * - * @param minTickPeriodMilliseconds the minimum allowed tick period in milliseconds. A value of 0 or less - * implies no minimum enforcement (host default behavior). - * @return this {@code Builder} instance for method chaining. - */ - public Builder withMinTickPeriodMilliseconds(int minTickPeriodMilliseconds) { - this.minTickPeriodMilliseconds = minTickPeriodMilliseconds; - return this; - } - - /** - * Provides a {@link LogHandler} implementation for the plugin to use. - * This handler receives log messages generated by the WASM module via the {@code proxy_log} ABI call. - * If no logger is provided, {@link LogHandler#DEFAULT} (a no-op logger) is used. - * - * @param logger the {@link LogHandler} implementation to handle plugin logs. - * @return this {@code Builder} instance for method chaining. - * @see LogHandler - */ - public Builder withLogger(LogHandler logger) { - this.logger = logger; - return this; - } - - /** - * Provides a {@link MetricsHandler} implementation for the plugin to use. - * This handler manages metric definition, recording, and retrieval requested by the WASM module - * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_define_metric}). - * If no handler is provided, {@link MetricsHandler#DEFAULT} (which returns UNIMPLEMENTED) - * might be used implicitly. - * - * @param metricsHandler the {@link MetricsHandler} implementation to manage plugin metrics. - * @return this {@code Builder} instance for method chaining. - * @see MetricsHandler - */ - public Builder withMetricsHandler(MetricsHandler metricsHandler) { - this.metricsHandler = metricsHandler; - return this; - } - - /** - * Provides a {@link SharedQueueHandler} implementation for the plugin to use. - * This handler manages operations on shared message queues requested by the WASM module - * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_register_shared_queue}). - * If no handler is provided, {@link SharedQueueHandler#DEFAULT} (which returns UNIMPLEMENTED) - * might be used implicitly. - * - * @param sharedQueueHandler the {@link SharedQueueHandler} implementation to manage shared queues. - * @return this {@code Builder} instance for method chaining. - * @see SharedQueueHandler - */ - public Builder withSharedQueueHandler(SharedQueueHandler sharedQueueHandler) { - this.sharedQueueHandler = sharedQueueHandler; - return this; - } - - /** - * Provides a {@link SharedDataHandler} implementation for the plugin to use. - * This handler manages operations on shared key-value data requested by the WASM module - * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_get_shared_data}). - * If no handler is provided, {@link SharedDataHandler#DEFAULT} (which returns UNIMPLEMENTED) - * might be used implicitly. - * - * @param sharedDataHandler the {@link SharedDataHandler} implementation to manage shared data. - * @return this {@code Builder} instance for method chaining. - * @see SharedDataHandler - */ - public Builder withSharedDataHandler(SharedDataHandler sharedDataHandler) { - this.sharedDataHandler = sharedDataHandler; - return this; - } - - /** - * Configures whether the plugin instance should be shared across multiple host requests or contexts. - * - *

If {@code shared} is {@code true}, a single WASM instance will be created and reused. - * across multiple concurrent requests. Since Proxy-Wasm plugins are not thread-safe, the requests will - * contend on an access lock for the plugin. Using a shared plugin allows the plugin to maintain state - * between the requests. It will use less memory but will have a performance impact due to the contention. - * - *

If {@code shared} is {@code false} (the default), the host will create a new, separate WASM instance for each - * request or context (depending on the host implementation and threading model). This provides better - * isolation, eliminates contention, but consumes more memory. - * - * @param shared {@code true} to indicate the plugin instance can be shared, {@code false} otherwise. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withShared(boolean shared) { - this.shared = shared; - return this; - } - - /** - * Sets the Virtual Machine (VM) configuration data for the plugin. - * This configuration is typically provided once when the VM (and the plugin) is initialized. - * It's accessible to the plugin via the {@code proxy_get_vm_configuration} ABI call. - * - * @param vmConfig A byte array containing the VM configuration data. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withVmConfig(byte[] vmConfig) { - this.vmConfig = vmConfig; - return this; - } - - /** - * Sets the Virtual Machine (VM) configuration data for the plugin using a String. - * The string will be converted to bytes using the platform's default charset. - * This configuration is accessible via the {@code proxy_get_vm_configuration} ABI call. - * - * @param vmConfig A String containing the VM configuration data. - * @return this {@code Builder} instance for method chaining. - * @see #withVmConfig(byte[]) - */ - public Builder withVmConfig(String vmConfig) { - this.vmConfig = bytes(vmConfig); - return this; - } - - /** - * Sets the specific configuration data for this plugin instance. - * This configuration is provided during the plugin's initialization phase - * (via {@code proxy_on_configure}) and allows tailoring the plugin's behavior. - * It's accessible to the plugin via the {@code proxy_get_plugin_configuration} ABI call. - * - * @param pluginConfig A byte array containing the plugin-specific configuration data. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withPluginConfig(byte[] pluginConfig) { - this.pluginConfig = pluginConfig; - return this; - } - - /** - * Sets the specific configuration data for this plugin instance using a String. - * The string will be converted to bytes using the platform's default charset. - * This configuration is accessible via the {@code proxy_get_plugin_configuration} ABI call. - * - * @param pluginConfig A String containing the plugin-specific configuration data. - * @return this {@code Builder} instance for method chaining. - * @see #withPluginConfig(byte[]) - */ - public Builder withPluginConfig(String pluginConfig) { - this.pluginConfig = bytes(pluginConfig); - return this; - } - - /** - * Provides an explicit memory instance to be used by the WASM module. - * - * @param memory The {@link ImportMemory} instance to be used by the WASM module. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withImportMemory(ImportMemory memory) { - proxyWasmBuilder.withImportMemory(memory); - return this; - } - - /** - * Configures a custom factory for creating the {@link Machine} used to execute the WASM code. - * The {@link Machine} controls the low-level execution of WASM instructions. - * By default, an interpreter-based machine is used. - * Providing a custom factory allows using alternative execution strategies, such as - * wasm to bytecode compilation to improve execution performance. - * - *

See the Chicory documentation (https://chicory.dev/docs/usage/runtime-compiler) for more details - * on WASM to bytecode compilation and execution. - * - * @param machineFactory A function that takes a WASM {@link Instance} and returns a {@link Machine}. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withMachineFactory(Function machineFactory) { - proxyWasmBuilder.withMachineFactory(machineFactory); - return this; - } - - /** - * Configures WebAssembly System Interface (WASI) options for the plugin instance. - * WASI provides a standard interface for WASM modules to interact with the underlying operating system - * for tasks like file system access, environment variables, etc. While Proxy-WASM defines its own ABI, - * some modules might also utilize WASI features. - * - * @param options The {@link WasiOptions} to configure for the WASI environment. - * @return this {@code Builder} instance for method chaining. - */ - public Builder withWasiOptions(WasiOptions options) { - proxyWasmBuilder.withWasiOptions(options); - return this; - } - - /** - * Constructs and initializes the {@link Plugin} instance based on the configuration - * provided to this builder. - * - *

This involves setting up the WASM environment, linking host functions, applying configurations, - * and calling the necessary Proxy-WASM lifecycle functions (like {@code _start} and - * {@code proxy_on_vm_start}). - * - * @return The fully configured and initialized {@link Plugin} instance. - * @throws StartException If any error occurs during the plugin initialization process - * (e.g., WASM instantiation failure, error during {@code proxy_on_vm_start}). - */ - public Plugin build() throws StartException { - return new io.roastedroot.proxywasm.internal.Plugin( - proxyWasmBuilder.build(module), - shared, - name, - foreignFunctions, - upstreams, - strictUpstreams, - minTickPeriodMilliseconds, - logger, - vmConfig, - pluginConfig, - metricsHandler, - sharedQueueHandler, - sharedDataHandler); - } - } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java index 879b131..06ecfd3 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/PluginFactory.java @@ -1,5 +1,18 @@ package io.roastedroot.proxywasm; +import static io.roastedroot.proxywasm.internal.Helpers.bytes; + +import com.dylibso.chicory.runtime.ImportMemory; +import com.dylibso.chicory.runtime.Instance; +import com.dylibso.chicory.runtime.Machine; +import com.dylibso.chicory.wasi.WasiOptions; +import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.internal.ProxyWasm; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + /** * A functional interface representing a factory for creating {@link Plugin} instances. * @@ -8,8 +21,8 @@ * Implementations might handle loading WASM modules, configuring builders, and returning * the ready-to-use plugin. */ -@FunctionalInterface public interface PluginFactory { + /** * Creates and returns a new {@link Plugin} instance. * Implementations are responsible for all necessary setup, including potentially @@ -20,4 +33,380 @@ public interface PluginFactory { * initialization errors within the plugin's start lifecycle). */ Plugin create() throws Exception; + + /** + * Returns the configured name of the plugin. + * + * @return the plugin name. + */ + String name(); + + /** + * Indicates whether this plugin instance is shared across multiple contexts or requests. + * + *

If {@code true}, the plugin will be instantiated once and reused, allowing it to maintain state + * between requests but potentially introducing contention. If {@code false}, a new instance will be created + * for each request or context, providing better isolation but consuming more memory. + * + * @return {@code true} if the plugin instance is shared, {@code false} otherwise. + */ + boolean shared(); + + /** + * Creates a new {@link Builder} to configure and construct a {@link PluginFactory} instance + * from the given WASM module. + * + * @param module the compiled {@link WasmModule} representing the plugin's code. + * @return a new {@link PluginFactory.Builder} instance. + */ + static PluginFactory.Builder builder(WasmModule module) { + return new PluginFactory.Builder(module); + } + + /** + * Builder for creating a PluginFactory instance that can create Plugin instances + * with pre-configured settings. + */ + final class Builder { + + private final WasmModule module; + private final ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withStart(false); + private String name; + private HashMap foreignFunctions; + private HashMap upstreams; + private boolean strictUpstreams; + private int minTickPeriodMilliseconds; + private LogHandler logger; + private byte[] vmConfig; + private byte[] pluginConfig; + private MetricsHandler metricsHandler; + private SharedQueueHandler sharedQueueHandler; + private SharedDataHandler sharedDataHandler; + private boolean shared; + + /** + * Private constructor for the Builder. + * Initializes the builder with the essential WASM module. + * + * @param module The compiled {@link WasmModule} containing the plugin code. + */ + private Builder(WasmModule module) { + this.module = module; + } + + /** + * Sets the optional name for this plugin instance. + * This name can be used for identification and logging purposes. + * + * @param name the desired name for the plugin. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withName(String name) { + this.name = name; + return this; + } + + /** + * Registers foreign (host-provided) functions that can be called by the WASM plugin. + * These functions allow the plugin to interact with the host environment beyond the standard + * Proxy-WASM ABI calls. + * + * @param functions A map where keys are the function names expected by the WASM module, + * and values are {@link ForeignFunction} implementations provided by the host. + * @return this {@code Builder} instance for method chaining. + * @see ForeignFunction + */ + public PluginFactory.Builder withForeignFunctions(Map functions) { + this.foreignFunctions = new HashMap<>(functions); + return this; + } + + /** + * Defines mappings from logical upstream names (used within the plugin) to actual network URIs. + * This allows the plugin to make network calls (e.g., HTTP, gRPC) to services known by name, + * without needing to hardcode addresses. + * + * @param upstreams A map where keys are the logical upstream names used by the plugin, + * and values are the corresponding {@link URI}s of the target services. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withUpstreams(Map upstreams) { + this.upstreams = new HashMap<>(upstreams); + return this; + } + + /** + * Configures the behavior when a plugin attempts to call an upstream that is not defined + * in the `upstreams` map provided via {@link #withUpstreams(Map)}. + * + *

If {@code strictUpstreams} is {@code true}, attempting to use an undefined upstream name + * will result in an error being reported back to the plugin. + * + *

If {@code strictUpstreams} is {@code false} (the default behavior if this method is not called), + * the host will try to parse the upstream name as URI. + * + * @param strictUpstreams {@code true} to enforce that all used upstream names must be explicitly mapped, + * {@code false} to allow fallback resolution. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withStrictUpstreams(boolean strictUpstreams) { + this.strictUpstreams = strictUpstreams; + return this; + } + + /** + * Sets a minimum interval for the plugin's periodic timer ticks ({@code proxy_on_tick}). + * The Proxy-WASM ABI allows plugins to request a timer tick period. This setting enforces + * a lower bound on that period to prevent plugins from requesting excessively frequent ticks, + * which could overload the host. + * + *

If the plugin requests a tick period shorter than this minimum, the host will use + * this minimum value instead. + * + * @param minTickPeriodMilliseconds the minimum allowed tick period in milliseconds. A value of 0 or less + * implies no minimum enforcement (host default behavior). + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withMinTickPeriodMilliseconds(int minTickPeriodMilliseconds) { + this.minTickPeriodMilliseconds = minTickPeriodMilliseconds; + return this; + } + + /** + * Provides a {@link LogHandler} implementation for the plugin to use. + * This handler receives log messages generated by the WASM module via the {@code proxy_log} ABI call. + * If no logger is provided, {@link LogHandler#DEFAULT} (a no-op logger) is used. + * + * @param logger the {@link LogHandler} implementation to handle plugin logs. + * @return this {@code Builder} instance for method chaining. + * @see LogHandler + */ + public PluginFactory.Builder withLogger(LogHandler logger) { + this.logger = logger; + return this; + } + + /** + * Provides a {@link MetricsHandler} implementation for the plugin to use. + * This handler manages metric definition, recording, and retrieval requested by the WASM module + * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_define_metric}). + * If no handler is provided, {@link MetricsHandler#DEFAULT} (which returns UNIMPLEMENTED) + * might be used implicitly. + * + * @param metricsHandler the {@link MetricsHandler} implementation to manage plugin metrics. + * @return this {@code Builder} instance for method chaining. + * @see MetricsHandler + */ + public PluginFactory.Builder withMetricsHandler(MetricsHandler metricsHandler) { + this.metricsHandler = metricsHandler; + return this; + } + + /** + * Provides a {@link SharedQueueHandler} implementation for the plugin to use. + * This handler manages operations on shared message queues requested by the WASM module + * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_register_shared_queue}). + * If no handler is provided, {@link SharedQueueHandler#DEFAULT} (which returns UNIMPLEMENTED) + * might be used implicitly. + * + * @param sharedQueueHandler the {@link SharedQueueHandler} implementation to manage shared queues. + * @return this {@code Builder} instance for method chaining. + * @see SharedQueueHandler + */ + public PluginFactory.Builder withSharedQueueHandler(SharedQueueHandler sharedQueueHandler) { + this.sharedQueueHandler = sharedQueueHandler; + return this; + } + + /** + * Provides a {@link SharedDataHandler} implementation for the plugin to use. + * This handler manages operations on shared key-value data requested by the WASM module + * via the relevant {@code proxy_*} ABI calls (e.g., {@code proxy_get_shared_data}). + * If no handler is provided, {@link SharedDataHandler#DEFAULT} (which returns UNIMPLEMENTED) + * might be used implicitly. + * + * @param sharedDataHandler the {@link SharedDataHandler} implementation to manage shared data. + * @return this {@code Builder} instance for method chaining. + * @see SharedDataHandler + */ + public PluginFactory.Builder withSharedDataHandler(SharedDataHandler sharedDataHandler) { + this.sharedDataHandler = sharedDataHandler; + return this; + } + + /** + * Sets the Virtual Machine (VM) configuration data for the plugin. + * This configuration is typically provided once when the VM (and the plugin) is initialized. + * It's accessible to the plugin via the {@code proxy_get_vm_configuration} ABI call. + * + * @param vmConfig A byte array containing the VM configuration data. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withVmConfig(byte[] vmConfig) { + this.vmConfig = vmConfig; + return this; + } + + /** + * Sets the Virtual Machine (VM) configuration data for the plugin using a String. + * The string will be converted to bytes using the platform's default charset. + * This configuration is accessible via the {@code proxy_get_vm_configuration} ABI call. + * + * @param vmConfig A String containing the VM configuration data. + * @return this {@code Builder} instance for method chaining. + * @see #withVmConfig(byte[]) + */ + public PluginFactory.Builder withVmConfig(String vmConfig) { + this.vmConfig = bytes(vmConfig); + return this; + } + + /** + * Sets the specific configuration data for this plugin instance. + * This configuration is provided during the plugin's initialization phase + * (via {@code proxy_on_configure}) and allows tailoring the plugin's behavior. + * It's accessible to the plugin via the {@code proxy_get_plugin_configuration} ABI call. + * + * @param pluginConfig A byte array containing the plugin-specific configuration data. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withPluginConfig(byte[] pluginConfig) { + this.pluginConfig = pluginConfig; + return this; + } + + /** + * Sets the specific configuration data for this plugin instance using a String. + * The string will be converted to bytes using the platform's default charset. + * This configuration is accessible via the {@code proxy_get_plugin_configuration} ABI call. + * + * @param pluginConfig A String containing the plugin-specific configuration data. + * @return this {@code Builder} instance for method chaining. + * @see #withPluginConfig(byte[]) + */ + public PluginFactory.Builder withPluginConfig(String pluginConfig) { + this.pluginConfig = bytes(pluginConfig); + return this; + } + + /** + * Provides an explicit memory instance to be used by the WASM module. + * + * @param memory The {@link ImportMemory} instance to be used by the WASM module. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withImportMemory(ImportMemory memory) { + proxyWasmBuilder.withImportMemory(memory); + return this; + } + + /** + * Configures a custom factory for creating the {@link Machine} used to execute the WASM code. + * The {@link Machine} controls the low-level execution of WASM instructions. + * By default, an interpreter-based machine is used. + * Providing a custom factory allows using alternative execution strategies, such as + * wasm to bytecode compilation to improve execution performance. + * + *

See the Chicory documentation (https://chicory.dev/docs/usage/runtime-compiler) for more details + * on WASM to bytecode compilation and execution. + * + * @param machineFactory A function that takes a WASM {@link Instance} and returns a {@link Machine}. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withMachineFactory( + Function machineFactory) { + proxyWasmBuilder.withMachineFactory(machineFactory); + return this; + } + + /** + * Configures WebAssembly System Interface (WASI) options for the plugin instance. + * WASI provides a standard interface for WASM modules to interact with the underlying operating system + * for tasks like file system access, environment variables, etc. While Proxy-WASM defines its own ABI, + * some modules might also utilize WASI features. + * + * @param options The {@link WasiOptions} to configure for the WASI environment. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withWasiOptions(WasiOptions options) { + proxyWasmBuilder.withWasiOptions(options); + return this; + } + + /** + * Configures whether the plugin instance should be shared across multiple host requests or contexts. + * + *

If {@code shared} is {@code true}, a single WASM instance will be created and reused. + * across multiple concurrent requests. Since Proxy-Wasm plugins are not thread-safe, the requests will + * contend on an access lock for the plugin. Using a shared plugin allows the plugin to maintain state + * between the requests. It will use less memory but will have a performance impact due to the contention. + * + *

If {@code shared} is {@code false} (the default), the host will create a new, separate WASM instance for each + * request or context (depending on the host implementation and threading model). This provides better + * isolation, eliminates contention, but consumes more memory. + * + * @param shared {@code true} to indicate the plugin instance can be shared, {@code false} otherwise. + * @return this {@code Builder} instance for method chaining. + */ + public PluginFactory.Builder withShared(boolean shared) { + this.shared = shared; + return this; + } + + /** + * Constructs a {@link PluginFactory} instance that will create {@link Plugin} instances + * using the configuration provided to this builder. + * + * @return A {@link PluginFactory} that creates plugins with the specified configuration. + */ + public PluginFactory build() { + + // Create deep copies of builder fields for immutability in multi-threaded environments. + String name = this.name; + HashMap foreignFunctions = + this.foreignFunctions != null ? new HashMap<>(this.foreignFunctions) : null; + HashMap upstreams = + this.upstreams != null ? new HashMap<>(this.upstreams) : null; + boolean strictUpstreams = this.strictUpstreams; + int minTickPeriodMilliseconds = this.minTickPeriodMilliseconds; + LogHandler logger = this.logger; + byte[] vmConfig = this.vmConfig != null ? this.vmConfig.clone() : null; + byte[] pluginConfig = this.pluginConfig != null ? this.pluginConfig.clone() : null; + MetricsHandler metricsHandler = this.metricsHandler; + SharedQueueHandler sharedQueueHandler = this.sharedQueueHandler; + SharedDataHandler sharedDataHandler = this.sharedDataHandler; + boolean shared = this.shared; + + return new PluginFactory() { + + @Override + public String name() { + return name; + } + + @Override + public boolean shared() { + return shared; + } + + @Override + public Plugin create() throws Exception { + + return new io.roastedroot.proxywasm.internal.Plugin( + proxyWasmBuilder.build(module), + name, + foreignFunctions, + upstreams, + strictUpstreams, + minTickPeriodMilliseconds, + logger, + vmConfig, + pluginConfig, + metricsHandler, + sharedQueueHandler, + sharedDataHandler); + } + }; + } + } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Plugin.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Plugin.java index 697514a..f693d60 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Plugin.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Plugin.java @@ -32,7 +32,6 @@ public final class Plugin implements io.roastedroot.proxywasm.Plugin { private final ReentrantLock lock = new ReentrantLock(); final ProxyWasm wasm; ServerAdaptor serverAdaptor; - private final boolean shared; private final String name; private final MetricsHandler metricsHandler; @@ -41,7 +40,6 @@ public final class Plugin implements io.roastedroot.proxywasm.Plugin { public Plugin( ProxyWasm proxyWasm, - boolean shared, String name, HashMap foreignFunctions, HashMap upstreams, @@ -56,7 +54,6 @@ public Plugin( throws StartException { Objects.requireNonNull(proxyWasm); this.name = Objects.requireNonNullElse(name, "default"); - this.shared = shared; this.foreignFunctions = Objects.requireNonNullElseGet(foreignFunctions, HashMap::new); this.upstreams = Objects.requireNonNullElseGet(upstreams, HashMap::new); this.strictUpstreams = strictUpstreams; @@ -89,10 +86,6 @@ public void unlock() { lock.unlock(); } - public boolean isShared() { - return shared; - } - public ServerAdaptor getServerAdaptor() { return serverAdaptor; } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Pool.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Pool.java index 8e8a3fd..6ede561 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Pool.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/internal/Pool.java @@ -2,8 +2,6 @@ import io.roastedroot.proxywasm.PluginFactory; import io.roastedroot.proxywasm.StartException; -import java.util.Collection; -import java.util.List; public interface Pool { @@ -16,56 +14,58 @@ public interface Pool { default void close() {} class SharedPlugin implements Pool { - private final Plugin plugin; - public SharedPlugin(ServerAdaptor serverAdaptor, Plugin plugin) throws StartException { - this.plugin = plugin; - this.plugin.setServerAdaptor(serverAdaptor); - } + private final ServerAdaptor serverAdaptor; + private final PluginFactory factory; + private Plugin plugin; - public void close() { - plugin.wasm.close(); + public SharedPlugin(ServerAdaptor serverAdaptor, PluginFactory factory) { + this.serverAdaptor = serverAdaptor; + this.factory = factory; } - public Collection getPluginPools() { - return List.of(plugin); + @Override + public String name() { + return this.factory.name(); } @Override - public String name() { - return plugin.name(); + public Plugin borrow() throws StartException { + if (plugin != null) { + return plugin; + } + try { + plugin = (Plugin) factory.create(); + } catch (Throwable e) { + throw new StartException("Plugin create failed.", e); + } + plugin.setServerAdaptor(serverAdaptor); + plugin.wasm.start(); + return plugin; } + // Return the plugin to the pool @Override public void release(Plugin plugin) { if (plugin != this.plugin) { throw new IllegalArgumentException("Plugin not from this pool"); } } - - @Override - public Plugin borrow() throws StartException { - plugin.wasm.start(); - return plugin; - } } class PluginPerRequest implements Pool { private final ServerAdaptor serverAdaptor; final PluginFactory factory; - private final String name; - public PluginPerRequest(ServerAdaptor serverAdaptor, PluginFactory factory, Plugin plugin) { + public PluginPerRequest(ServerAdaptor serverAdaptor, PluginFactory factory) { this.serverAdaptor = serverAdaptor; this.factory = factory; - this.name = plugin.name(); - release(plugin); } @Override public String name() { - return this.name; + return this.factory.name(); } @Override diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/internal/AbstractProxyWasmFeature.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/internal/AbstractProxyWasmFeature.java index 8fbbbac..9823444 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/internal/AbstractProxyWasmFeature.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/internal/AbstractProxyWasmFeature.java @@ -2,7 +2,6 @@ import io.roastedroot.proxywasm.PluginFactory; import io.roastedroot.proxywasm.StartException; -import io.roastedroot.proxywasm.internal.Plugin; import io.roastedroot.proxywasm.internal.Pool; import io.roastedroot.proxywasm.internal.ServerAdaptor; import io.roastedroot.proxywasm.jaxrs.ProxyWasm; @@ -27,20 +26,14 @@ public void init(Iterable factories, ServerAdaptor serverAdaptor) } for (var factory : factories) { - Plugin plugin = null; - try { - plugin = (Plugin) factory.create(); - } catch (Throwable e) { - throw new StartException("Plugin create failed.", e); - } - String name = plugin.name(); + String name = factory.name(); if (this.pluginPools.containsKey(name)) { throw new IllegalArgumentException("Duplicate wasm plugin name: " + name); } Pool pool = - plugin.isShared() - ? new Pool.SharedPlugin(serverAdaptor, plugin) - : new Pool.PluginPerRequest(serverAdaptor, factory, plugin); + factory.shared() + ? new Pool.SharedPlugin(serverAdaptor, factory) + : new Pool.PluginPerRequest(serverAdaptor, factory); this.pluginPools.put(name, pool); } } diff --git a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/example/App.java b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/example/App.java index c5c53dc..16c49df 100644 --- a/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/example/App.java +++ b/proxy-wasm-jaxrs/src/test/java/io/roastedroot/proxywasm/jaxrs/example/App.java @@ -1,13 +1,12 @@ package io.roastedroot.proxywasm.jaxrs.example; -import com.dylibso.chicory.experimental.aot.AotMachine; +import com.dylibso.chicory.compiler.MachineFactoryCompiler; import com.dylibso.chicory.wasm.Parser; import com.dylibso.chicory.wasm.WasmModule; import com.google.gson.Gson; -import io.roastedroot.proxywasm.Plugin; import io.roastedroot.proxywasm.PluginFactory; -import io.roastedroot.proxywasm.StartException; import java.net.URI; +import java.net.URISyntaxException; import java.nio.file.Path; import java.util.Map; @@ -20,48 +19,43 @@ public static WasmModule parseTestModule(String file) { return Parser.parse(Path.of(EXAMPLES_DIR + file)); } - public static PluginFactory headerTests() throws StartException { - return () -> - Plugin.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) - .withName("headerTests") - .withShared(true) - .withLogger(new MockLogger("headerTests")) - .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) - .withMachineFactory(AotMachine::new) - .build(); + public static PluginFactory headerTests() { + return PluginFactory.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) + .withName("headerTests") + .withShared(true) + .withLogger(new MockLogger("headerTests")) + .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); } - public static PluginFactory headerTestsNotShared() throws StartException { - return () -> - Plugin.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) - .withName("headerTestsNotShared") - .withLogger(new MockLogger("headerTestsNotShared")) - .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) - .withMachineFactory(AotMachine::new) - .build(); + public static PluginFactory headerTestsNotShared() { + return PluginFactory.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) + .withName("headerTestsNotShared") + .withLogger(new MockLogger("headerTestsNotShared")) + .withPluginConfig(gson.toJson(Map.of("type", "headerTests"))) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); } - public static PluginFactory tickTests() throws StartException { - return () -> - Plugin.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) - .withName("tickTests") - .withShared(true) - .withLogger(new MockLogger("tickTests")) - .withPluginConfig(gson.toJson(Map.of("type", "tickTests"))) - .withMachineFactory(AotMachine::new) - .build(); + public static PluginFactory tickTests() { + return PluginFactory.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) + .withName("tickTests") + .withShared(true) + .withLogger(new MockLogger("tickTests")) + .withPluginConfig(gson.toJson(Map.of("type", "tickTests"))) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); } - public static PluginFactory ffiTests() throws StartException { - return () -> - Plugin.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) - .withName("ffiTests") - .withLogger(new MockLogger("ffiTests")) - .withPluginConfig( - gson.toJson(Map.of("type", "ffiTests", "function", "reverse"))) - .withForeignFunctions(Map.of("reverse", App::reverse)) - .withMachineFactory(AotMachine::new) - .build(); + public static PluginFactory ffiTests() { + return PluginFactory.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) + .withName("ffiTests") + .withLogger(new MockLogger("ffiTests")) + .withPluginConfig(gson.toJson(Map.of("type", "ffiTests", "function", "reverse"))) + .withForeignFunctions(Map.of("reverse", App::reverse)) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); } public static byte[] reverse(byte[] data) { @@ -72,19 +66,18 @@ public static byte[] reverse(byte[] data) { return reversed; } - public static PluginFactory httpCallTests() throws StartException { - return () -> - Plugin.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) - .withName("httpCallTests") - .withLogger(new MockLogger("httpCallTests")) - .withPluginConfig( - gson.toJson( - Map.of( - "type", "httpCallTests", - "upstream", "web_service", - "path", "/ok"))) - .withUpstreams(Map.of("web_service", new URI("http://localhost:8081"))) - .withMachineFactory(AotMachine::new) - .build(); + public static PluginFactory httpCallTests() throws URISyntaxException { + return PluginFactory.builder(parseTestModule("/go-examples/unit_tester/main.wasm")) + .withName("httpCallTests") + .withLogger(new MockLogger("httpCallTests")) + .withPluginConfig( + gson.toJson( + Map.of( + "type", "httpCallTests", + "upstream", "web_service", + "path", "/ok"))) + .withUpstreams(Map.of("web_service", new URI("http://localhost:8081"))) + .withMachineFactory(MachineFactoryCompiler::compile) + .build(); } }