From 08f1418f3bff2c60dcd1c9cc30c0406678d9adab Mon Sep 17 00:00:00 2001 From: Hiram Chirino Date: Fri, 28 Mar 2025 08:10:58 -0400 Subject: [PATCH] Refactoring. Hiding some internal handler implementation details in the plugin package. And renaming some classes to be a little more consistent. Signed-off-by: Hiram Chirino --- .../io/roastedroot/proxywasm/Handler.java | 4 + .../io/roastedroot/proxywasm/ProxyWasm.java | 10 +- .../proxywasm/plugin/HttpCallResponse.java | 16 + .../plugin/HttpCallResponseHandler.java | 5 + .../proxywasm/plugin/HttpContext.java | 3 +- .../roastedroot/proxywasm/plugin/Metric.java | 33 ++ .../roastedroot/proxywasm/plugin/Plugin.java | 426 ++++++++++++++++-- .../proxywasm/plugin/PluginHandler.java | 417 ----------------- .../io/roastedroot/proxywasm/plugin/Pool.java | 8 +- .../proxywasm/plugin/SendResponse.java | 30 +- .../proxywasm/plugin/ServerAdaptor.java | 19 +- .../proxywasm/jaxrs/example/Resources.java | 4 +- .../ProxyWasmJaxrsQuarkusProcessor.java | 4 +- .../proxywasm/jaxrs/example/Resources.java | 12 +- .../{NamedWasmPlugin.java => WasmPlugin.java} | 4 +- .../proxywasm/jaxrs/WasmPluginFeature.java | 11 +- ...yWasmFilter.java => WasmPluginFilter.java} | 12 +- ...ttpServer.java => VertxServerAdaptor.java} | 10 +- 18 files changed, 527 insertions(+), 501 deletions(-) create mode 100644 proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponse.java create mode 100644 proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponseHandler.java create mode 100644 proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Metric.java delete mode 100644 proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/PluginHandler.java rename proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/{NamedWasmPlugin.java => WasmPlugin.java} (75%) rename proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/{ProxyWasmFilter.java => WasmPluginFilter.java} (96%) rename proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/{VertxHttpServer.java => VertxServerAdaptor.java} (92%) diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java index 0535911..7d7c333 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/Handler.java @@ -3,6 +3,10 @@ import java.util.List; public interface Handler { + /** + * The default handler. It holds no state. + */ + Handler DEFAULT = new Handler() {}; default void log(LogLevel level, String message) throws WasmException {} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyWasm.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyWasm.java index 0838dbe..1629722 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyWasm.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/ProxyWasm.java @@ -22,7 +22,6 @@ public final class ProxyWasm implements Closeable { private final ABI abi; - private final Handler pluginHandler; private final WasiPreview1 wasi; private final AtomicInteger nextContextID = new AtomicInteger(1); @@ -33,6 +32,7 @@ public final class ProxyWasm implements Closeable { private ProxyMap httpCallResponseHeaders; private ProxyMap httpCallResponseTrailers; private byte[] httpCallResponseBody; + private Handler pluginHandler; private ProxyWasm(Builder other) throws StartException { this.pluginHandler = Objects.requireNonNullElse(other.pluginHandler, new Handler() {}); @@ -52,6 +52,14 @@ private ProxyWasm(Builder other) throws StartException { } } + public Handler getPluginHandler() { + return pluginHandler; + } + + public void setPluginHandler(Handler pluginHandler) { + this.pluginHandler = pluginHandler; + } + public void start() throws StartException { if (pluginContext != null) { return; diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponse.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponse.java new file mode 100644 index 0000000..0e586fb --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponse.java @@ -0,0 +1,16 @@ +package io.roastedroot.proxywasm.plugin; + +import io.roastedroot.proxywasm.ProxyMap; + +public class HttpCallResponse { + + public final int statusCode; + public final ProxyMap headers; + public final byte[] body; + + public HttpCallResponse(int statusCode, ProxyMap headers, byte[] body) { + this.statusCode = statusCode; + this.headers = headers; + this.body = body; + } +} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponseHandler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponseHandler.java new file mode 100644 index 0000000..a2de329 --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpCallResponseHandler.java @@ -0,0 +1,5 @@ +package io.roastedroot.proxywasm.plugin; + +public interface HttpCallResponseHandler { + void call(HttpCallResponse response); +} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java index 3123af3..cd8b2d1 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/HttpContext.java @@ -126,10 +126,11 @@ public SendResponse consumeSentHttpResponse() { } class HandlerImpl extends ChainedHandler { + private final Handler next = plugin.wasm.getPluginHandler(); @Override protected Handler next() { - return plugin.handler; + return next; } public ProxyMap getHttpRequestHeaders() { diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Metric.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Metric.java new file mode 100644 index 0000000..62e4191 --- /dev/null +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Metric.java @@ -0,0 +1,33 @@ +package io.roastedroot.proxywasm.plugin; + +import io.roastedroot.proxywasm.MetricType; + +public class Metric { + + final int id; + final MetricType type; + final String name; + long value; + + public Metric(int id, MetricType type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + + public int id() { + return id; + } + + public MetricType type() { + return type; + } + + public String name() { + return name; + } + + public long value() { + return value; + } +} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java index 43d103b..e2f62c4 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Plugin.java @@ -1,41 +1,63 @@ package io.roastedroot.proxywasm.plugin; import static io.roastedroot.proxywasm.Helpers.bytes; +import static io.roastedroot.proxywasm.WellKnownHeaders.AUTHORITY; +import static io.roastedroot.proxywasm.WellKnownHeaders.METHOD; +import static io.roastedroot.proxywasm.WellKnownHeaders.PATH; +import static io.roastedroot.proxywasm.WellKnownHeaders.SCHEME; +import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_NAME; +import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_VM_ID; import com.dylibso.chicory.runtime.ImportMemory; import com.dylibso.chicory.runtime.Instance; import com.dylibso.chicory.wasm.WasmModule; +import io.roastedroot.proxywasm.ArrayProxyMap; +import io.roastedroot.proxywasm.ChainedHandler; import io.roastedroot.proxywasm.ForeignFunction; +import io.roastedroot.proxywasm.Handler; +import io.roastedroot.proxywasm.LogLevel; +import io.roastedroot.proxywasm.MetricType; +import io.roastedroot.proxywasm.ProxyMap; import io.roastedroot.proxywasm.ProxyWasm; import io.roastedroot.proxywasm.StartException; +import io.roastedroot.proxywasm.WasmException; +import io.roastedroot.proxywasm.WasmResult; +import java.net.URI; +import java.net.URISyntaxException; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; public final class Plugin { - final PluginHandler handler; private final ReentrantLock lock = new ReentrantLock(); - private final boolean shared; final ProxyWasm wasm; ServerAdaptor httpServer; + private final boolean shared; + private final String name; - public Logger logger() { - return handler.logger; - } - - private Plugin(ProxyWasm proxyWasm, PluginHandler handler, boolean shared) { + private Plugin(Builder builder, ProxyWasm proxyWasm) throws StartException { Objects.requireNonNull(proxyWasm); - Objects.requireNonNull(handler); - this.shared = shared; + this.name = Objects.requireNonNullElse(builder.name, "default"); + this.shared = builder.shared; + this.foreignFunctions = builder.foreignFunctions; + this.upstreams = builder.upstreams; + this.strictUpstreams = builder.strictUpstreams; + this.minTickPeriodMilliseconds = builder.minTickPeriodMilliseconds; + this.logger = builder.logger; + this.vmConfig = builder.vmConfig; + this.pluginConfig = builder.pluginConfig; + this.wasm = proxyWasm; - this.handler = handler; - this.handler.setPlugin(this); + this.wasm.setPluginHandler(new HandlerImpl()); + this.wasm.start(); } public String name() { - return handler.getName(); + return name; } public static Plugin.Builder builder() { @@ -58,6 +80,10 @@ public void setHttpServer(ServerAdaptor httpServer) { this.httpServer = httpServer; } + public Logger logger() { + return logger; + } + public HttpContext createHttpContext(HttpRequestAdaptor requestAdaptor) { return new HttpContext(this, requestAdaptor); } @@ -66,7 +92,15 @@ public void close() { lock(); try { wasm.close(); - handler.close(); + if (cancelTick != null) { + cancelTick.run(); + cancelTick = null; + } + for (var cancelHttpCall : httpCalls.values()) { + cancelHttpCall.run(); + } + httpCalls.clear(); + } finally { unlock(); } @@ -74,38 +108,44 @@ public void close() { public static class Builder implements Cloneable { - private PluginHandler handler = new PluginHandler(); - private ProxyWasm.Builder proxyWasmBuilder = - ProxyWasm.builder().withPluginHandler(handler).withStart(false); + private ProxyWasm.Builder proxyWasmBuilder = ProxyWasm.builder().withStart(false); private boolean shared = true; + private String name; + private HashMap foreignFunctions; + private HashMap upstreams; + private boolean strictUpstreams; + private int minTickPeriodMilliseconds; + private Logger logger; + private byte[] vmConfig; + private byte[] pluginConfig; public Plugin.Builder withName(String name) { - this.handler.name = name; + this.name = name; return this; } public Builder withForeignFunctions(Map functions) { - this.handler.foreignFunctions = new HashMap<>(functions); + this.foreignFunctions = new HashMap<>(functions); return this; } public Builder withUpstreams(Map upstreams) { - this.handler.upstreams = new HashMap<>(upstreams); + this.upstreams = new HashMap<>(upstreams); return this; } public Builder withStrictUpstreams(boolean strictUpstreams) { - this.handler.strictUpstreams = strictUpstreams; + this.strictUpstreams = strictUpstreams; return this; } public Builder withMinTickPeriodMilliseconds(int minTickPeriodMilliseconds) { - this.handler.minTickPeriodMilliseconds = minTickPeriodMilliseconds; + this.minTickPeriodMilliseconds = minTickPeriodMilliseconds; return this; } public Builder withLogger(Logger logger) { - this.handler.logger = logger; + this.logger = logger; return this; } @@ -115,22 +155,22 @@ public Plugin.Builder withShared(boolean shared) { } public Plugin.Builder withVmConfig(byte[] vmConfig) { - this.handler.vmConfig = vmConfig; + this.vmConfig = vmConfig; return this; } public Plugin.Builder withVmConfig(String vmConfig) { - this.handler.vmConfig = bytes(vmConfig); + this.vmConfig = bytes(vmConfig); return this; } public Plugin.Builder withPluginConfig(byte[] pluginConfig) { - this.handler.pluginConfig = pluginConfig; + this.pluginConfig = pluginConfig; return this; } public Plugin.Builder withPluginConfig(String pluginConfig) { - this.handler.pluginConfig = bytes(pluginConfig); + this.pluginConfig = bytes(pluginConfig); return this; } @@ -152,7 +192,339 @@ public Plugin build(Instance instance) throws StartException { } public Plugin build(ProxyWasm proxyWasm) throws StartException { - return new Plugin(proxyWasm, handler, shared); + return new Plugin(this, proxyWasm); + } + } + + public Logger logger; + static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); + byte[] vmConfig; + byte[] pluginConfig; + private final AtomicInteger lastCallId = new AtomicInteger(0); + private final HashMap httpCalls = new HashMap<>(); + HashMap upstreams = new HashMap<>(); + boolean strictUpstreams; + int minTickPeriodMilliseconds; + private int tickPeriodMilliseconds; + private Runnable cancelTick; + HashMap foreignFunctions; + private final AtomicInteger lastMetricId = new AtomicInteger(0); + private HashMap metrics = new HashMap<>(); + private HashMap metricsByName = new HashMap<>(); + private byte[] funcCallData = new byte[0]; + private final HashMap, byte[]> properties = new HashMap<>(); + + public static class Metric { + + public final int id; + public final MetricType type; + public final String name; + public long value; + + public Metric(int id, MetricType type, String name) { + this.id = id; + this.type = type; + this.name = name; + } + } + + class HandlerImpl extends ChainedHandler { + + @Override + protected Handler next() { + return Handler.DEFAULT; + } + + // ////////////////////////////////////////////////////////////////////// + // Plugin config + // ////////////////////////////////////////////////////////////////////// + @Override + public byte[] getVmConfig() { + return vmConfig; + } + + @Override + public byte[] getPluginConfig() { + return pluginConfig; + } + + // ////////////////////////////////////////////////////////////////////// + // Properties + // ////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getProperty(List path) throws WasmException { + // TODO: do we need field for vm_id and root_id? + if (PLUGIN_VM_ID.equals(path)) { + return bytes(name); + } + if (PLUGIN_NAME.equals(path)) { + return bytes(name); + } + return properties.get(path); + } + + @Override + public WasmResult setProperty(List path, byte[] value) { + properties.put(path, value); + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // Logging + // ////////////////////////////////////////////////////////////////////// + + @Override + public void log(LogLevel level, String message) throws WasmException { + Logger l = logger; + if (l == null) { + super.log(level, message); + return; + } + l.log(level, message); + } + + @Override + public LogLevel getLogLevel() throws WasmException { + Logger l = logger; + if (l == null) { + return super.getLogLevel(); + } + return l.getLogLevel(); + } + + // ////////////////////////////////////////////////////////////////////// + // Timers + // ////////////////////////////////////////////////////////////////////// + @Override + public WasmResult setTickPeriodMilliseconds(int tickMs) { + + // check for no change + if (tickMs == tickPeriodMilliseconds) { + return WasmResult.OK; + } + + // cancel the current tick, if any + if (cancelTick != null) { + cancelTick.run(); + cancelTick = null; + } + + // set the new tick period, if any + tickPeriodMilliseconds = tickMs; + if (tickPeriodMilliseconds == 0) { + return WasmResult.OK; + } + + // schedule the new tick + cancelTick = + httpServer.scheduleTick( + Math.max(minTickPeriodMilliseconds, tickPeriodMilliseconds), + () -> { + lock(); + try { + wasm.tick(); + } finally { + unlock(); + } + }); + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // Foreign function interface (FFI) + // ////////////////////////////////////////////////////////////////////// + + @Override + public byte[] getFuncCallData() { + return funcCallData; + } + + @Override + public WasmResult setFuncCallData(byte[] data) { + funcCallData = data; + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // HTTP calls + // ////////////////////////////////////////////////////////////////////// + + @Override + public int httpCall( + String upstreamName, + ProxyMap headers, + byte[] body, + ProxyMap trailers, + int timeoutMilliseconds) + throws WasmException { + + var method = headers.get(METHOD); + if (method == null) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + + var scheme = headers.get(SCHEME); + if (scheme == null) { + scheme = "http"; + } + var authority = headers.get(AUTHORITY); + if (authority == null) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + headers.put("Host", authority); + + var connectHostPort = upstreams.get(upstreamName); + if (connectHostPort == null && strictUpstreams) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + if (connectHostPort == null) { + connectHostPort = authority; + } + + URI connectUri = null; + try { + connectUri = URI.create(scheme + "://" + connectHostPort); + } catch (IllegalArgumentException e) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + + var connectHost = connectUri.getHost(); + var connectPort = connectUri.getPort(); + if (connectPort == -1) { + connectPort = "https".equals(scheme) ? 443 : 80; + } + + var path = headers.get(PATH); + if (path == null) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + if (!path.isEmpty() && !path.startsWith("/")) { + path = "/" + path; + } + + URI uri = null; + try { + uri = + URI.create( + new URI(scheme, null, authority, connectPort, null, null, null) + + path); + } catch (IllegalArgumentException | URISyntaxException e) { + throw new WasmException(WasmResult.BAD_ARGUMENT); + } + + // Remove all the pseudo headers + for (var r : new ArrayProxyMap(headers).entries()) { + if (r.getKey().startsWith(":")) { + headers.remove(r.getKey()); + } + } + + try { + var id = lastCallId.incrementAndGet(); + var future = + httpServer.scheduleHttpCall( + method, + connectHost, + connectPort, + uri, + headers, + body, + trailers, + timeoutMilliseconds, + (resp) -> { + lock(); + try { + if (httpCalls.remove(id) == null) { + return; // the call could have already been cancelled + } + wasm.sendHttpCallResponse( + id, resp.headers, new ArrayProxyMap(), resp.body); + } finally { + unlock(); + } + }); + httpCalls.put(id, future); + return id; + } catch (InterruptedException e) { + throw new WasmException(WasmResult.INTERNAL_FAILURE); + } + } + + @Override + public int dispatchHttpCall( + String upstreamName, + ProxyMap headers, + byte[] body, + ProxyMap trailers, + int timeoutMilliseconds) + throws WasmException { + return httpCall(upstreamName, headers, body, trailers, timeoutMilliseconds); + } + + // ////////////////////////////////////////////////////////////////////// + // Metrics + // ////////////////////////////////////////////////////////////////////// + + @Override + public int defineMetric(MetricType type, String name) throws WasmException { + var id = lastMetricId.incrementAndGet(); + io.roastedroot.proxywasm.plugin.Metric value = + new io.roastedroot.proxywasm.plugin.Metric(id, type, name); + metrics.put(id, value); + metricsByName.put(name, value); + return id; + } + + @Override + public long getMetric(int metricId) throws WasmException { + var metric = metrics.get(metricId); + if (metric == null) { + throw new WasmException(WasmResult.NOT_FOUND); + } + return metric.value; + } + + @Override + public WasmResult incrementMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value += value; + return WasmResult.OK; + } + + @Override + public WasmResult recordMetric(int metricId, long value) { + var metric = metrics.get(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metric.value = value; + return WasmResult.OK; + } + + @Override + public WasmResult removeMetric(int metricId) { + io.roastedroot.proxywasm.plugin.Metric metric = metrics.remove(metricId); + if (metric == null) { + return WasmResult.NOT_FOUND; + } + metricsByName.remove(metric.name); + return WasmResult.OK; + } + + // ////////////////////////////////////////////////////////////////////// + // FFI + // ////////////////////////////////////////////////////////////////////// + + @Override + public ForeignFunction getForeignFunction(String name) { + if (foreignFunctions == null) { + return null; + } + return foreignFunctions.get(name); } } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/PluginHandler.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/PluginHandler.java deleted file mode 100644 index a32c15c..0000000 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/PluginHandler.java +++ /dev/null @@ -1,417 +0,0 @@ -package io.roastedroot.proxywasm.plugin; - -import static io.roastedroot.proxywasm.Helpers.bytes; -import static io.roastedroot.proxywasm.WellKnownHeaders.AUTHORITY; -import static io.roastedroot.proxywasm.WellKnownHeaders.METHOD; -import static io.roastedroot.proxywasm.WellKnownHeaders.PATH; -import static io.roastedroot.proxywasm.WellKnownHeaders.SCHEME; -import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_NAME; -import static io.roastedroot.proxywasm.WellKnownProperties.PLUGIN_VM_ID; - -import io.roastedroot.proxywasm.ArrayProxyMap; -import io.roastedroot.proxywasm.ChainedHandler; -import io.roastedroot.proxywasm.ForeignFunction; -import io.roastedroot.proxywasm.Handler; -import io.roastedroot.proxywasm.LogLevel; -import io.roastedroot.proxywasm.MetricType; -import io.roastedroot.proxywasm.ProxyMap; -import io.roastedroot.proxywasm.WasmException; -import io.roastedroot.proxywasm.WasmResult; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.HashMap; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -class PluginHandler extends ChainedHandler { - - // ////////////////////////////////////////////////////////////////////// - // Filter Chain Methods - // ////////////////////////////////////////////////////////////////////// - private Handler next; - Plugin plugin; - - PluginHandler() { - this(new Handler() {}); - } - - PluginHandler(Handler next) { - this.next = next; - } - - @Override - protected Handler next() { - return next; - } - - // ////////////////////////////////////////////////////////////////////// - // Cleanup - // ////////////////////////////////////////////////////////////////////// - public void close() { - if (cancelTick != null) { - cancelTick.run(); - cancelTick = null; - } - for (var cancelHttpCall : httpCalls.values()) { - cancelHttpCall.run(); - } - httpCalls.clear(); - } - - // ////////////////////////////////////////////////////////////////////// - // Plugin config - // ////////////////////////////////////////////////////////////////////// - - byte[] vmConfig; - - @Override - public byte[] getVmConfig() { - return vmConfig; - } - - byte[] pluginConfig; - - @Override - public byte[] getPluginConfig() { - return pluginConfig; - } - - // ////////////////////////////////////////////////////////////////////// - // Properties - // ////////////////////////////////////////////////////////////////////// - - String name = "default"; - - public String getName() { - return name; - } - - public void setName(String name) { - Objects.requireNonNull(name); - this.name = name; - } - - private final HashMap, byte[]> properties = new HashMap<>(); - - @Override - public byte[] getProperty(List path) throws WasmException { - // TODO: do we need field for vm_id and root_id? - if (PLUGIN_VM_ID.equals(path)) { - return bytes(name); - } - if (PLUGIN_NAME.equals(path)) { - return bytes(name); - } - return properties.get(path); - } - - @Override - public WasmResult setProperty(List path, byte[] value) { - properties.put(path, value); - return WasmResult.OK; - } - - // ////////////////////////////////////////////////////////////////////// - // Logging - // ////////////////////////////////////////////////////////////////////// - - public Logger logger; - - static final boolean DEBUG = "true".equals(System.getenv("DEBUG")); - - @Override - public void log(LogLevel level, String message) throws WasmException { - Logger l = logger; - if (l == null) { - super.log(level, message); - return; - } - l.log(level, message); - } - - @Override - public LogLevel getLogLevel() throws WasmException { - Logger l = logger; - if (l == null) { - return super.getLogLevel(); - } - return l.getLogLevel(); - } - - // ////////////////////////////////////////////////////////////////////// - // Timers - // ////////////////////////////////////////////////////////////////////// - - int minTickPeriodMilliseconds; - private int tickPeriodMilliseconds; - private Runnable cancelTick; - - public int getTickPeriodMilliseconds() { - return tickPeriodMilliseconds; - } - - @Override - public WasmResult setTickPeriodMilliseconds(int tickPeriodMilliseconds) { - - // check for no change - if (tickPeriodMilliseconds == this.tickPeriodMilliseconds) { - return WasmResult.OK; - } - - // cancel the current tick, if any - if (cancelTick != null) { - cancelTick.run(); - cancelTick = null; - } - - // set the new tick period, if any - this.tickPeriodMilliseconds = tickPeriodMilliseconds; - if (this.tickPeriodMilliseconds == 0) { - return WasmResult.OK; - } - - // schedule the new tick - this.cancelTick = - this.plugin.httpServer.scheduleTick( - Math.max(minTickPeriodMilliseconds, this.tickPeriodMilliseconds), - () -> { - this.plugin.lock(); - try { - this.plugin.wasm.tick(); - } finally { - this.plugin.unlock(); - } - }); - return WasmResult.OK; - } - - // ////////////////////////////////////////////////////////////////////// - // Foreign function interface (FFI) - // ////////////////////////////////////////////////////////////////////// - - private byte[] funcCallData = new byte[0]; - - @Override - public byte[] getFuncCallData() { - return this.funcCallData; - } - - @Override - public WasmResult setFuncCallData(byte[] data) { - this.funcCallData = data; - return WasmResult.OK; - } - - public void setPlugin(Plugin plugin) { - this.plugin = plugin; - } - - // ////////////////////////////////////////////////////////////////////// - // HTTP calls - // ////////////////////////////////////////////////////////////////////// - - private final AtomicInteger lastCallId = new AtomicInteger(0); - private final HashMap httpCalls = new HashMap<>(); - - HashMap upstreams = new HashMap<>(); - boolean strictUpstreams; - - @Override - public int httpCall( - String upstreamName, - ProxyMap headers, - byte[] body, - ProxyMap trailers, - int timeoutMilliseconds) - throws WasmException { - - var method = headers.get(METHOD); - if (method == null) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - - var scheme = headers.get(SCHEME); - if (scheme == null) { - scheme = "http"; - } - var authority = headers.get(AUTHORITY); - if (authority == null) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - headers.put("Host", authority); - - var connectHostPort = upstreams.get(upstreamName); - if (connectHostPort == null && strictUpstreams) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - if (connectHostPort == null) { - connectHostPort = authority; - } - - URI connectUri = null; - try { - connectUri = URI.create(scheme + "://" + connectHostPort); - } catch (IllegalArgumentException e) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - - var connectHost = connectUri.getHost(); - var connectPort = connectUri.getPort(); - if (connectPort == -1) { - connectPort = "https".equals(scheme) ? 443 : 80; - } - - var path = headers.get(PATH); - if (path == null) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - if (!path.isEmpty() && !path.startsWith("/")) { - path = "/" + path; - } - - URI uri = null; - try { - uri = - URI.create( - new URI(scheme, null, authority, connectPort, null, null, null) + path); - } catch (IllegalArgumentException | URISyntaxException e) { - throw new WasmException(WasmResult.BAD_ARGUMENT); - } - - // Remove all the pseudo headers - for (var r : new ArrayProxyMap(headers).entries()) { - if (r.getKey().startsWith(":")) { - headers.remove(r.getKey()); - } - } - - try { - var id = lastCallId.incrementAndGet(); - var future = - this.plugin.httpServer.scheduleHttpCall( - method, - connectHost, - connectPort, - uri, - headers, - body, - trailers, - timeoutMilliseconds, - (resp) -> { - this.plugin.lock(); - try { - if (httpCalls.remove(id) == null) { - return; // the call could have already been cancelled - } - this.plugin.wasm.sendHttpCallResponse( - id, resp.headers, new ArrayProxyMap(), resp.body); - } finally { - this.plugin.unlock(); - } - }); - httpCalls.put(id, future); - return id; - } catch (InterruptedException e) { - throw new WasmException(WasmResult.INTERNAL_FAILURE); - } - } - - @Override - public int dispatchHttpCall( - String upstreamName, - ProxyMap headers, - byte[] body, - ProxyMap trailers, - int timeoutMilliseconds) - throws WasmException { - return httpCall(upstreamName, headers, body, trailers, timeoutMilliseconds); - } - - // ////////////////////////////////////////////////////////////////////// - // Metrics - // ////////////////////////////////////////////////////////////////////// - - public static class Metric { - - public final int id; - public final MetricType type; - public final String name; - public long value; - - public Metric(int id, MetricType type, String name) { - this.id = id; - this.type = type; - this.name = name; - } - } - - private final AtomicInteger lastMetricId = new AtomicInteger(0); - private HashMap metrics = new HashMap<>(); - private HashMap metricsByName = new HashMap<>(); - - @Override - public int defineMetric(MetricType type, String name) throws WasmException { - var id = lastMetricId.incrementAndGet(); - Metric value = new Metric(id, type, name); - metrics.put(id, value); - metricsByName.put(name, value); - return id; - } - - @Override - public long getMetric(int metricId) throws WasmException { - var metric = metrics.get(metricId); - if (metric == null) { - throw new WasmException(WasmResult.NOT_FOUND); - } - return metric.value; - } - - public Metric getMetric(String name) { - return metricsByName.get(name); - } - - @Override - public WasmResult incrementMetric(int metricId, long value) { - var metric = metrics.get(metricId); - if (metric == null) { - return WasmResult.NOT_FOUND; - } - metric.value += value; - return WasmResult.OK; - } - - @Override - public WasmResult recordMetric(int metricId, long value) { - var metric = metrics.get(metricId); - if (metric == null) { - return WasmResult.NOT_FOUND; - } - metric.value = value; - return WasmResult.OK; - } - - @Override - public WasmResult removeMetric(int metricId) { - Metric metric = metrics.remove(metricId); - if (metric == null) { - return WasmResult.NOT_FOUND; - } - metricsByName.remove(metric.name); - return WasmResult.OK; - } - - // ////////////////////////////////////////////////////////////////////// - // FFI - // ////////////////////////////////////////////////////////////////////// - HashMap foreignFunctions; - - @Override - public ForeignFunction getForeignFunction(String name) { - if (foreignFunctions == null) { - return null; - } - return foreignFunctions.get(name); - } -} diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Pool.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Pool.java index 2d7307f..ee67b5e 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Pool.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/Pool.java @@ -14,10 +14,10 @@ public interface Pool { default void close() {} - class AppScoped implements Pool { + class SharedPlugin implements Pool { private final Plugin plugin; - public AppScoped(Plugin plugin) throws StartException { + public SharedPlugin(Plugin plugin) throws StartException { this.plugin = plugin; } @@ -48,12 +48,12 @@ public Plugin borrow() throws StartException { } } - class RequestScoped implements Pool { + class PluginPerRequest implements Pool { final PluginFactory factory; private final String name; - public RequestScoped(PluginFactory factory, Plugin plugin) { + public PluginPerRequest(PluginFactory factory, Plugin plugin) { this.factory = factory; this.name = plugin.name(); release(plugin); diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/SendResponse.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/SendResponse.java index b79b2b1..272a47b 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/SendResponse.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/SendResponse.java @@ -4,11 +4,11 @@ public class SendResponse { - public final int statusCode; - public final byte[] statusCodeDetails; - public final byte[] body; - public final ProxyMap headers; - public final int grpcStatus; + private final int statusCode; + private final byte[] statusCodeDetails; + private final byte[] body; + private final ProxyMap headers; + private final int grpcStatus; public SendResponse( int responseCode, @@ -22,4 +22,24 @@ public SendResponse( this.headers = additionalHeaders; this.grpcStatus = grpcStatus; } + + public int statusCode() { + return statusCode; + } + + public byte[] statusCodeDetails() { + return statusCodeDetails; + } + + public byte[] body() { + return body; + } + + public ProxyMap headers() { + return headers; + } + + public int grpcStatus() { + return grpcStatus; + } } diff --git a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/ServerAdaptor.java b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/ServerAdaptor.java index 1940555..1857732 100644 --- a/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/ServerAdaptor.java +++ b/proxy-wasm-java-host/src/main/java/io/roastedroot/proxywasm/plugin/ServerAdaptor.java @@ -19,23 +19,6 @@ Runnable scheduleHttpCall( byte[] body, ProxyMap trailers, int timeout, - HandlerHttpResponseHandler handler) + HttpCallResponseHandler handler) throws InterruptedException; - - class HandlerHttpResponse { - - public final int statusCode; - public final ProxyMap headers; - public final byte[] body; - - public HandlerHttpResponse(int statusCode, ProxyMap headers, byte[] body) { - this.statusCode = statusCode; - this.headers = headers; - this.body = body; - } - } - - interface HandlerHttpResponseHandler { - void call(HandlerHttpResponse response); - } } diff --git a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java index 4ccec2e..50a2f6c 100644 --- a/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java +++ b/proxy-wasm-jaxrs-quarkus-example/src/main/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java @@ -1,6 +1,6 @@ package io.roastedroot.proxywasm.jaxrs.example; -import io.roastedroot.proxywasm.jaxrs.NamedWasmPlugin; +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -9,7 +9,7 @@ public class Resources { @Path("/ffiTests/reverse") @POST - @NamedWasmPlugin("ffiTests") // filter with ffiTests wasm plugin + @WasmPlugin("ffiTests") // filter with ffiTests wasm plugin public String ffiTests(String body) { return body; } diff --git a/proxy-wasm-jaxrs-quarkus/src/main/java/io/roastedroot/proxywasm/jaxrs/quarkus/deployment/ProxyWasmJaxrsQuarkusProcessor.java b/proxy-wasm-jaxrs-quarkus/src/main/java/io/roastedroot/proxywasm/jaxrs/quarkus/deployment/ProxyWasmJaxrsQuarkusProcessor.java index 8b9b31d..7bd66a5 100644 --- a/proxy-wasm-jaxrs-quarkus/src/main/java/io/roastedroot/proxywasm/jaxrs/quarkus/deployment/ProxyWasmJaxrsQuarkusProcessor.java +++ b/proxy-wasm-jaxrs-quarkus/src/main/java/io/roastedroot/proxywasm/jaxrs/quarkus/deployment/ProxyWasmJaxrsQuarkusProcessor.java @@ -4,7 +4,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.jaxrs.spi.deployment.AdditionalJaxRsResourceMethodAnnotationsBuildItem; -import io.roastedroot.proxywasm.jaxrs.NamedWasmPlugin; +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; import io.roastedroot.proxywasm.jaxrs.WasmPluginFeature; import java.util.List; import org.jboss.jandex.DotName; @@ -24,6 +24,6 @@ AdditionalBeanBuildItem resources() { @BuildStep public AdditionalJaxRsResourceMethodAnnotationsBuildItem annotations() { return new AdditionalJaxRsResourceMethodAnnotationsBuildItem( - List.of(DotName.createSimple(NamedWasmPlugin.class.getName()))); + List.of(DotName.createSimple(WasmPlugin.class.getName()))); } } diff --git a/proxy-wasm-jaxrs-quarkus/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java b/proxy-wasm-jaxrs-quarkus/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java index 51b1624..d947078 100644 --- a/proxy-wasm-jaxrs-quarkus/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java +++ b/proxy-wasm-jaxrs-quarkus/src/test/java/io/roastedroot/proxywasm/jaxrs/example/Resources.java @@ -1,6 +1,6 @@ package io.roastedroot.proxywasm.jaxrs.example; -import io.roastedroot.proxywasm.jaxrs.NamedWasmPlugin; +import io.roastedroot.proxywasm.jaxrs.WasmPlugin; import jakarta.ws.rs.GET; import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; @@ -37,35 +37,35 @@ public Response ok() { @Path("/headerTests") @GET - @NamedWasmPlugin("headerTests") + @WasmPlugin("headerTests") public String uhttpHeaders(@HeaderParam("x-request-counter") String counter) { return String.format("counter: %s", counter); } @Path("/headerTestsNotShared") @GET - @NamedWasmPlugin("headerTestsNotShared") + @WasmPlugin("headerTestsNotShared") public String unotSharedHttpHeaders(@HeaderParam("x-request-counter") String counter) { return String.format("counter: %s", counter); } @Path("/tickTests/{sub: .+ }") @GET - @NamedWasmPlugin("tickTests") + @WasmPlugin("tickTests") public String tickTests(@PathParam("sub") String sub) { return "hello world"; } @Path("/ffiTests/reverse") @POST - @NamedWasmPlugin("ffiTests") + @WasmPlugin("ffiTests") public String ffiTests(String body) { return body; } @Path("/httpCallTests") @GET - @NamedWasmPlugin("httpCallTests") + @WasmPlugin("httpCallTests") public String httpCallTests() { return "hello world"; } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java similarity index 75% rename from proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java rename to proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java index 89de27f..5cc576c 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/NamedWasmPlugin.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPlugin.java @@ -6,9 +6,9 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@NameBinding // Marks this annotation as being used for JAX-RS filtering +@NameBinding @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) -public @interface NamedWasmPlugin { +public @interface WasmPlugin { String value(); } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java index 0ca1b80..7d7940d 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFeature.java @@ -38,8 +38,8 @@ public WasmPluginFeature(Instance factories, @Any ServerAdaptor h } Pool pool = plugin.isShared() - ? new Pool.AppScoped(plugin) - : new Pool.RequestScoped(factory, plugin); + ? new Pool.SharedPlugin(plugin) + : new Pool.PluginPerRequest(factory, plugin); this.pluginPools.put(name, pool); } } @@ -64,17 +64,16 @@ public void configure(ResourceInfo resourceInfo, FeatureContext context) { var resourceMethod = resourceInfo.getResourceMethod(); if (resourceMethod != null) { - NamedWasmPlugin pluignNameAnnotation = - resourceMethod.getAnnotation(NamedWasmPlugin.class); + WasmPlugin pluignNameAnnotation = resourceMethod.getAnnotation(WasmPlugin.class); if (pluignNameAnnotation == null) { // If no annotation on method, check the class level pluignNameAnnotation = - resourceInfo.getResourceClass().getAnnotation(NamedWasmPlugin.class); + resourceInfo.getResourceClass().getAnnotation(WasmPlugin.class); } if (pluignNameAnnotation != null) { Pool factory = pluginPools.get(pluignNameAnnotation.value()); if (factory != null) { - context.register(new ProxyWasmFilter(factory, httpServerRequest)); + context.register(new WasmPluginFilter(factory, httpServerRequest)); } } } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java similarity index 96% rename from proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java rename to proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java index 36aec35..a6e6c7b 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/ProxyWasmFilter.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/WasmPluginFilter.java @@ -20,7 +20,7 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -public class ProxyWasmFilter +public class WasmPluginFilter implements ContainerRequestFilter, WriterInterceptor, ContainerResponseFilter { private static final String FILTER_CONTEXT_PROPERTY_NAME = HttpContext.class.getName(); @@ -28,7 +28,7 @@ public class ProxyWasmFilter Instance requestAdaptor; - public ProxyWasmFilter(Pool pluginPool, Instance httpServer) { + public WasmPluginFilter(Pool pluginPool, Instance httpServer) { this.pluginPool = pluginPool; this.requestAdaptor = httpServer; } @@ -208,13 +208,13 @@ public void close() throws IOException { public Response toResponse(SendResponse other) { Response.ResponseBuilder builder = - Response.status(other.statusCode, string(other.statusCodeDetails)); - if (other.headers != null) { - for (var entry : other.headers.entries()) { + Response.status(other.statusCode(), string(other.statusCodeDetails())); + if (other.headers() != null) { + for (var entry : other.headers().entries()) { builder = builder.header(entry.getKey(), entry.getValue()); } } - builder.entity(other.body); + builder.entity(other.body()); return builder.build(); } } diff --git a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxHttpServer.java b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxServerAdaptor.java similarity index 92% rename from proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxHttpServer.java rename to proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxServerAdaptor.java index b5d5bd2..1c2a7e0 100644 --- a/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxHttpServer.java +++ b/proxy-wasm-jaxrs/src/main/java/io/roastedroot/proxywasm/jaxrs/vertx/VertxServerAdaptor.java @@ -1,6 +1,8 @@ package io.roastedroot.proxywasm.jaxrs.vertx; import io.roastedroot.proxywasm.ProxyMap; +import io.roastedroot.proxywasm.plugin.HttpCallResponse; +import io.roastedroot.proxywasm.plugin.HttpCallResponseHandler; import io.roastedroot.proxywasm.plugin.ServerAdaptor; import io.vertx.core.Vertx; import io.vertx.core.buffer.Buffer; @@ -18,7 +20,7 @@ @Alternative @Priority(200) @ApplicationScoped -public class VertxHttpServer implements ServerAdaptor { +public class VertxServerAdaptor implements ServerAdaptor { @Inject Vertx vertx; @@ -52,7 +54,7 @@ public Runnable scheduleHttpCall( byte[] body, ProxyMap trailers, int timeout, - HandlerHttpResponseHandler handler) + HttpCallResponseHandler handler) throws InterruptedException { var f = client.request(HttpMethod.valueOf(method), port, host, uri.toString()) @@ -78,14 +80,14 @@ public Runnable scheduleHttpCall( e.getKey(), e.getValue())); handler.call( - new HandlerHttpResponse( + new HttpCallResponse( result.statusCode(), h, bodyHandler.getBytes())); }); } else { handler.call( - new HandlerHttpResponse( + new HttpCallResponse( 500, ProxyMap.of(), resp.cause().getMessage().getBytes()));