Skip to content

Commit

Permalink
wasm-compose: simplify the example. (#800)
Browse files Browse the repository at this point in the history
This commit simplifies the example by removing an unnecessary service layer
from the compose component.

Now the example either directly talks to a "service" component or composes a
new service component that injects a "middleware" component into the execution
graph.

It also updates the server in the example to Wasmtime 2.0.0.
  • Loading branch information
peterhuene committed Oct 21, 2022
1 parent 1fecc98 commit 5e8639a
Show file tree
Hide file tree
Showing 10 changed files with 52 additions and 159 deletions.
102 changes: 27 additions & 75 deletions crates/wasm-compose/example/README.md
Expand Up @@ -5,21 +5,18 @@ to compose a component from other components.

## Directory layout

There are four subdirectories in this example:
There are three subdirectories in this example:

* `backend` - a simple HTTP backend component that responds with the original
request body.
* `middleware` - a middleware component that gzip compresses response bodies.
* `service` - a service component that is configured with a backend component
to send requests to.
* `server` - a custom HTTP server that instantiates a composed `service`
component for each HTTP request.
* `service` - a service component that responds with the original request body.
* `middleware` - a middleware component that compresses response bodies.
* `server` - a custom HTTP server that instantiates a service component for
each HTTP request.

## Overview

The server will listen for `POST` requests at `http://localhost:8080`.

When it receives a request, the server will instantiate the `service` component
When it receives a request, the server will instantiate a service component
and forward it the request.

Each component implements a `service` interface defined in `service.wit` as:
Expand Down Expand Up @@ -50,18 +47,15 @@ HTTP request processing.

### Execution flow

The example will compose a `service` component that initially executes like
The example implements a `service` component that initially executes like
this:

```mermaid
sequenceDiagram
participant server (native)
participant service (wasm)
participant backend (wasm)
server (native)->>service (wasm): execute(<request>)
service (wasm)->>backend (wasm): execute(<request>)
Note right of backend (wasm): echo request body
backend (wasm)->>service (wasm): uncompressed response
Note right of service (wasm): echo request body
service (wasm)->>server (native): uncompressed response
```

Expand All @@ -72,16 +66,13 @@ altering the execution flow to this:
```mermaid
sequenceDiagram
participant server (native)
participant service (wasm)
participant middleware (wasm)
participant backend (wasm)
server (native)->>service (wasm): execute(<request>)
service (wasm)->>middleware (wasm): execute(<request>)
middleware (wasm)->>backend (wasm): execute(<request>)
Note right of backend (wasm): echo request body
backend (wasm)->>middleware (wasm): uncompressed response
middleware (wasm)->>service (wasm): compressed response
service (wasm)->>server (native): compressed response
participant service (wasm)
server (native)->>middleware (wasm): execute(<request>)
middleware (wasm)->>service (wasm): execute(<request>)
Note right of service (wasm): echo request body
service (wasm)->>middleware (wasm): uncompressed response
middleware (wasm)->>server (native): compressed response
```

All this without having to rebuild any of the original components!
Expand All @@ -98,10 +89,10 @@ root of this repository.

## Building the components

To build the `backend` component, use `cargo component build`:
To build the `service` component, use `cargo component build`:

```sh
cd backend
cd service
cargo component build --release
```

Expand All @@ -112,41 +103,16 @@ cd middleware
cargo component build --release
```

Finally, to build the `service` component, use `cargo component build`:

```sh
cd service
cargo component build --release
```

## Composing the `service` component

Initially, we will compose a service component that directly sends requests
to the backend service.

The `server/config.yml` configuration file instructs `wasm-compose` to
search for dependencies from the expected output paths of the components we
built previously.

Based on this, `wasm-compose` will automatically satisfy the `backend`
dependency of the `service` component with the `backend` component.

To compose the `service` component, run `wasm-tools compose`:

```sh
cd server
wasm-tools compose -c config.yml -o service.wasm ../service/target/wasm32-unknown-unknown/release/svc.wasm
```

There should now be a `service.wasm` in the `server` directory.

## Running the server

Initially, we will run the server with the `service` component that responds
with an uncompressed response body.

The server can be run with `cargo run`:

```sh
cd server
cargo run --release -- service.wasm
cargo run --release -- ../service/target/wasm32-unknown-unknown/release/svc.wasm
```

This will start a HTTP server that listens at `http://localhost:8080`.
Expand Down Expand Up @@ -186,34 +152,20 @@ The request body was: Hello, world!
Note that the response body matches the request body, but it was not
compressed.

## Changing the composition
## Composing with a middleware

If we want to instead compress the response bodies for the service, we can easily
change the composition to send requests through the `middleware` component
compose a new component that sends requests through the `middleware` component
without rebuilding any of the previously built components.

To change how the `service` component is composed, edit `server/config.yml`
and uncomment the specified lines:

```yml
instantiations:
$component:
arguments:
backend: middleware
```

This provides an explicit dependency of `middleware` for the `backend` argument
of the composed component.

`wasm-compose` will therefore use the `middleware` component to satisfy the
dependency; the `backend` dependency of the `middleware` component will automatically
be satisfied by the `backend` component.
The `server/config.yml` file contains the configuration needed to compose a new
component from the `service` and `middleware` components.

And run `wasm-compose` again:
Run `wasm-compose` to compose the new component:

```sh
cd server
wasm-tools compose -c config.yml -o service.wasm ../service/target/wasm32-unknown-unknown/release/svc.wasm
wasm-tools compose -c config.yml -o service.wasm ../middleware/target/wasm32-unknown-unknown/release/middleware.wasm
```

This results in a new `service.wasm` in the `server` directory where the
Expand All @@ -223,7 +175,7 @@ This results in a new `service.wasm` in the `server` directory where the

If you haven't already, stop the currently running server by pressing `ctrl-c`.

Start the server again with `cargo run`:
Start the server again with `cargo run` with the newly composed component:

```sh
cd server
Expand Down
3 changes: 0 additions & 3 deletions crates/wasm-compose/example/backend/.vscode/settings.json

This file was deleted.

19 changes: 0 additions & 19 deletions crates/wasm-compose/example/backend/Cargo.toml

This file was deleted.

30 changes: 0 additions & 30 deletions crates/wasm-compose/example/backend/src/lib.rs

This file was deleted.

2 changes: 1 addition & 1 deletion crates/wasm-compose/example/server/Cargo.toml
Expand Up @@ -9,6 +9,6 @@ async-std = { version = "1.12.0", features = ["attributes"] }
clap = { version = "3.2.16", features = ["derive"] }
driftwood = "0.0.6"
tide = "0.16.0"
wasmtime = { features = ["component-model"], git = "https://github.com/bytecodealliance/wasmtime", rev = "e45577e097b064797def3554468cffa5fdd443d3" }
wasmtime = { version = "2.0.0", features = ["component-model"] }

[workspace]
12 changes: 5 additions & 7 deletions crates/wasm-compose/example/server/config.yml
@@ -1,9 +1,7 @@
search-paths:
- ../backend/target/wasm32-unknown-unknown/release
- ../middleware/target/wasm32-unknown-unknown/release
- ../service/target/wasm32-unknown-unknown/release

# Remove the comments below to compose with the middleware component
# instantiations:
# $component:
# arguments:
# backend: middleware
instantiations:
$component:
arguments:
backend: svc
5 changes: 3 additions & 2 deletions crates/wasm-compose/example/server/src/main.rs
Expand Up @@ -154,12 +154,13 @@ impl ServerApp {
// Instantiate the service component and get its `execute` export
let instance = linker.instantiate(&mut store, &state.component)?;
let execute = instance
.get_typed_func::<(ServiceRequest,), (ServiceResult,), _ >(&mut store, "execute")?;
.get_typed_func::<(ServiceRequest,), (ServiceResult,), _>(&mut store, "execute")?;

// Call the `execute` export with the request and translate the response
execute
.call(&mut store, (ServiceRequest::new(req).await?,))?
.0.map_err(Into::into)
.0
.map_err(Into::into)
.and_then(TryInto::try_into)
}
}
Expand Down
1 change: 0 additions & 1 deletion crates/wasm-compose/example/service/.gitignore

This file was deleted.

3 changes: 0 additions & 3 deletions crates/wasm-compose/example/service/Cargo.toml
Expand Up @@ -13,9 +13,6 @@ crate-type = ["cdylib"]
[package.metadata.component]
direct-interface-export = "service"

[package.metadata.component.imports]
backend = "../service.wit"

[package.metadata.component.exports]
service = "../service.wit"

Expand Down
34 changes: 16 additions & 18 deletions crates/wasm-compose/example/service/src/lib.rs
@@ -1,30 +1,28 @@
use service::{Error, Request, Response, Service};
use std::str;

struct Component;

impl Service for Component {
fn execute(req: Request) -> Result<Response, Error> {
// Right now, generated types aren't shared for bindings, so we
// have to manually convert between the different types; eventually
// this will not be necessary and we can call `backend::execute`
// with the request passed to us.
let headers: Vec<_> = req
// The content should be plain text
let content_type = req
.headers
.iter()
.map(|(k, v)| (k.as_slice(), v.as_slice()))
.collect();
.find(|(k, _)| k == b"content-type")
.map(|(_, v)| v)
.ok_or_else(|| Error::BadRequest)?;
if content_type != b"text/plain" {
return Err(Error::BadRequest);
}

// Send the request to the backend and convert the response
backend::execute(backend::Request {
headers: &headers,
body: req.body.as_slice(),
})
.map(|r| Response {
headers: r.headers,
body: r.body,
})
.map_err(|e| match e {
backend::Error::BadRequest => Error::BadRequest,
// We assume the body is UTF-8 encoded
let body = str::from_utf8(&req.body).map_err(|_| Error::BadRequest)?;

// Echo the body back in the response
Ok(Response {
headers: vec![(b"content-type".to_vec(), b"text/plain".to_vec())],
body: format!("The request body was: {body}").into_bytes(),
})
}
}
Expand Down

0 comments on commit 5e8639a

Please sign in to comment.