feat: Implement path based router for host with multi-component routing capability within a workload#5
Conversation
0bb8ea0 to
8559b39
Compare
431bc01 to
f5fb3be
Compare
ed8e0a0 to
51ad9c8
Compare
crates/wash-runtime/src/host/http.rs
Outdated
| } | ||
|
|
||
| fn route_incoming_request(&self, req: &Request<Incoming>) -> Result<String> { | ||
| let lock = self.router.try_read()?; |
There was a problem hiding this comment.
try_read?
dont we want to wait until the router is rebuild? (I would assume that that doesnt take long)
51ad9c8 to
3f3bc48
Compare
Mees-Molenaar
left a comment
There was a problem hiding this comment.
Looks promising 🚀
| || interface_name.starts_with("wasi:http/") | ||
| { | ||
| trace!(name, "skipping export from linking"); | ||
| continue; |
There was a problem hiding this comment.
Do we always want to skip this? Or only if there are components that have specific configurations?
If we want this, maybe add a descriptive comment that the plugin export for wasi:http is handled later
| let components_arc = resolved_workload.components(); | ||
| let components = components_arc.read().await; |
There was a problem hiding this comment.
I dont like adding the type in the variable name, either just do it in one go, or shadow components
let components = resolved_workload.components();
'let components = components.read().await;`
| // Notify HTTP handler for each HTTP-exporting component | ||
| if workload_requests_http { |
There was a problem hiding this comment.
Let wrap this in a function with a clear name so that you can read what the code is doing based on the function call instead of the comment
crates/wash-runtime/src/host/http.rs
Outdated
| ) -> anyhow::Result<()>; | ||
|
|
||
| /// Pick a workload ID based on the incoming request | ||
| /// Route an incoming request to a specific (workload, component) pair |
There was a problem hiding this comment.
This is (almost) the name of the function. These kind of comments just gives more to read while adding nothing (in my opinion)
crates/wash-runtime/src/host/http.rs
Outdated
| if routes.is_empty() { | ||
| let mut router_guard = self.router.write().map_err(|e| anyhow::anyhow!("{e}"))?; | ||
| *router_guard = None; | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
I think this is unnecessary, lets just create an empty router if there are no routes
crates/wash-runtime/src/host/http.rs
Outdated
| let mut routes = self.routes.write().await; | ||
| if let Some(existing) = routes.get(&path_pattern) { | ||
| tracing::warn!( | ||
| path = %path_pattern, | ||
| existing_workload = %existing.workload_id, | ||
| existing_component = %existing.component_id, | ||
| new_workload = %workload_id, | ||
| new_component = %component_id, | ||
| "path pattern already registered — overwriting previous route" | ||
| ); | ||
| } | ||
| routes.insert(path_pattern, target); | ||
| } |
There was a problem hiding this comment.
I dont think we want to overwrite, I think we want to error when it already exists. Although I am not sure if we can do that, because probably a new workload is (trying) to be started and only when it is successfull it will delete the old one?
Any ideas / thoughts on this?
There was a problem hiding this comment.
In the scenario of a running workload, as you correctly pointed out : erroring is the correct choice.
about your concern icw the new workload starting before the old one is deleted : the old workload is always stopped first, which clears all de component-> path registrations. Only then the new workload starts and registers its paths. So till the time the new workload resolves, the paths will be free.
|
|
||
| for component_id in http_component_ids { | ||
| if let Err(e) = http_handler | ||
| .on_workload_resolved(&resolved_workload, &component_id) |
There was a problem hiding this comment.
We now call on_workload_resolved multiple times, which confused me when I was reading the on_workload_resolved implementation.
I think it would be clearer that we call on_workload_resolved once and then have the logic of adding the logic to determine which paths for which components need to be added completely handled there.
crates/wash-runtime/src/host/http.rs
Outdated
| let Some(router) = lock.as_ref() else { | ||
| anyhow::bail!("No routes configured yet"); | ||
| }; |
There was a problem hiding this comment.
Let's create a default empty router, and then rebuild will also just return an empty router and then we can either specify the error more if we have no match for the path and if the routes are empty that the log is no routes configured yet and otherwise no route found for path.
| let engine = Engine::builder().build()?; | ||
|
|
||
| let port = find_available_port().await?; | ||
| let addr: SocketAddr = format!("127.0.0.1:{port}").parse().unwrap(); | ||
| let http_handler = PathRouter::default(); | ||
| let http_plugin = HttpServer::new(http_handler, addr); | ||
|
|
||
| let logging_plugin = WasiLogging {}; | ||
|
|
||
| let host = HostBuilder::new() | ||
| .with_engine(engine.clone()) | ||
| .with_http_handler(Arc::new(http_plugin)) | ||
| .with_plugin(Arc::new(logging_plugin))? | ||
| .build()?; |
There was a problem hiding this comment.
Let's create a test helper for that, because it is not interesting to read for each test
| } | ||
| } | ||
|
|
||
| // Convert per-component interface config (Option C) |
c45a8ed to
a87b7ca
Compare
Signed-off-by: Aditya <aditya.salunkh919@gmail.com>
a87b7ca to
37b2e36
Compare
interface_config vs Named Interfaces: Path-Based Routing
The Problem
A workload contains multiple components that each need to handle different HTTP paths. The runtime needs to route incoming requests to the correct component based on the URL path.
Two approaches exist for passing config to plugins: Named Interfaces (upstream wasmCloud PR) and interface_config (per-component config). This document explains why
interface_configis the right fit for path-based routing.The Two Approaches
Named Interfaces (Workload-Level)
Config lives on
hostInterfacesat the workload level. Each named entry represents a separate instance of the same interface type. The component selects which instance to use via an identifier (e.g.store::open("cache")).interface_config (Per-Component)
Config lives on each component. Different components declare their own config for the same interface type. The plugin reads per-component config during binding and routes accordingly.
Why interface_config Is Better for Path Routing
1. Direction of Control
The fundamental difference is who initiates the call:
store::open("cache")Path routing is inherently runtime-to-component: a request arrives, the runtime examines the path, and forwards it to the right component. The component doesn't choose — it just handles what it receives.
Named interfaces are designed for the opposite direction — the component explicitly selects which backend instance to use. There's no equivalent of
store::open("name")forincoming-handlerbecause the component doesn't initiate incoming requests.2. Config Granularity
Named interfaces attach config at the workload level — shared across all components. But path routing needs config at the component level — each component declares its own path.
With named interfaces, you'd need a separate named entry per path, then an additional mapping from name → component. That mapping doesn't exist in the spec today.
With
interface_config, the mapping is implicit: each component declares its own path. No extra indirection.3. Manifest Clarity
Named interfaces for path routing would look awkward:
You'd need a
target-componentfield that doesn't exist in the spec. This breaks the named interfaces model, where the component picks the instance — not the other way around. If a host interface points to more than one component for a single-use resource likeincoming-handler, the linkage fails because two components can't both listen on the same endpoint.With
interface_config, the intent is clear: the config sits right next to the component it belongs to. The plugin builds a straight path → component lookup. Named interfaces would add an unnecessary name → path → component indirection.When to Use Which
Named Interfaces: Same component needs multiple instances of the same interface (multi-backend).
interface_config: Different components need different config for the same interface (per-component routing).
They solve different problems and can coexist.