Skip to content

Commit

Permalink
feat(shuttle-next): first edition of axum-wasm router (#472)
Browse files Browse the repository at this point in the history
* feat(shuttle-next): POC of axum wasm router

* feat: use std mutex

* feat: serialize/deserialize http requests to rmp

this serializes/deserializes http requests across the ffi to the axum-wasm router, using the rust messagepack dataformat. sending the body is still a wip

* feat: serialize the full HTTP req/res to rmp

* fix: comment typo

* feat: start hyper with tonic and serve wasm router

* feat: clone inner router arc

* feat: send request body without serialization

* docs: todo comment

* feat: write response body

* fix: serialize response parts

* feat: deserialize parts directly from reader

this also adds a new fd to separate streaming of body and parts

* feat: add axum-wasm to runtime cli

* refactor: remove unused method

* refactor: typo

Co-authored-by: Pieter <[email protected]>

* refactor: comments, clean up wrappers

* refactor: move axum-wasm utils to shuttle-common

* refactor: fmt

* refactor: clippy

Co-authored-by: Pieter <[email protected]>
  • Loading branch information
oddgrd and chesedo authored Nov 21, 2022
1 parent ee342e4 commit 019764e
Show file tree
Hide file tree
Showing 13 changed files with 767 additions and 65 deletions.
148 changes: 93 additions & 55 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ chrono = { version = "0.4.22", features = ["serde"] }
comfy-table = { version = "6.1.0", optional = true }
crossterm = { version = "0.25.0", optional = true }
http = { version = "0.2.8", optional = true }
http-serde = { version = "1.1.2", optional = true }
hyper = { version = "0.14.23", optional = true }
once_cell = "1.13.1"
rmp-serde = { version = "1.1.1", optional = true }
rustrict = "0.5.0"
serde = { version = "1.0.143", features = ["derive"] }
serde_json = { version = "1.0.85", optional = true }
Expand All @@ -24,3 +27,4 @@ default = ["models"]

models = ["display", "serde_json", "http"]
display = ["comfy-table", "crossterm"]
axum-wasm = ["http-serde", "hyper", "rmp-serde"]
2 changes: 2 additions & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ pub mod log;
#[cfg(feature = "models")]
pub mod models;
pub mod project;
#[cfg(feature = "axum-wasm")]
pub mod wasm;

use serde::{Deserialize, Serialize};
use uuid::Uuid;
Expand Down
160 changes: 160 additions & 0 deletions common/src/wasm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use hyper::http::{HeaderMap, Method, Request, Response, StatusCode, Uri, Version};
use rmps::Serializer;
use serde::{Deserialize, Serialize};

extern crate rmp_serde as rmps;

// todo: add http extensions field
#[derive(Serialize, Deserialize, Debug)]
pub struct RequestWrapper {
#[serde(with = "http_serde::method")]
pub method: Method,

#[serde(with = "http_serde::uri")]
pub uri: Uri,

#[serde(with = "http_serde::version")]
pub version: Version,

#[serde(with = "http_serde::header_map")]
pub headers: HeaderMap,
}

impl From<hyper::http::request::Parts> for RequestWrapper {
fn from(parts: hyper::http::request::Parts) -> Self {
RequestWrapper {
method: parts.method,
uri: parts.uri,
version: parts.version,
headers: parts.headers,
}
}
}

impl RequestWrapper {
/// Serialize a RequestWrapper to the Rust MessagePack data format
pub fn into_rmp(self) -> Vec<u8> {
let mut buf = Vec::new();
self.serialize(&mut Serializer::new(&mut buf)).unwrap();

buf
}

/// Consume the wrapper and return a request builder with `Parts` set
pub fn into_request_builder(self) -> hyper::http::request::Builder {
let mut request = Request::builder()
.method(self.method)
.version(self.version)
.uri(self.uri);

request
.headers_mut()
.unwrap()
.extend(self.headers.into_iter());

request
}
}

// todo: add http extensions field
#[derive(Serialize, Deserialize, Debug)]
pub struct ResponseWrapper {
#[serde(with = "http_serde::status_code")]
pub status: StatusCode,

#[serde(with = "http_serde::version")]
pub version: Version,

#[serde(with = "http_serde::header_map")]
pub headers: HeaderMap,
}

impl From<hyper::http::response::Parts> for ResponseWrapper {
fn from(parts: hyper::http::response::Parts) -> Self {
ResponseWrapper {
status: parts.status,
version: parts.version,
headers: parts.headers,
}
}
}

impl ResponseWrapper {
/// Serialize a ResponseWrapper into the Rust MessagePack data format
pub fn into_rmp(self) -> Vec<u8> {
let mut buf = Vec::new();
self.serialize(&mut Serializer::new(&mut buf)).unwrap();

buf
}

/// Consume the wrapper and return a response builder with `Parts` set
pub fn into_response_builder(self) -> hyper::http::response::Builder {
let mut response = Response::builder()
.status(self.status)
.version(self.version);

response
.headers_mut()
.unwrap()
.extend(self.headers.into_iter());

response
}
}

#[cfg(test)]
mod test {
use super::*;
use hyper::body::Body;
use hyper::http::HeaderValue;

#[test]
fn request_roundtrip() {
let request: Request<Body> = Request::builder()
.method(Method::PUT)
.version(Version::HTTP_11)
.header("test", HeaderValue::from_static("request"))
.uri(format!("https://axum-wasm.example/hello"))
.body(Body::empty())
.unwrap();

let (parts, _) = request.into_parts();
let rmp = RequestWrapper::from(parts).into_rmp();

let back: RequestWrapper = rmps::from_slice(&rmp).unwrap();

assert_eq!(
back.headers.get("test").unwrap(),
HeaderValue::from_static("request")
);
assert_eq!(back.method, Method::PUT);
assert_eq!(back.version, Version::HTTP_11);
assert_eq!(
back.uri.to_string(),
"https://axum-wasm.example/hello".to_string()
);
}

#[test]
fn response_roundtrip() {
let response: Response<Body> = Response::builder()
.version(Version::HTTP_11)
.header("test", HeaderValue::from_static("response"))
.status(StatusCode::NOT_MODIFIED)
.body(Body::empty())
.unwrap();

let (parts, _) = response.into_parts();
let rmp = ResponseWrapper::from(parts).into_rmp();

let back: ResponseWrapper = rmps::from_slice(&rmp).unwrap();

assert_eq!(
back.headers.get("test").unwrap(),
HeaderValue::from_static("response")
);
assert_eq!(back.status, StatusCode::NOT_MODIFIED);
assert_eq!(back.version, Version::HTTP_11);
}
}
6 changes: 6 additions & 0 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ anyhow = "1.0.62"
async-trait = "0.1.58"
cap-std = "0.26.0"
clap ={ version = "4.0.18", features = ["derive"] }
hyper = { version = "0.14.23", features = ["full"] }
rmp-serde = { version = "1.1.1" }
serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
thiserror = "1.0.37"
tokio = { version = "=1.20.1", features = ["full"] }
Expand All @@ -35,3 +37,7 @@ version = "0.7.0"
default-features = false
features = ["loader"]
path = "../service"

[features]
shuttle-axum = ["shuttle-common/axum-wasm"]

4 changes: 4 additions & 0 deletions runtime/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ wasm:
cd ../tmp/wasm; cargo build --target wasm32-wasi
cp ../tmp/wasm/target/wasm32-wasi/debug/shuttle_serenity.wasm bot.wasm

axum:
cd ../tmp/axum-wasm; cargo build --target wasm32-wasi
cp ../tmp/axum-wasm/target/wasm32-wasi/debug/shuttle_axum.wasm axum.wasm

test: wasm
cargo test -- --nocapture

Expand Down
55 changes: 47 additions & 8 deletions runtime/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## How to run
# How to run

## shuttle-next
```bash
$ make wasm
$ DISCORD_TOKEN=xxx cargo run
Expand All @@ -8,9 +9,47 @@ $ DISCORD_TOKEN=xxx cargo run
In another terminal:

``` bash
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/bot.wasm"}' localhost:8000 runtime.Runtime/Load
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/Start
grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:8000 runtime.Runtime/SubscribeLogs
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/bot.wasm"}' localhost:6001 runtime.Runtime/Load
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start
grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs
```

## axum-wasm

Compile the wasm axum router:

```bash
make axum
```

Run the test:

```bash
cargo test axum --features shuttle-axum -- --nocapture
```

Load and run:

```bash
cargo run --features shuttle-axum -- --axum --provisioner-address http://localhost:8000
```

In another terminal:

``` bash
# a full, absolute path from home was needed for me in the load request
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "runtime/axum.wasm"}' localhost:6001 runtime.Runtime/Load

grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start

# grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs
```

Curl the service:
```bash
curl localhost:7002/hello

curl localhost:7002/goodbye
```
## shuttle-legacy

Expand All @@ -33,16 +72,16 @@ Or directly (this is the path hardcoded in `deployer::start`):
# first, make sure the shuttle-runtime binary is built
cargo build
# then
/home/<path to shuttle repo>/target/debug/shuttle-runtime --legacy --provisioner-address http://localhost:8000
/home/<path to shuttle repo>/target/debug/shuttle-runtime --legacy --provisioner-address http://localhost:6001
```

Pass the path to `deployer::start`
Then in another shell, load a `.so` file and start it up:

``` bash
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "examples/rocket/hello-world/target/debug/libhello_world.so"}' localhost:8000 runtime.Runtime/Load
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/Start
grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:8000 runtime.Runtime/SubscribeLogs
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "examples/rocket/hello-world/target/debug/libhello_world.so"}' localhost:6001 runtime.Runtime/Load
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Start
grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs
```

## Running the tests
Expand Down
6 changes: 5 additions & 1 deletion runtime/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ pub struct Args {
pub provisioner_address: Endpoint,

/// Is this runtime for a legacy service
#[clap(long)]
#[clap(long, conflicts_with("axum"))]
pub legacy: bool,

/// Is this runtime for an axum-wasm service
#[clap(long, conflicts_with("legacy"))]
pub axum: bool,
}
Loading

0 comments on commit 019764e

Please sign in to comment.