From 3dc3ac76d3fc06728416eedd932fff0da4218230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Fri, 3 Mar 2023 08:52:15 +0000 Subject: [PATCH 1/9] Feature/eng 378 axum wasm multiple handlers per endpoint (#588) * refactor: clean up if let * feat: move method validation, add test for it * test: add test for chained endpoint handlers * feat: chain endpoints with the same address * feat: duplicate method error, handler chain bug fix chaining handlers with a full type path caused a bug where a `,` was inserted, I think the reason is that `path` types can't be followed by `.`. Importing all the routing methods in `app` works, but it is not ideal * refactor: remove some clones * refactor: remove unwrap Co-authored-by: Pieter * feat: use BTreeMap for building endpoint chains * fix: only add namespace to first handler in chain * fix: skip rustfmt for quote in app_to_token test rustfmt will add a comma to the expected output, which will also be done in the expanded macro output, but not in the stringified output * refactor: revert btreemap change * docs: document why we sort the endpoint chains --------- Co-authored-by: Pieter --- codegen/src/next/mod.rs | 215 ++++++++++++++---- codegen/tests/ui/next/duplicate-methods.rs | 16 ++ .../tests/ui/next/duplicate-methods.stderr | 14 ++ codegen/tests/ui/next/invalid-method.rs | 11 + codegen/tests/ui/next/invalid-method.stderr | 29 +++ 5 files changed, 237 insertions(+), 48 deletions(-) create mode 100644 codegen/tests/ui/next/duplicate-methods.rs create mode 100644 codegen/tests/ui/next/duplicate-methods.stderr create mode 100644 codegen/tests/ui/next/invalid-method.rs create mode 100644 codegen/tests/ui/next/invalid-method.stderr diff --git a/codegen/src/next/mod.rs b/codegen/src/next/mod.rs index e03cfd858d..4f62b12e1f 100644 --- a/codegen/src/next/mod.rs +++ b/codegen/src/next/mod.rs @@ -1,8 +1,10 @@ +use std::collections::HashMap; + use proc_macro_error::emit_error; use quote::{quote, ToTokens}; use syn::{ - parenthesized, parse::Parse, parse2, punctuated::Punctuated, token::Paren, Expr, File, Ident, - Item, ItemFn, Lit, LitStr, Token, + parenthesized, parse::Parse, parse2, punctuated::Punctuated, token::Paren, Expr, ExprLit, File, + Ident, Item, ItemFn, Lit, LitStr, Token, }; #[derive(Debug, Eq, PartialEq)] @@ -124,7 +126,22 @@ impl Endpoint { has_err = true; } if let Expr::Path(path) = value { - method = Some(path.path.segments[0].ident.clone()); + let method_ident = path.path.segments[0].ident.clone(); + + match method_ident.to_string().as_str() { + "get" | "post" | "delete" | "put" | "options" | "head" | "trace" + | "patch" => { + method = Some(method_ident); + } + _ => { + emit_error!( + method_ident, + "method is not supported"; + hint = "Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch`" + ); + has_err = true; + } + }; }; } "route" => { @@ -136,10 +153,13 @@ impl Endpoint { ); has_err = true; } - if let Expr::Lit(literal) = value { - if let Some(Lit::Str(literal)) = Some(literal.lit) { - route = Some(literal); - } + + if let Expr::Lit(ExprLit { + lit: Lit::Str(literal), + .. + }) = value + { + route = Some(literal); } } _ => { @@ -184,6 +204,18 @@ impl Endpoint { } } +#[derive(Debug, Eq, PartialEq)] +pub struct EndpointChain<'a> { + route: &'a LitStr, + handlers: Vec, +} + +#[derive(Debug, Eq, PartialEq)] +struct Handler { + method: Ident, + function: Ident, +} + impl ToTokens for Endpoint { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { @@ -192,18 +224,27 @@ impl ToTokens for Endpoint { function, } = self; - match method.to_string().as_str() { - "get" | "post" | "delete" | "put" | "options" | "head" | "trace" | "patch" => {} - _ => { - emit_error!( - method, - "method is not supported"; - hint = "Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch`" - ) - } - }; + let route = quote!(.route(#route, #method(#function))); + + route.to_tokens(tokens); + } +} - let route = quote!(.route(#route, shuttle_next::routing::#method(#function))); +impl ToTokens for Handler { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { method, function } = self; + + let handler = quote!(#method(#function)); + + handler.to_tokens(tokens); + } +} + +impl<'a> ToTokens for EndpointChain<'a> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Self { route, handlers } = self; + + let route = quote!(.route(#route, shuttle_next::routing::#(#handlers).*)); route.to_tokens(tokens); } @@ -237,13 +278,46 @@ impl ToTokens for App { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let Self { endpoints } = self; + let mut endpoint_chains = endpoints + .iter() + .fold(HashMap::new(), |mut chain, endpoint| { + let entry = chain + .entry(&endpoint.route) + .or_insert_with(Vec::::new); + + let method = endpoint.method.clone(); + let function = endpoint.function.clone(); + + if entry.iter().any(|handler| handler.method == method) { + emit_error!( + method, + "only one method of each type is allowed per route"; + hint = format!("Remove one of the {} methods on the \"{}\" route.", method, endpoint.route.value()) + ); + } else { + entry.push(Handler { method, function }); + } + + chain + }) + .into_iter() + .map(|(key, value)| EndpointChain { + route: key, + handlers: value, + }) + .collect::>(); + + // syn::LitStr does not implement Ord, so rather than using a BTreeMap to build the chains, we + // use a HashMap and then sort the endpoint chains to ensure the output is deterministic. + endpoint_chains.sort_by(|a, b| a.route.value().cmp(&b.route.value())); + let app = quote!( async fn __app(request: shuttle_next::Request,) -> shuttle_next::response::Response { use shuttle_next::Service; let mut router = shuttle_next::Router::new() - #(#endpoints)*; + #(#endpoint_chains)*; let response = router.call(request).await.unwrap(); @@ -341,46 +415,91 @@ mod tests { }; let actual = quote!(#endpoint); - let expected = quote!(.route("/hello", shuttle_next::routing::get(hello))); + let expected = quote!(.route("/hello", get(hello))); assert_eq!(actual.to_string(), expected.to_string()); } #[test] + #[rustfmt::skip::macros(quote)] fn app_to_token() { - let app = App { - endpoints: vec![ - Endpoint { - route: parse_quote!("/hello"), - method: parse_quote!(get), - function: parse_quote!(hello), - }, - Endpoint { - route: parse_quote!("/goodbye"), - method: parse_quote!(post), - function: parse_quote!(goodbye), + let cases = vec![ + ( + App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(post), + function: parse_quote!(goodbye), + }, + ], }, - ], - }; + quote!( + async fn __app( + request: shuttle_next::Request, + ) -> shuttle_next::response::Response { + use shuttle_next::Service; - let actual = quote!(#app); - let expected = quote!( - async fn __app( - request: shuttle_next::Request, - ) -> shuttle_next::response::Response { - use shuttle_next::Service; - - let mut router = shuttle_next::Router::new() - .route("/hello", shuttle_next::routing::get(hello)) - .route("/goodbye", shuttle_next::routing::post(goodbye)); + let mut router = shuttle_next::Router::new() + .route("/goodbye", shuttle_next::routing::post(goodbye)) + .route("/hello", shuttle_next::routing::get(hello)); - let response = router.call(request).await.unwrap(); + let response = router.call(request).await.unwrap(); - response - } - ); + response + } + ), + ), + ( + App { + endpoints: vec![ + Endpoint { + route: parse_quote!("/hello"), + method: parse_quote!(get), + function: parse_quote!(hello), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(get), + function: parse_quote!(get_goodbye), + }, + Endpoint { + route: parse_quote!("/goodbye"), + method: parse_quote!(post), + function: parse_quote!(post_goodbye), + }, + ], + }, + quote!( + async fn __app( + request: shuttle_next::Request, + ) -> shuttle_next::response::Response { + use shuttle_next::Service; + + let mut router = shuttle_next::Router::new() + .route( + "/goodbye", + shuttle_next::routing::get(get_goodbye).post(post_goodbye) + ) + .route("/hello", shuttle_next::routing::get(hello)); + + let response = router.call(request).await.unwrap(); + + response + } + ), + ), + ]; - assert_eq!(actual.to_string(), expected.to_string()); + for (app, expected) in cases { + let actual = quote!(#app); + assert_eq!(actual.to_string(), expected.to_string()); + } } #[test] diff --git a/codegen/tests/ui/next/duplicate-methods.rs b/codegen/tests/ui/next/duplicate-methods.rs new file mode 100644 index 0000000000..6c33ac070f --- /dev/null +++ b/codegen/tests/ui/next/duplicate-methods.rs @@ -0,0 +1,16 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = get, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method = post, route = "/hello")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } + + #[shuttle_codegen::endpoint(method = post, route = "/hello")] + async fn goodbye() -> &'static str { + "Goodbye, World!" + } +} diff --git a/codegen/tests/ui/next/duplicate-methods.stderr b/codegen/tests/ui/next/duplicate-methods.stderr new file mode 100644 index 0000000000..562a6b32cf --- /dev/null +++ b/codegen/tests/ui/next/duplicate-methods.stderr @@ -0,0 +1,14 @@ +error: only one method of each type is allowed per route + + = help: Remove one of the post methods on the "/hello" route. + + --> tests/ui/next/duplicate-methods.rs:12:42 + | +12 | #[shuttle_codegen::endpoint(method = post, route = "/hello")] + | ^^^^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/duplicate-methods.rs:16:2 + | +16 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/duplicate-methods.rs` diff --git a/codegen/tests/ui/next/invalid-method.rs b/codegen/tests/ui/next/invalid-method.rs new file mode 100644 index 0000000000..73f29bb876 --- /dev/null +++ b/codegen/tests/ui/next/invalid-method.rs @@ -0,0 +1,11 @@ +shuttle_codegen::app! { + #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } + + #[shuttle_codegen::endpoint(method =, route = "/hello")] + async fn hello() -> &'static str { + "Hello, World!" + } +} diff --git a/codegen/tests/ui/next/invalid-method.stderr b/codegen/tests/ui/next/invalid-method.stderr new file mode 100644 index 0000000000..fcd7acacc8 --- /dev/null +++ b/codegen/tests/ui/next/invalid-method.stderr @@ -0,0 +1,29 @@ +error: method is not supported + + = help: Try one of the following: `get`, `post`, `delete`, `put`, `options`, `head`, `trace` or `patch` + + --> tests/ui/next/invalid-method.rs:2:42 + | +2 | #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + | ^^^ + +error: no method provided + + = help: Add a method to your endpoint: `method = get` + + --> tests/ui/next/invalid-method.rs:2:32 + | +2 | #[shuttle_codegen::endpoint(method = pet, route = "/hello")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +error: expected expression + --> tests/ui/next/invalid-method.rs:7:41 + | +7 | #[shuttle_codegen::endpoint(method =, route = "/hello")] + | ^ + +error[E0601]: `main` function not found in crate `$CRATE` + --> tests/ui/next/invalid-method.rs:11:2 + | +11 | } + | ^ consider adding a `main` function to `$DIR/tests/ui/next/invalid-method.rs` From 91a9fdde5b3ba505ee2ae5642e4fa4eefce390f2 Mon Sep 17 00:00:00 2001 From: Pieter Date: Fri, 3 Mar 2023 16:47:39 +0200 Subject: [PATCH 2/9] refactor: find code to wrap legacy runtime (#675) * refactor: find code to wrap legacy runtime * misc: update README with test instructions --- Cargo.lock | 3 +- proto/runtime.proto | 9 +- resources/secrets/Cargo.toml | 1 - resources/secrets/src/lib.rs | 7 +- runtime/Cargo.toml | 10 +- runtime/README.md | 19 ++- runtime/src/axum/mod.rs | 11 +- runtime/src/bin/rocket.rs | 50 ++++++++ runtime/src/legacy/error.rs | 14 -- runtime/src/legacy/mod.rs | 241 +++++++++++++++++------------------ runtime/src/lib.rs | 5 +- service/src/lib.rs | 6 +- 12 files changed, 198 insertions(+), 178 deletions(-) create mode 100644 runtime/src/bin/rocket.rs delete mode 100644 runtime/src/legacy/error.rs diff --git a/Cargo.lock b/Cargo.lock index cb8b4de404..25def720b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6415,8 +6415,10 @@ dependencies = [ "futures", "hyper", "rmp-serde", + "rocket", "shuttle-common", "shuttle-proto", + "shuttle-secrets", "shuttle-service", "thiserror", "tokio", @@ -6436,7 +6438,6 @@ version = "0.8.0" dependencies = [ "async-trait", "shuttle-service", - "tokio", ] [[package]] diff --git a/proto/runtime.proto b/proto/runtime.proto index 8faab75891..567a3fa589 100644 --- a/proto/runtime.proto +++ b/proto/runtime.proto @@ -36,8 +36,6 @@ message LoadResponse { message StartRequest { // Id to associate with the deployment being started bytes deployment_id = 1; - // Name of service to start - string service_name = 2; // Address and port to start the service on string ip = 3; } @@ -47,12 +45,7 @@ message StartResponse { bool success = 1; } -message StopRequest { - // Id to associate with the deployment being stopped - bytes deployment_id = 1; - // Name of service to stop - string service_name = 2; -} +message StopRequest {} message StopResponse { // Was the stop successful diff --git a/resources/secrets/Cargo.toml b/resources/secrets/Cargo.toml index 13455707e6..83c107c1ca 100644 --- a/resources/secrets/Cargo.toml +++ b/resources/secrets/Cargo.toml @@ -10,4 +10,3 @@ keywords = ["shuttle-service", "secrets"] [dependencies] async-trait = "0.1.56" shuttle-service = { path = "../../service", version = "0.8.0", default-features = false } -tokio = { version = "1.19.2", features = ["rt"] } diff --git a/resources/secrets/src/lib.rs b/resources/secrets/src/lib.rs index 27ab12ff85..856ea0593f 100644 --- a/resources/secrets/src/lib.rs +++ b/resources/secrets/src/lib.rs @@ -3,7 +3,6 @@ use std::collections::BTreeMap; use async_trait::async_trait; use shuttle_service::{Error, Factory, ResourceBuilder}; -use tokio::runtime::Runtime; pub struct Secrets; @@ -14,11 +13,7 @@ impl ResourceBuilder for Secrets { Self {} } - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let secrets = factory.get_secrets().await?; Ok(SecretStore { secrets }) diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index d57b96596d..cfa96cd800 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -6,6 +6,9 @@ license.workspace = true publish = false # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "rocket" + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } @@ -25,6 +28,11 @@ wasmtime = "4.0.0" wasmtime-wasi = "4.0.0" futures = "0.3.25" +# For rocket.rs +# TODO: remove +shuttle-secrets = { path = "../resources/secrets" } +rocket = "0.5.0-rc.2" + [dependencies.shuttle-common] workspace = true features = ["wasm"] @@ -34,4 +42,4 @@ workspace = true [dependencies.shuttle-service] workspace = true -features = ["loader"] +features = ["loader", "web-rocket"] # TODO: remove web-rocket diff --git a/runtime/README.md b/runtime/README.md index a3b0c40fbb..5e1f1d910c 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -57,8 +57,8 @@ curl localhost:7002/goodbye ``` ## shuttle-legacy - -Load and run an .so library that implements `shuttle_service::Service`. +This will no loger load a `.so` will the code to start the runtime will be codegened for all services. +An example can be found in `src/bin/rocket.rs` which contains the secrets rocket example at the bottom and the codegen at the top. To test, first start a provisioner from the root directory using: @@ -66,10 +66,10 @@ To test, first start a provisioner from the root directory using: docker-compose -f docker-compose.rendered.yml up provisioner ``` -Then in another shell, start the runtime using the clap CLI: +Then in another shell, start the wrapped runtime using the clap CLI: ```bash -cargo run -- --legacy --provisioner-address http://localhost:5000 +cargo run -- --port 6001 --storage-manager-type working-dir --storage-manager-path ./ ``` Or directly (this is the path hardcoded in `deployer::start`): @@ -77,24 +77,23 @@ Or directly (this is the path hardcoded in `deployer::start`): # first, make sure the shuttle-runtime binary is built cargo build # then -/home//target/debug/shuttle-runtime --legacy --provisioner-address http://localhost:5000 +/home//target/debug/shuttle-runtime --port 6001 --storage-manager-type working-dir --storage-manager-path ./ ``` -Pass the path to `deployer::start` -Then in another shell, load a `.so` file and start it up: +Then in another shell, load the service and start it up: ``` bash # load -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "/home//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", "path": "/home//examples/rocket/hello-world/target/debug/libhello_world.so", "secrets": {"MY_API_KEY": "test"}}' localhost:6001 runtime.Runtime/Load # run (this deployment id is default uuid encoded as base64) -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw"}' localhost:6001 runtime.Runtime/Start +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start # subscribe to logs grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs # stop (the service started in the legacy runtime can't currently be stopped) -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:6001 runtime.Runtime/Stop +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{}' localhost:6001 runtime.Runtime/Stop ``` ## Running the tests diff --git a/runtime/src/axum/mod.rs b/runtime/src/axum/mod.rs index 15b3cf0c79..5bc7989640 100644 --- a/runtime/src/axum/mod.rs +++ b/runtime/src/axum/mod.rs @@ -20,7 +20,6 @@ use shuttle_proto::runtime::{ self, LoadRequest, LoadResponse, StartRequest, StartResponse, StopRequest, StopResponse, SubscribeLogsRequest, }; -use shuttle_service::ServiceName; use tokio::sync::mpsc::{Receiver, Sender}; use tokio::sync::{mpsc, oneshot}; use tokio_stream::wrappers::ReceiverStream; @@ -149,18 +148,12 @@ impl Runtime for AxumWasm { &self, request: tonic::Request, ) -> Result, Status> { - let request = request.into_inner(); - - let service_name = ServiceName::from_str(request.service_name.as_str()) - .map_err(|err| Status::from_error(Box::new(err)))?; + let _request = request.into_inner(); let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx - .send(format!("stopping deployment: {}", &service_name)) - .is_err() - { + if kill_tx.send(format!("stopping deployment")).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } diff --git a/runtime/src/bin/rocket.rs b/runtime/src/bin/rocket.rs new file mode 100644 index 0000000000..8da00a54f7 --- /dev/null +++ b/runtime/src/bin/rocket.rs @@ -0,0 +1,50 @@ +// The few line below is what we should now codegen for legacy +#[tokio::main] +async fn main() { + shuttle_runtime::start(loader).await; +} + +async fn loader( + mut factory: shuttle_runtime::ProvisionerFactory, +) -> shuttle_service::ShuttleRocket { + use shuttle_service::ResourceBuilder; + + let secrets = shuttle_secrets::Secrets::new().build(&mut factory).await?; + + rocket(secrets).await +} + +// Everything below this is the usual code a user will write +use anyhow::anyhow; +use rocket::response::status::BadRequest; +use rocket::State; +use shuttle_secrets::SecretStore; + +#[rocket::get("/secret")] +async fn secret(state: &State) -> Result> { + Ok(state.secret.clone()) +} + +struct MyState { + secret: String, +} + +// #[shuttle_service::main] +pub async fn rocket( + // #[shuttle_secrets::Secrets] secret_store: SecretStore, + secret_store: SecretStore, +) -> shuttle_service::ShuttleRocket { + // get secret defined in `Secrets.toml` file. + let secret = if let Some(secret) = secret_store.get("MY_API_KEY") { + secret + } else { + return Err(anyhow!("secret was not found").into()); + }; + + let state = MyState { secret }; + let rocket = rocket::build() + .mount("/", rocket::routes![secret]) + .manage(state); + + Ok(rocket) +} diff --git a/runtime/src/legacy/error.rs b/runtime/src/legacy/error.rs deleted file mode 100644 index 9c57cd4e2d..0000000000 --- a/runtime/src/legacy/error.rs +++ /dev/null @@ -1,14 +0,0 @@ -use shuttle_service::loader::LoaderError; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Load error: {0}")] - Load(#[from] LoaderError), - #[error("Run error: {0}")] - Run(#[from] shuttle_service::Error), - #[error("Start error: {0}")] - Start(#[from] shuttle_service::error::CustomError), -} - -pub type Result = std::result::Result; diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index 137e7a369f..d5fe5ab8b0 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -1,141 +1,147 @@ use std::{ - collections::BTreeMap, iter::FromIterator, net::SocketAddr, ops::DerefMut, path::PathBuf, - str::FromStr, sync::Mutex, + collections::BTreeMap, + iter::FromIterator, + net::{Ipv4Addr, SocketAddr}, + ops::DerefMut, + str::FromStr, + sync::Mutex, + time::Duration, }; -use anyhow::{anyhow, Context}; +use anyhow::Context; use async_trait::async_trait; -use shuttle_common::{storage_manager::StorageManager, LogItem}; +use clap::Parser; +use futures::Future; +use shuttle_common::{ + storage_manager::{StorageManager, WorkingDirStorageManager}, + LogItem, +}; use shuttle_proto::{ provisioner::provisioner_client::ProvisionerClient, runtime::{ - self, runtime_server::Runtime, LoadRequest, LoadResponse, StartRequest, StartResponse, - StopRequest, StopResponse, SubscribeLogsRequest, + self, + runtime_server::{Runtime, RuntimeServer}, + LoadRequest, LoadResponse, StartRequest, StartResponse, StopRequest, StopResponse, + SubscribeLogsRequest, }, }; -use shuttle_service::{ - loader::{LoadedService, Loader}, - Factory, Logger, ServiceName, -}; +use shuttle_service::{Factory, Service, ServiceName}; use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender}; use tokio::sync::oneshot; use tokio_stream::wrappers::ReceiverStream; -use tonic::{transport::Endpoint, Request, Response, Status}; +use tonic::{ + transport::{Endpoint, Server}, + Request, Response, Status, +}; use tracing::{error, instrument, trace}; use uuid::Uuid; -use crate::provisioner_factory::ProvisionerFactory; +use crate::{provisioner_factory::ProvisionerFactory, Args}; -mod error; +pub async fn start( + loader: impl Loader> + Send + 'static, +) { + let args = Args::parse(); + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), args.port); -pub struct Legacy -where - S: StorageManager, -{ + let provisioner_address = args.provisioner_address; + let mut server_builder = + Server::builder().http2_keepalive_interval(Some(Duration::from_secs(60))); + + let router = { + let legacy = Legacy::new( + provisioner_address, + loader, + WorkingDirStorageManager::new(args.storage_manager_path), + ); + + let svc = RuntimeServer::new(legacy); + server_builder.add_service(svc) + }; + + router.serve(addr).await.unwrap(); +} + +pub struct Legacy { // Mutexes are for interior mutability - so_path: Mutex>, logs_rx: Mutex>>, logs_tx: UnboundedSender, provisioner_address: Endpoint, kill_tx: Mutex>>, - secrets: Mutex>>, - storage_manager: S, + storage_manager: M, + loader: Mutex>, + service: Mutex>, } -impl Legacy -where - S: StorageManager, -{ - pub fn new(provisioner_address: Endpoint, storage_manager: S) -> Self { +impl Legacy { + pub fn new(provisioner_address: Endpoint, loader: L, storage_manager: M) -> Self { let (tx, rx) = mpsc::unbounded_channel(); Self { - so_path: Mutex::new(None), logs_rx: Mutex::new(Some(rx)), logs_tx: tx, kill_tx: Mutex::new(None), provisioner_address, - secrets: Mutex::new(None), storage_manager, + loader: Mutex::new(Some(loader)), + service: Mutex::new(None), } } } #[async_trait] -impl Runtime for Legacy +pub trait Loader where - S: StorageManager + 'static, + Fac: Factory, { - async fn load(&self, request: Request) -> Result, Status> { - let LoadRequest { path, secrets, .. } = request.into_inner(); - trace!(path, "loading"); - - let so_path = PathBuf::from(path); - - if !so_path.exists() { - return Err(Status::not_found("'.so' to load does not exist")); - } + type Service: Service; - *self.so_path.lock().unwrap() = Some(so_path); + async fn load(self, factory: Fac) -> Result; +} - *self.secrets.lock().unwrap() = Some(BTreeMap::from_iter(secrets.into_iter())); +#[async_trait] +impl Loader for F +where + F: FnOnce(Fac) -> O + Send, + O: Future> + Send, + Fac: Factory + 'static, + S: Service, +{ + type Service = S; - let message = LoadResponse { success: true }; - Ok(Response::new(message)) + async fn load(self, factory: Fac) -> Result { + (self)(factory).await } +} - async fn start( - &self, - request: Request, - ) -> Result, Status> { - trace!("legacy starting"); +#[async_trait] +impl Runtime for Legacy +where + M: StorageManager + Send + Sync + 'static, + L: Loader, Service = S> + Send + 'static, + S: Service + Send + 'static, +{ + async fn load(&self, request: Request) -> Result, Status> { + let LoadRequest { + path, + secrets, + service_name, + } = request.into_inner(); + trace!(path, "loading"); + + let secrets = BTreeMap::from_iter(secrets.into_iter()); let provisioner_client = ProvisionerClient::connect(self.provisioner_address.clone()) .await .context("failed to connect to provisioner") .map_err(|err| Status::internal(err.to_string()))?; - let so_path = self - .so_path - .lock() - .unwrap() - .as_ref() - .ok_or_else(|| -> error::Error { - error::Error::Start(anyhow!("trying to start a service that was not loaded")) - }) - .map_err(|err| Status::from_error(Box::new(err)))? - .clone(); - let secrets = self - .secrets - .lock() - .unwrap() - .as_ref() - .ok_or_else(|| -> error::Error { - error::Error::Start(anyhow!( - "trying to get secrets from a service that was not loaded" - )) - }) - .map_err(|err| Status::from_error(Box::new(err)))? - .clone(); - - trace!("prepare done"); - - let StartRequest { - deployment_id, - service_name, - ip, - } = request.into_inner(); - let service_address = SocketAddr::from_str(&ip) - .context("invalid socket address") - .map_err(|err| Status::invalid_argument(err.to_string()))?; - let service_name = ServiceName::from_str(service_name.as_str()) .map_err(|err| Status::from_error(Box::new(err)))?; - let deployment_id = Uuid::from_slice(&deployment_id) - .map_err(|error| Status::invalid_argument(error.to_string()))?; + let deployment_id = Uuid::new_v4(); - let mut factory = ProvisionerFactory::new( + let factory = ProvisionerFactory::new( provisioner_client, service_name, deployment_id, @@ -144,17 +150,34 @@ where ); trace!("got factory"); - let logs_tx = self.logs_tx.clone(); + let loader = self.loader.lock().unwrap().deref_mut().take().unwrap(); - let logger = Logger::new(logs_tx, deployment_id); + let service = loader.load(factory).await.unwrap(); + + *self.service.lock().unwrap() = Some(service); + + let message = LoadResponse { success: true }; + Ok(Response::new(message)) + } + + async fn start( + &self, + request: Request, + ) -> Result, Status> { + trace!("legacy starting"); + let service = self.service.lock().unwrap().deref_mut().take(); + let service = service.unwrap(); + + let StartRequest { ip, .. } = request.into_inner(); + let service_address = SocketAddr::from_str(&ip) + .context("invalid socket address") + .map_err(|err| Status::invalid_argument(err.to_string()))?; + + let _logs_tx = self.logs_tx.clone(); trace!(%service_address, "starting"); - let service = load_service(service_address, so_path, &mut factory, logger) - .await - .map_err(|error| Status::internal(error.to_string()))?; let (kill_tx, kill_rx) = tokio::sync::oneshot::channel(); - *self.kill_tx.lock().unwrap() = Some(kill_tx); // start service as a background task with a kill receiver @@ -189,21 +212,11 @@ where } } - // todo: this doesn't currently stop the service, since we can't stop the tokio runtime it - // is started on. - async fn stop(&self, request: Request) -> Result, Status> { - let request = request.into_inner(); - - let service_name = ServiceName::from_str(request.service_name.as_str()) - .map_err(|err| Status::from_error(Box::new(err)))?; - + async fn stop(&self, _request: Request) -> Result, Status> { let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx - .send(format!("stopping deployment: {}", &service_name)) - .is_err() - { + if kill_tx.send(format!("stopping deployment")).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } @@ -218,15 +231,14 @@ where /// Run the service until a stop signal is received #[instrument(skip(service, kill_rx))] async fn run_until_stopped( - service: LoadedService, + // service: LoadedService, + service: impl Service, addr: SocketAddr, kill_rx: tokio::sync::oneshot::Receiver, ) { - let (handle, library) = service; - trace!("starting deployment on {}", &addr); tokio::select! { - _ = handle => { + _ = service.bind(addr) => { trace!("deployment stopped on {}", &addr); }, message = kill_rx => { @@ -236,21 +248,4 @@ async fn run_until_stopped( } } } - - tokio::spawn(async move { - trace!("closing .so file"); - library.close().unwrap(); - }); -} - -#[instrument(skip(addr, so_path, factory, logger))] -async fn load_service( - addr: SocketAddr, - so_path: PathBuf, - factory: &mut dyn Factory, - logger: Logger, -) -> error::Result { - let loader = Loader::from_so_file(so_path)?; - - Ok(loader.load(factory, addr, logger).await?) } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index fc17f47c44..62fedf7f31 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,8 +1,9 @@ mod args; mod axum; mod legacy; -pub mod provisioner_factory; +mod provisioner_factory; pub use args::{Args, StorageManagerType}; pub use axum::AxumWasm; -pub use legacy::Legacy; +pub use legacy::{start, Legacy}; +pub use provisioner_factory::ProvisionerFactory; diff --git a/service/src/lib.rs b/service/src/lib.rs index 2ece4b571c..b40fe8b614 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -389,7 +389,7 @@ pub trait Factory: Send + Sync { #[async_trait] pub trait ResourceBuilder { fn new() -> Self; - async fn build(self, factory: &mut dyn Factory, runtime: &Runtime) -> Result; + async fn build(self, factory: &mut dyn Factory) -> Result; } /// A tokio handle the service was started on @@ -406,7 +406,7 @@ pub trait Service: Send + Sync { /// This function is run exactly once on each instance of a deployment. /// /// The deployer expects this instance of [Service][Service] to bind to the passed [SocketAddr][SocketAddr]. - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error>; + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error>; } /// This function is generated by our codegen. It uses the factory to get other services and instantiate them on @@ -476,7 +476,7 @@ impl Drop for Bootstrapper { #[cfg(feature = "web-rocket")] #[async_trait] impl Service for rocket::Rocket { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { let shutdown = rocket::config::Shutdown { ctrlc: false, ..rocket::config::Shutdown::default() From 69b82e605d3b168f2e803767f0847799d0447dea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Fri, 3 Mar 2023 15:00:01 +0000 Subject: [PATCH 3/9] feat: remove box self from services, remove syncwrapper from axum service (#677) * feat: remove box self from all services * feat: remove syncwrapper from axum integration --- Cargo.lock | 1 - service/Cargo.toml | 3 +-- service/src/lib.rs | 32 +++++++++++++++----------------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 25def720b9..7498f25b6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6469,7 +6469,6 @@ dependencies = [ "shuttle-codegen", "shuttle-common", "sqlx", - "sync_wrapper", "thiserror", "thruster", "tide", diff --git a/service/Cargo.toml b/service/Cargo.toml index 261d85514c..2379c1e4a9 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -31,7 +31,6 @@ salvo = { version = "0.37.5", optional = true } serde_json = { workspace = true } serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"], optional = true } poise = { version = "0.5.2", optional = true } -sync_wrapper = { version = "0.1.1", optional = true } thiserror = { workspace = true } thruster = { version = "1.3.0", optional = true } tide = { version = "0.16.0", optional = true } @@ -70,7 +69,7 @@ codegen = ["shuttle-codegen/frameworks"] loader = ["cargo", "libloading"] web-actix-web = ["actix-web", "num_cpus"] -web-axum = ["axum", "sync_wrapper"] +web-axum = ["axum"] web-rocket = ["rocket"] web-thruster = ["thruster"] web-tide = ["tide", "async-std"] diff --git a/service/src/lib.rs b/service/src/lib.rs index b40fe8b614..d65961515e 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -509,7 +509,7 @@ impl Service for T where T: poem::Endpoint + Sync + Send + 'static, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { poem::Server::new(poem::listener::TcpListener::bind(addr)) .run(self) .await @@ -529,7 +529,7 @@ where T: Send + Sync + Clone + 'static + warp::Filter, T::Extract: warp::reply::Reply, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { warp::serve(*self).run(addr).await; Ok(()) } @@ -540,12 +540,10 @@ pub type ShuttleWarp = Result, Error>; #[cfg(feature = "web-axum")] #[async_trait] -impl Service for sync_wrapper::SyncWrapper { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { - let router = self.into_inner(); - +impl Service for axum::Router { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { axum::Server::bind(&addr) - .serve(router.into_make_service()) + .serve(self.into_make_service()) .await .map_err(error::CustomError::new)?; @@ -553,13 +551,16 @@ impl Service for sync_wrapper::SyncWrapper { } } +#[cfg(feature = "web-axum")] +pub type ShuttleAxum = Result; + #[cfg(feature = "web-actix-web")] #[async_trait] impl Service for F where F: FnOnce(&mut actix_web::web::ServiceConfig) + Sync + Send + Clone + 'static, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), Error> { // Start a worker for each cpu, but no more than 4. let worker_count = num_cpus::get().max(4); @@ -575,13 +576,10 @@ where #[cfg(feature = "web-actix-web")] pub type ShuttleActixWeb = Result; -#[cfg(feature = "web-axum")] -pub type ShuttleAxum = Result, Error>; - #[cfg(feature = "web-salvo")] #[async_trait] impl Service for salvo::Router { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { salvo::Server::new(salvo::listener::TcpListener::bind(addr)) .serve(self) .await; @@ -599,7 +597,7 @@ impl Service for T where T: thruster::ThrusterServer + Sync + Send + 'static, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { self.build(&addr.ip().to_string(), addr.port()).await; Ok(()) @@ -615,7 +613,7 @@ impl Service for tide::Server where T: Clone + Send + Sync + 'static, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { self.listen(addr).await.map_err(error::CustomError::new)?; Ok(()) @@ -637,7 +635,7 @@ where T::Error: std::error::Error + Send + Sync, T::Future: std::future::Future + Send + Sync, { - async fn bind(mut self: Box, addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error> { let shared = tower::make::Shared::new(self); hyper::Server::bind(&addr) .serve(shared) @@ -651,7 +649,7 @@ where #[cfg(feature = "bot-serenity")] #[async_trait] impl Service for serenity::Client { - async fn bind(mut self: Box, _addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, _addr: SocketAddr) -> Result<(), error::Error> { self.start().await.map_err(error::CustomError::new)?; Ok(()) @@ -668,7 +666,7 @@ where T: std::marker::Send + std::marker::Sync + 'static, E: std::marker::Send + std::marker::Sync + 'static, { - async fn bind(mut self: Box, _addr: SocketAddr) -> Result<(), error::Error> { + async fn bind(mut self, _addr: SocketAddr) -> Result<(), error::Error> { self.start().await.map_err(error::CustomError::new)?; Ok(()) From 52096fc6d895ab0146f24313d112d13d2198a582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Mon, 6 Mar 2023 08:14:09 +0000 Subject: [PATCH 4/9] feat: extract next runtime into separate binary (#679) * feat: extract next runtime into separate binary * fix: remove startrequestsservice name --- cargo-shuttle/src/lib.rs | 1 - deployer/src/deployment/run.rs | 8 +--- runtime/Cargo.toml | 6 ++- runtime/Makefile | 4 +- runtime/README.md | 21 +++++----- runtime/src/bin/next.rs | 40 +++++++++++++++++++ runtime/src/{ => legacy}/args.rs | 8 ---- runtime/src/legacy/mod.rs | 6 ++- runtime/src/lib.rs | 7 ++-- runtime/src/main.rs | 66 ------------------------------- runtime/src/next/args.rs | 9 +++++ runtime/src/{axum => next}/mod.rs | 8 ++-- 12 files changed, 82 insertions(+), 102 deletions(-) create mode 100644 runtime/src/bin/next.rs rename runtime/src/{ => legacy}/args.rs (77%) delete mode 100644 runtime/src/main.rs create mode 100644 runtime/src/next/args.rs rename runtime/src/{axum => next}/mod.rs (99%) diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index d09ae6d2da..5fcc02eab8 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -516,7 +516,6 @@ impl Shuttle { let start_request = StartRequest { deployment_id: id.as_bytes().to_vec(), - service_name, ip: addr.to_string(), }; diff --git a/deployer/src/deployment/run.rs b/deployer/src/deployment/run.rs index b0ce1e3bef..c1123005a8 100644 --- a/deployer/src/deployment/run.rs +++ b/deployer/src/deployment/run.rs @@ -218,7 +218,6 @@ impl Built { .unwrap(); run( self.id, - self.service_name, runtime_client, address, deployment_updater, @@ -280,7 +279,6 @@ async fn load( #[instrument(skip(runtime_client, deployment_updater, kill_recv, cleanup), fields(state = %State::Running))] async fn run( id: Uuid, - service_name: String, runtime_client: &mut RuntimeClient, address: SocketAddr, deployment_updater: impl DeploymentUpdater, @@ -294,7 +292,6 @@ async fn run( let start_request = tonic::Request::new(StartRequest { deployment_id: id.as_bytes().to_vec(), - service_name: service_name.clone(), ip: address.to_string(), }); @@ -310,10 +307,7 @@ async fn run( while let Ok(kill_id) = kill_recv.recv().await { if kill_id == id { - let stop_request = tonic::Request::new(StopRequest { - deployment_id: id.as_bytes().to_vec(), - service_name: service_name.clone(), - }); + let stop_request = tonic::Request::new(StopRequest {}); response = runtime_client.stop(stop_request).await; break; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index cfa96cd800..b1bb859ed8 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -9,6 +9,9 @@ publish = false [[bin]] name = "rocket" +[[bin]] +name = "next" + [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } @@ -23,6 +26,7 @@ tonic = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } uuid = { workspace = true, features = ["v4"] } +# TODO: bump these crates to 6.0 when we bump rust to >= 1.66 wasi-common = "4.0.0" wasmtime = "4.0.0" wasmtime-wasi = "4.0.0" @@ -35,7 +39,7 @@ rocket = "0.5.0-rc.2" [dependencies.shuttle-common] workspace = true -features = ["wasm"] +features = ["wasm", "service"] [dependencies.shuttle-proto] workspace = true diff --git a/runtime/Makefile b/runtime/Makefile index 6472a425ec..fd4fccbd00 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -3,8 +3,8 @@ all: axum axum: - cd ../tmp/axum-wasm-expanded; cargo build --target wasm32-wasi - cp ../tmp/axum-wasm-expanded/target/wasm32-wasi/debug/shuttle_axum_expanded.wasm axum.wasm + cd tests/resources/axum-wasm-expanded; cargo build --target wasm32-wasi + cp tests/resources/axum-wasm-expanded/target/wasm32-wasi/debug/shuttle_axum_expanded.wasm axum.wasm test: axum cargo test --all-features -- --nocapture diff --git a/runtime/README.md b/runtime/README.md index 5e1f1d910c..d7caf894db 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -30,7 +30,7 @@ make test Load and run: ```bash -cargo run -- --axum --provisioner-address http://localhost:5000 +cargo run --bin next -- --port 6001 ``` In another terminal: @@ -40,24 +40,27 @@ In another terminal: grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "/home//runtime/axum.wasm"}' localhost:6001 runtime.Runtime/Load # start -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw"}' localhost:6001 runtime.Runtime/Start +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start -# subscribe to logs (unimplemented) +# subscribe to logs grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs # stop -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw"}' localhost:6001 runtime.Runtime/Stop +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{}' localhost:6001 runtime.Runtime/Stop ``` Curl the service: ```bash -curl localhost:7002/hello +curl localhost:8000/hello -curl localhost:7002/goodbye +curl localhost:8000/goodbye ``` ## shuttle-legacy -This will no loger load a `.so` will the code to start the runtime will be codegened for all services. + +This will no longer load a `.so` file, the code to start the runtime will be +codegened for all services. + An example can be found in `src/bin/rocket.rs` which contains the secrets rocket example at the bottom and the codegen at the top. To test, first start a provisioner from the root directory using: @@ -69,7 +72,7 @@ docker-compose -f docker-compose.rendered.yml up provisioner Then in another shell, start the wrapped runtime using the clap CLI: ```bash -cargo run -- --port 6001 --storage-manager-type working-dir --storage-manager-path ./ +cargo run --bin rocket -- --port 6001 --storage-manager-type working-dir --storage-manager-path ./ ``` Or directly (this is the path hardcoded in `deployer::start`): @@ -87,7 +90,7 @@ Then in another shell, load the service and start it up: grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "/home//examples/rocket/hello-world/target/debug/libhello_world.so", "secrets": {"MY_API_KEY": "test"}}' localhost:6001 runtime.Runtime/Load # run (this deployment id is default uuid encoded as base64) -grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start +grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"deployment_id": "MDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAw", "ip": "127.0.0.1:8000"}' localhost:6001 runtime.Runtime/Start # subscribe to logs grpcurl -plaintext -import-path ../proto -proto runtime.proto localhost:6001 runtime.Runtime/SubscribeLogs diff --git a/runtime/src/bin/next.rs b/runtime/src/bin/next.rs new file mode 100644 index 0000000000..554cb882ec --- /dev/null +++ b/runtime/src/bin/next.rs @@ -0,0 +1,40 @@ +use std::{ + net::{Ipv4Addr, SocketAddr}, + time::Duration, +}; + +use clap::Parser; +use shuttle_proto::runtime::runtime_server::RuntimeServer; +use shuttle_runtime::{AxumWasm, NextArgs}; +use tonic::transport::Server; +use tracing::trace; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +#[tokio::main(flavor = "multi_thread")] +async fn main() { + let args = NextArgs::parse(); + + // TODO: replace with tracing helper from main branch + let fmt_layer = fmt::layer(); + let filter_layer = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("info")) + .unwrap(); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); + + trace!(args = ?args, "parsed args"); + + let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), args.port); + + let mut server_builder = + Server::builder().http2_keepalive_interval(Some(Duration::from_secs(60))); + + let axum = AxumWasm::default(); + let svc = RuntimeServer::new(axum); + let router = server_builder.add_service(svc); + + router.serve(addr).await.unwrap(); +} diff --git a/runtime/src/args.rs b/runtime/src/legacy/args.rs similarity index 77% rename from runtime/src/args.rs rename to runtime/src/legacy/args.rs index 9f02159703..77af08cbd6 100644 --- a/runtime/src/args.rs +++ b/runtime/src/legacy/args.rs @@ -14,14 +14,6 @@ pub struct Args { #[arg(long, default_value = "http://localhost:5000")] pub provisioner_address: Endpoint, - /// Is this runtime for a legacy service - #[arg(long, conflicts_with("axum"))] - pub legacy: bool, - - /// Is this runtime for an axum-wasm service - #[arg(long, conflicts_with("legacy"))] - pub axum: bool, - /// Type of storage manager to start #[arg(long, value_enum)] pub storage_manager_type: StorageManagerType, diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index d5fe5ab8b0..ea042a8561 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -36,7 +36,11 @@ use tonic::{ use tracing::{error, instrument, trace}; use uuid::Uuid; -use crate::{provisioner_factory::ProvisionerFactory, Args}; +use crate::provisioner_factory::ProvisionerFactory; + +use self::args::Args; + +mod args; pub async fn start( loader: impl Loader> + Send + 'static, diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 62fedf7f31..9d241f933d 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,9 +1,8 @@ -mod args; -mod axum; mod legacy; +mod next; mod provisioner_factory; -pub use args::{Args, StorageManagerType}; -pub use axum::AxumWasm; pub use legacy::{start, Legacy}; +pub use next::AxumWasm; +pub use next::NextArgs; pub use provisioner_factory::ProvisionerFactory; diff --git a/runtime/src/main.rs b/runtime/src/main.rs deleted file mode 100644 index 8a3a5f8037..0000000000 --- a/runtime/src/main.rs +++ /dev/null @@ -1,66 +0,0 @@ -use std::{ - net::{Ipv4Addr, SocketAddr}, - time::Duration, -}; - -use clap::Parser; -use shuttle_common::storage_manager::{ArtifactsStorageManager, WorkingDirStorageManager}; -use shuttle_proto::runtime::runtime_server::RuntimeServer; -use shuttle_runtime::{Args, AxumWasm, Legacy, StorageManagerType}; -use tonic::transport::Server; -use tracing::trace; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; - -#[tokio::main(flavor = "multi_thread")] -async fn main() { - let args = Args::parse(); - - let fmt_layer = fmt::layer(); - let filter_layer = EnvFilter::try_from_default_env() - .or_else(|_| EnvFilter::try_new("info")) - .unwrap(); - - tracing_subscriber::registry() - .with(filter_layer) - .with(fmt_layer) - .init(); - - trace!(args = ?args, "parsed args"); - - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), args.port); - - let provisioner_address = args.provisioner_address; - let mut server_builder = - Server::builder().http2_keepalive_interval(Some(Duration::from_secs(60))); - - let router = if args.legacy { - match args.storage_manager_type { - StorageManagerType::Artifacts => { - let legacy = Legacy::new( - provisioner_address, - ArtifactsStorageManager::new(args.storage_manager_path), - ); - - let svc = RuntimeServer::new(legacy); - server_builder.add_service(svc) - } - StorageManagerType::WorkingDir => { - let legacy = Legacy::new( - provisioner_address, - WorkingDirStorageManager::new(args.storage_manager_path), - ); - - let svc = RuntimeServer::new(legacy); - server_builder.add_service(svc) - } - } - } else if args.axum { - let axum = AxumWasm::default(); - let svc = RuntimeServer::new(axum); - server_builder.add_service(svc) - } else { - panic!("No runtime was selected"); - }; - - router.serve(addr).await.unwrap(); -} diff --git a/runtime/src/next/args.rs b/runtime/src/next/args.rs new file mode 100644 index 0000000000..ccbb3ac6e7 --- /dev/null +++ b/runtime/src/next/args.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(version)] +pub struct NextArgs { + /// Port to start runtime on + #[arg(long)] + pub port: u16, +} diff --git a/runtime/src/axum/mod.rs b/runtime/src/next/mod.rs similarity index 99% rename from runtime/src/axum/mod.rs rename to runtime/src/next/mod.rs index 5bc7989640..ccd30e321e 100644 --- a/runtime/src/axum/mod.rs +++ b/runtime/src/next/mod.rs @@ -30,6 +30,10 @@ use wasmtime::{Engine, Linker, Module, Store}; use wasmtime_wasi::sync::net::UnixStream as WasiUnixStream; use wasmtime_wasi::{WasiCtx, WasiCtxBuilder}; +mod args; + +pub use self::args::NextArgs; + extern crate rmp_serde as rmps; const LOGS_FD: u32 = 20; @@ -94,9 +98,7 @@ impl Runtime for AxumWasm { &self, request: tonic::Request, ) -> Result, Status> { - let StartRequest { - deployment_id, ip, .. - } = request.into_inner(); + let StartRequest { deployment_id, ip } = request.into_inner(); let address = SocketAddr::from_str(&ip) .context("invalid socket address") From 3489e83cfabd775fc6c54dbba0339c642b69be85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Mon, 6 Mar 2023 08:17:02 +0000 Subject: [PATCH 5/9] Feature: remove tokio runtime from all resources (#680) * feat: remove runtime from resources * fix: compile errors, comments * fix: remove comment --- resources/aws-rds/Cargo.toml | 1 - resources/aws-rds/src/lib.rs | 17 +++++---------- resources/persist/Cargo.toml | 1 - resources/persist/src/lib.rs | 2 -- resources/shared-db/Cargo.toml | 1 - resources/shared-db/src/lib.rs | 35 ++++++------------------------ resources/static-folder/Cargo.toml | 1 - resources/static-folder/src/lib.rs | 22 ++++--------------- service/src/lib.rs | 3 --- service/src/persist.rs | 7 +----- 10 files changed, 17 insertions(+), 73 deletions(-) diff --git a/resources/aws-rds/Cargo.toml b/resources/aws-rds/Cargo.toml index bcf5652c1c..0dfec8369e 100644 --- a/resources/aws-rds/Cargo.toml +++ b/resources/aws-rds/Cargo.toml @@ -12,7 +12,6 @@ async-trait = "0.1.56" paste = "1.0.7" shuttle-service = { path = "../../service", version = "0.8.0", default-features = false } sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls"] } -tokio = { version = "1.19.2", features = ["rt"] } [features] postgres = ["sqlx/postgres"] diff --git a/resources/aws-rds/src/lib.rs b/resources/aws-rds/src/lib.rs index 6b6165228a..dec92c1984 100644 --- a/resources/aws-rds/src/lib.rs +++ b/resources/aws-rds/src/lib.rs @@ -7,7 +7,6 @@ use shuttle_service::{ error::CustomError, Factory, ResourceBuilder, }; -use tokio::runtime::Runtime; macro_rules! aws_engine { ($feature:expr, $pool_path:path, $options_path:path, $struct_ident:ident) => { @@ -24,22 +23,16 @@ macro_rules! aws_engine { Self {} } - async fn build(self, factory: &mut dyn Factory, runtime: &Runtime) -> Result<$pool_path, shuttle_service::Error> { + async fn build(self, factory: &mut dyn Factory) -> Result<$pool_path, shuttle_service::Error> { let connection_string = factory .get_db_connection_string(Type::AwsRds(AwsRdsEngine::$struct_ident)) .await?; - // A sqlx Pool cannot cross runtime boundaries, so make sure to create the Pool on the service end - let pool = runtime - .spawn(async move { - $options_path::new() - .min_connections(1) - .max_connections(5) - .connect(&connection_string) - .await - }) + let pool = $options_path::new() + .min_connections(1) + .max_connections(5) + .connect(&connection_string) .await - .map_err(CustomError::new)? .map_err(CustomError::new)?; Ok(pool) diff --git a/resources/persist/Cargo.toml b/resources/persist/Cargo.toml index 7686a44c1b..82e0559e6e 100644 --- a/resources/persist/Cargo.toml +++ b/resources/persist/Cargo.toml @@ -14,4 +14,3 @@ serde = { version = "1.0.0", features = ["derive"] } shuttle-common = { path = "../../common", version = "0.8.0", default-features = false } shuttle-service = { path = "../../service", version = "0.8.0", default-features = false } thiserror = "1.0.32" -tokio = { version = "1.19.2", features = ["rt"] } diff --git a/resources/persist/src/lib.rs b/resources/persist/src/lib.rs index f6fd8e8a4e..84d3ce458f 100644 --- a/resources/persist/src/lib.rs +++ b/resources/persist/src/lib.rs @@ -10,7 +10,6 @@ use std::io::BufReader; use std::io::BufWriter; use std::path::PathBuf; use thiserror::Error; -use tokio::runtime::Runtime; #[derive(Error, Debug)] pub enum PersistError { @@ -74,7 +73,6 @@ impl ResourceBuilder for Persist { async fn build( self, factory: &mut dyn Factory, - _runtime: &Runtime, ) -> Result { Ok(PersistInstance { service_name: factory.get_service_name(), diff --git a/resources/shared-db/Cargo.toml b/resources/shared-db/Cargo.toml index 611d5126a0..0cbf266eb6 100644 --- a/resources/shared-db/Cargo.toml +++ b/resources/shared-db/Cargo.toml @@ -12,7 +12,6 @@ async-trait = "0.1.56" mongodb = { version = "2.3.0", optional = true } shuttle-service = { path = "../../service", version = "0.8.0", default-features = false } sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls"], optional = true } -tokio = { version = "1.19.2", features = ["rt"] } [features] postgres = ["sqlx/postgres"] diff --git a/resources/shared-db/src/lib.rs b/resources/shared-db/src/lib.rs index c65069c757..fb3fd15a38 100644 --- a/resources/shared-db/src/lib.rs +++ b/resources/shared-db/src/lib.rs @@ -1,7 +1,5 @@ #![doc = include_str!("../README.md")] -use tokio::runtime::Runtime; - use async_trait::async_trait; use shuttle_service::{database, error::CustomError, Error, Factory, ResourceBuilder}; @@ -16,26 +14,16 @@ impl ResourceBuilder for Postgres { Self {} } - async fn build( - self, - factory: &mut dyn Factory, - runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let connection_string = factory .get_db_connection_string(database::Type::Shared(database::SharedEngine::Postgres)) .await?; - // A sqlx Pool cannot cross runtime boundaries, so make sure to create the Pool on the service end - let pool = runtime - .spawn(async move { - sqlx::postgres::PgPoolOptions::new() - .min_connections(1) - .max_connections(5) - .connect(&connection_string) - .await - }) + let pool = sqlx::postgres::PgPoolOptions::new() + .min_connections(1) + .max_connections(5) + .connect(&connection_string) .await - .map_err(CustomError::new)? .map_err(CustomError::new)?; Ok(pool) @@ -53,11 +41,7 @@ impl ResourceBuilder for MongoDb { Self {} } - async fn build( - self, - factory: &mut dyn Factory, - runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let connection_string = factory .get_db_connection_string(database::Type::Shared(database::SharedEngine::MongoDb)) .await @@ -69,12 +53,7 @@ impl ResourceBuilder for MongoDb { client_options.min_pool_size = Some(1); client_options.max_pool_size = Some(5); - // A mongodb client cannot cross runtime boundaries, so make sure to create the client on the service end - let client = runtime - .spawn(async move { mongodb::Client::with_options(client_options) }) - .await - .map_err(CustomError::new)? - .map_err(CustomError::new)?; + let client = mongodb::Client::with_options(client_options).map_err(CustomError::new)?; // Return a handle to the database defined at the end of the connection string, which is the users provisioned // database diff --git a/resources/static-folder/Cargo.toml b/resources/static-folder/Cargo.toml index bd44daee78..cade0556fc 100644 --- a/resources/static-folder/Cargo.toml +++ b/resources/static-folder/Cargo.toml @@ -10,7 +10,6 @@ keywords = ["shuttle-service", "static-folder"] [dependencies] async-trait = "0.1.56" shuttle-service = { path = "../../service", version = "0.8.0", default-features = false } -tokio = { version = "1.19.2", features = ["rt"] } [dev-dependencies] tempdir = "0.3.7" diff --git a/resources/static-folder/src/lib.rs b/resources/static-folder/src/lib.rs index 259288dd9d..524ffe80f4 100644 --- a/resources/static-folder/src/lib.rs +++ b/resources/static-folder/src/lib.rs @@ -7,7 +7,6 @@ use std::{ fs::rename, path::{Path, PathBuf}, }; -use tokio::runtime::Runtime; pub struct StaticFolder<'a> { /// The folder to reach at runtime. Defaults to `static` @@ -33,11 +32,7 @@ impl<'a> ResourceBuilder for StaticFolder<'a> { Self { folder: "static" } } - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { let folder = Path::new(self.folder); // Prevent users from users from reading anything outside of their crate's build folder @@ -171,8 +166,7 @@ mod tests { // Call plugin let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); - let actual_folder = static_folder.build(&mut factory, &runtime).await.unwrap(); + let actual_folder = static_folder.build(&mut factory).await.unwrap(); assert_eq!( actual_folder, @@ -185,8 +179,6 @@ mod tests { "Hello, test!", "expected file content to match" ); - - runtime.shutdown_background(); } #[tokio::test] @@ -194,15 +186,12 @@ mod tests { async fn cannot_use_absolute_path() { let mut factory = MockFactory::new(); let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); let _ = static_folder .folder("/etc") - .build(&mut factory, &runtime) + .build(&mut factory) .await .unwrap(); - - runtime.shutdown_background(); } #[tokio::test] @@ -217,13 +206,10 @@ mod tests { // Call plugin let static_folder = StaticFolder::new(); - let runtime = tokio::runtime::Runtime::new().unwrap(); let _ = static_folder .folder("../escape") - .build(&mut factory, &runtime) + .build(&mut factory) .await .unwrap(); - - runtime.shutdown_background(); } } diff --git a/service/src/lib.rs b/service/src/lib.rs index d65961515e..7b1b555976 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -329,8 +329,6 @@ pub trait Factory: Send + Sync { /// You may want to create your own managed resource by implementing this trait for some builder `B` to construct resource `T`. [`Factory`] can be used to provision resources /// on shuttle's servers if your resource will need any. /// -/// The biggest thing to look out for is that your resource object might panic when it crosses the boundary between the shuttle's backend runtime and the runtime -/// of services. These resources should be created on the passed in `runtime` for this trait to prevent these panics. /// /// Your resource will be available on a [shuttle_service::main][main] function as follow: /// ``` @@ -371,7 +369,6 @@ pub trait Factory: Send + Sync { /// async fn build( /// self, /// factory: &mut dyn Factory, -/// _runtime: &Runtime, /// ) -> Result { /// Ok(Resource { name: self.name }) /// } diff --git a/service/src/persist.rs b/service/src/persist.rs index 6b75101582..adeebc2639 100644 --- a/service/src/persist.rs +++ b/service/src/persist.rs @@ -9,7 +9,6 @@ use std::io::BufReader; use std::io::BufWriter; use std::path::PathBuf; use thiserror::Error; -use tokio::runtime::Runtime; #[derive(Error, Debug)] pub enum PersistError { @@ -70,11 +69,7 @@ impl ResourceBuilder for Persist { Self {} } - async fn build( - self, - factory: &mut dyn Factory, - _runtime: &Runtime, - ) -> Result { + async fn build(self, factory: &mut dyn Factory) -> Result { Ok(PersistInstance { service_name: factory.get_service_name(), }) From 8be4742b1c075834364aed9c5991c3a50dabc1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Mon, 6 Mar 2023 14:11:37 +0000 Subject: [PATCH 6/9] feat: trim the service loader, unpin tokio (#681) * feat: trim the serviceloader, unpin tokio * fix: tokio sync is needed * feat: unpin tokio in runtime as well * fix: static folder tests need tokio rt * feat: rename loader, build_crate return bin path * fix: build_Crate invalid args --- Cargo.lock | 11 -- cargo-shuttle/Cargo.toml | 2 +- cargo-shuttle/src/lib.rs | 7 +- deployer/Cargo.toml | 2 +- deployer/src/deployment/queue.rs | 9 +- deployer/src/handlers/mod.rs | 2 +- resources/static-folder/Cargo.toml | 2 +- runtime/Cargo.toml | 4 +- service/Cargo.toml | 7 +- service/src/{loader.rs => builder.rs} | 156 +------------------------- service/src/lib.rs | 82 +------------- 11 files changed, 22 insertions(+), 262 deletions(-) rename service/src/{loader.rs => builder.rs} (57%) diff --git a/Cargo.lock b/Cargo.lock index 7498f25b6e..ce6076eab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3823,16 +3823,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if 1.0.0", - "winapi", -] - [[package]] name = "libnghttp2-sys" version = "0.1.7+1.45.0" @@ -6456,7 +6446,6 @@ dependencies = [ "crossbeam-channel", "futures", "hyper", - "libloading", "num_cpus", "pipe", "poem", diff --git a/cargo-shuttle/Cargo.toml b/cargo-shuttle/Cargo.toml index ce36c87dcc..475c5d900f 100644 --- a/cargo-shuttle/Cargo.toml +++ b/cargo-shuttle/Cargo.toml @@ -64,7 +64,7 @@ path = "../resources/secrets" [dependencies.shuttle-service] workspace = true -features = ["loader"] +features = ["builder"] [features] vendored-openssl = ["openssl/vendored"] diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 5fcc02eab8..2f5fab49f2 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -30,7 +30,7 @@ use git2::{Repository, StatusOptions}; use ignore::overrides::OverrideBuilder; use ignore::WalkBuilder; use shuttle_common::models::{project, secret}; -use shuttle_service::loader::{build_crate, Runtime}; +use shuttle_service::builder::{build_crate, Runtime}; use std::fmt::Write; use strum::IntoEnumIterator; use tar::Builder; @@ -376,7 +376,6 @@ impl Shuttle { }); let working_directory = self.ctx.working_directory(); - let id = Default::default(); trace!("building project"); println!( @@ -384,7 +383,7 @@ impl Shuttle { "Building".bold().green(), working_directory.display() ); - let runtime = build_crate(id, working_directory, false, tx).await?; + let runtime = build_crate(working_directory, false, tx).await?; trace!("loading secrets"); let secrets_path = working_directory.join("Secrets.toml"); @@ -515,7 +514,7 @@ impl Shuttle { let addr = SocketAddr::new(addr, run_args.port); let start_request = StartRequest { - deployment_id: id.as_bytes().to_vec(), + deployment_id: Uuid::default().as_bytes().to_vec(), ip: addr.to_string(), }; diff --git a/deployer/Cargo.toml b/deployer/Cargo.toml index 4dad739918..73edc850f2 100644 --- a/deployer/Cargo.toml +++ b/deployer/Cargo.toml @@ -54,7 +54,7 @@ workspace = true [dependencies.shuttle-service] workspace = true -features = ["loader"] +features = ["builder"] [dev-dependencies] ctor = "0.1.26" diff --git a/deployer/src/deployment/queue.rs b/deployer/src/deployment/queue.rs index 9e871986c6..0d454b4bbb 100644 --- a/deployer/src/deployment/queue.rs +++ b/deployer/src/deployment/queue.rs @@ -11,7 +11,7 @@ use chrono::Utc; use crossbeam_channel::Sender; use opentelemetry::global; use serde_json::json; -use shuttle_service::loader::{build_crate, get_config, Runtime}; +use shuttle_service::builder::{build_crate, get_config, Runtime}; use tokio::time::{sleep, timeout}; use tracing::{debug, debug_span, error, info, instrument, trace, warn, Instrument, Span}; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -206,7 +206,7 @@ impl Queued { }); let project_path = project_path.canonicalize()?; - let runtime = build_deployment(self.id, &project_path, tx.clone()).await?; + let runtime = build_deployment(&project_path, tx.clone()).await?; if self.will_run_tests { info!( @@ -321,11 +321,10 @@ async fn extract_tar_gz_data(data: impl Read, dest: impl AsRef) -> Result< #[instrument(skip(project_path, tx))] async fn build_deployment( - deployment_id: Uuid, project_path: &Path, tx: crossbeam_channel::Sender, ) -> Result { - build_crate(deployment_id, project_path, true, tx) + build_crate(project_path, true, tx) .await .map_err(|e| Error::Build(e.into())) } @@ -413,7 +412,7 @@ mod tests { use std::{collections::BTreeMap, fs::File, io::Write, path::Path}; use shuttle_common::storage_manager::ArtifactsStorageManager; - use shuttle_service::loader::Runtime; + use shuttle_service::builder::Runtime; use tempdir::TempDir; use tokio::fs; use uuid::Uuid; diff --git a/deployer/src/handlers/mod.rs b/deployer/src/handlers/mod.rs index f3def20806..ed034d7b80 100644 --- a/deployer/src/handlers/mod.rs +++ b/deployer/src/handlers/mod.rs @@ -18,7 +18,7 @@ use shuttle_common::models::secret; use shuttle_common::project::ProjectName; use shuttle_common::storage_manager::StorageManager; use shuttle_common::LogItem; -use shuttle_service::loader::clean_crate; +use shuttle_service::builder::clean_crate; use tower_http::auth::RequireAuthorizationLayer; use tower_http::trace::TraceLayer; use tracing::{debug, debug_span, error, field, instrument, trace, Span}; diff --git a/resources/static-folder/Cargo.toml b/resources/static-folder/Cargo.toml index cade0556fc..860c9875cd 100644 --- a/resources/static-folder/Cargo.toml +++ b/resources/static-folder/Cargo.toml @@ -13,4 +13,4 @@ shuttle-service = { path = "../../service", version = "0.8.0", default-features [dev-dependencies] tempdir = "0.3.7" -tokio = { version = "1.19.2", features = ["macros"] } +tokio = { version = "1.19.2", features = ["macros", "rt"] } diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b1bb859ed8..897f97d8b3 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -20,7 +20,7 @@ clap ={ version = "4.0.18", features = ["derive"] } hyper = { version = "0.14.23", features = ["server"] } rmp-serde = { version = "1.1.1" } thiserror = { workspace = true } -tokio = { version = "=1.22.0", features = ["full"] } +tokio = { version = "1.22.0", features = ["full"] } tokio-stream = "0.1.11" tonic = { workspace = true } tracing = { workspace = true } @@ -46,4 +46,4 @@ workspace = true [dependencies.shuttle-service] workspace = true -features = ["loader", "web-rocket"] # TODO: remove web-rocket +features = ["builder", "web-rocket"] # TODO: remove web-rocket diff --git a/service/Cargo.toml b/service/Cargo.toml index 2379c1e4a9..ae3a29eb89 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -22,7 +22,6 @@ chrono = { workspace = true } crossbeam-channel = "0.5.6" futures = { version = "0.3.25", features = ["std"] } hyper = { version = "0.14.23", features = ["server", "tcp", "http1"], optional = true } -libloading = { version = "0.7.4", optional = true } num_cpus = { version = "1.14.0", optional = true } pipe = "0.4.0" poem = { version = "1.3.49", optional = true } @@ -34,7 +33,7 @@ poise = { version = "0.5.2", optional = true } thiserror = { workspace = true } thruster = { version = "1.3.0", optional = true } tide = { version = "0.16.0", optional = true } -tokio = { version = "=1.22.0", features = ["rt", "rt-multi-thread", "sync"] } +tokio = { version = "1.22.0", features = ["sync"] } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } @@ -59,14 +58,14 @@ features = ["tracing", "service"] [dev-dependencies] portpicker = "0.1.1" sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } -tokio = { version = "1.22.0", features = ["macros"] } +tokio = { version = "1.22.0", features = ["macros", "rt"] } uuid = { workspace = true, features = ["v4"] } [features] default = ["codegen"] codegen = ["shuttle-codegen/frameworks"] -loader = ["cargo", "libloading"] +builder = ["cargo"] web-actix-web = ["actix-web", "num_cpus"] web-axum = ["axum"] diff --git a/service/src/loader.rs b/service/src/builder.rs similarity index 57% rename from service/src/loader.rs rename to service/src/builder.rs index 9f130716ce..8702842066 100644 --- a/service/src/loader.rs +++ b/service/src/builder.rs @@ -1,106 +1,18 @@ -use std::any::Any; -use std::ffi::OsStr; -use std::net::SocketAddr; -use std::panic::AssertUnwindSafe; use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context}; use cargo::core::compiler::{CompileKind, CompileMode, CompileTarget, MessageFormat}; -use cargo::core::{Manifest, PackageId, Shell, Summary, Verbosity, Workspace}; +use cargo::core::{Shell, Summary, Verbosity, Workspace}; use cargo::ops::{clean, compile, CleanOptions, CompileOptions}; use cargo::util::interning::InternedString; use cargo::util::{homedir, ToSemver}; use cargo::Config; use cargo_metadata::Message; use crossbeam_channel::Sender; -use libloading::{Library, Symbol}; use pipe::PipeWriter; -use thiserror::Error as ThisError; use tracing::{error, trace}; -use futures::FutureExt; -use uuid::Uuid; - -use crate::error::CustomError; -use crate::{logger, Bootstrapper, NAME, NEXT_NAME, VERSION}; -use crate::{Error, Factory, ServeHandle}; - -const ENTRYPOINT_SYMBOL_NAME: &[u8] = b"_create_service\0"; - -type CreateService = unsafe extern "C" fn() -> *mut Bootstrapper; - -#[derive(Debug, ThisError)] -pub enum LoaderError { - #[error("failed to load library: {0}")] - Load(libloading::Error), - #[error("failed to find the shuttle entrypoint. Did you use the provided shuttle macros?")] - GetEntrypoint(libloading::Error), -} - -pub type LoadedService = (ServeHandle, Library); - -pub struct Loader { - bootstrapper: Bootstrapper, - so: Library, -} - -impl Loader { - /// Dynamically load from a `.so` file a value of a type implementing the - /// [`Service`][crate::Service] trait. Relies on the `.so` library having an `extern "C"` - /// function called `ENTRYPOINT_SYMBOL_NAME`, likely automatically generated - /// using the [`shuttle_service::main`][crate::main] macro. - pub fn from_so_file>(so_path: P) -> Result { - trace!(so_path = so_path.as_ref().to_str(), "loading .so path"); - unsafe { - let lib = Library::new(so_path).map_err(LoaderError::Load)?; - - let entrypoint: Symbol = lib - .get(ENTRYPOINT_SYMBOL_NAME) - .map_err(LoaderError::GetEntrypoint)?; - let raw = entrypoint(); - - Ok(Self { - bootstrapper: *Box::from_raw(raw), - so: lib, - }) - } - } - - pub async fn load( - self, - factory: &mut dyn Factory, - addr: SocketAddr, - logger: logger::Logger, - ) -> Result { - trace!("loading service"); - - let mut bootstrapper = self.bootstrapper; - - AssertUnwindSafe(bootstrapper.bootstrap(factory, logger)) - .catch_unwind() - .await - .map_err(|e| Error::BuildPanic(map_any_to_panic_string(e)))??; - - trace!("bootstrapping done"); - - // Start service on this side of the FFI - let handle = tokio::spawn(async move { - bootstrapper.into_handle(addr)?.await.map_err(|e| { - if e.is_panic() { - let mes = e.into_panic(); - - Error::BindPanic(map_any_to_panic_string(mes)) - } else { - Error::Custom(CustomError::new(e)) - } - })? - }); - - trace!("creating handle done"); - - Ok((handle, self.so)) - } -} +use crate::{NAME, NEXT_NAME, VERSION}; /// How to run/build the project pub enum Runtime { @@ -110,7 +22,6 @@ pub enum Runtime { /// Given a project directory path, builds the crate pub async fn build_crate( - deployment_id: Uuid, project_path: &Path, release_mode: bool, tx: Sender, @@ -141,11 +52,8 @@ pub async fn build_crate( let mut ws = Workspace::new(&manifest_path, &config)?; let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure your Cargo.toml file specifies it as a library."))?; - let manifest = current.manifest_mut(); - ensure_cdylib(manifest)?; let summary = current.manifest_mut().summary_mut(); - make_name_unique(summary, deployment_id); let is_next = is_next(summary); if !is_next { @@ -156,7 +64,7 @@ pub async fn build_crate( let opts = get_compile_options(&config, release_mode, is_next)?; let compilation = compile(&ws, &opts); - let path = compilation?.cdylibs[0].path.clone(); + let path = compilation?.binaries[0].path.clone(); Ok(if is_next { Runtime::Next(path) } else { @@ -259,44 +167,6 @@ fn get_compile_options( Ok(opts) } -/// Make sure "cdylib" is set, else set it if possible -fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> { - if let Some(target) = manifest - .targets_mut() - .iter_mut() - .find(|target| target.is_lib()) - { - if !target.is_cdylib() { - *target = cargo::core::manifest::Target::lib_target( - target.name(), - vec![cargo::core::compiler::CrateType::Cdylib], - target.src_path().path().unwrap().to_path_buf(), - target.edition(), - ); - } - - Ok(()) - } else { - Err(anyhow!( - "Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file." - )) - } -} - -/// Ensure name is unique. Without this `tracing`/`log` crashes because the global subscriber is somehow "already set" -// TODO: remove this when getting rid of the FFI -fn make_name_unique(summary: &mut Summary, deployment_id: Uuid) { - let old_package_id = summary.package_id(); - *summary = summary.clone().override_id( - PackageId::new( - format!("{}-{deployment_id}", old_package_id.name()), - old_package_id.version(), - old_package_id.source_id(), - ) - .unwrap(), - ); -} - fn is_next(summary: &Summary) -> bool { summary .dependencies() @@ -339,23 +209,3 @@ fn check_no_panic(ws: &Workspace) -> anyhow::Result<()> { Ok(()) } - -fn map_any_to_panic_string(a: Box) -> String { - a.downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "".to_string()) -} - -#[cfg(test)] -mod tests { - mod from_so_file { - use crate::loader::{Loader, LoaderError}; - - #[test] - fn invalid() { - let result = Loader::from_so_file("invalid.so"); - - assert!(matches!(result, Err(LoaderError::Load(_)))); - } - } -} diff --git a/service/src/lib.rs b/service/src/lib.rs index 7b1b555976..c7a6e189cd 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -85,7 +85,6 @@ //! //! ```bash //! $ cargo shuttle project new -//! $ cargo shuttle project status // until the project is "ready" //! ``` //! //! Then, deploy the service with: @@ -211,16 +210,13 @@ //! use std::collections::BTreeMap; -use std::future::Future; use std::net::SocketAddr; use std::path::PathBuf; -use std::pin::Pin; pub use async_trait::async_trait; // Pub uses by `codegen` pub use anyhow::Context; -pub use tokio::runtime::Runtime; pub use tracing; pub use tracing_subscriber; @@ -286,10 +282,9 @@ extern crate shuttle_codegen; /// /// More [shuttle managed resources can be found here](https://github.com/shuttle-hq/shuttle/tree/main/resources) pub use shuttle_codegen::main; -use tokio::task::JoinHandle; -#[cfg(feature = "loader")] -pub mod loader; +#[cfg(feature = "builder")] +pub mod builder; pub use shuttle_common::project::ProjectName as ServiceName; @@ -329,7 +324,6 @@ pub trait Factory: Send + Sync { /// You may want to create your own managed resource by implementing this trait for some builder `B` to construct resource `T`. [`Factory`] can be used to provision resources /// on shuttle's servers if your resource will need any. /// -/// /// Your resource will be available on a [shuttle_service::main][main] function as follow: /// ``` /// #[shuttle_service::main] @@ -389,87 +383,17 @@ pub trait ResourceBuilder { async fn build(self, factory: &mut dyn Factory) -> Result; } -/// A tokio handle the service was started on -pub type ServeHandle = JoinHandle>; - /// The core trait of the shuttle platform. Every crate deployed to shuttle needs to implement this trait. /// /// Use the [main][main] macro to expose your implementation to the deployment backend. -// -// TODO: our current state machine in the api crate stores this service and can move it across -// threads (handlers) causing `Service` to need `Sync`. We should remove this restriction #[async_trait] -pub trait Service: Send + Sync { +pub trait Service: Send { /// This function is run exactly once on each instance of a deployment. /// /// The deployer expects this instance of [Service][Service] to bind to the passed [SocketAddr][SocketAddr]. async fn bind(mut self, addr: SocketAddr) -> Result<(), error::Error>; } -/// This function is generated by our codegen. It uses the factory to get other services and instantiate them on -/// the correct tokio runtime. This function also sets the runtime logger. The output is a future where `T` -/// should implement [Service]. -pub type StateBuilder = - for<'a> fn( - &'a mut dyn Factory, - &'a Runtime, - Logger, - ) -> Pin> + Send + 'a>>; - -/// This function is generated by codegen to ensure binding happens on the other side of the FFI and on the correct -/// tokio runtime. -pub type Binder = for<'a> fn(Box, SocketAddr, &'a Runtime) -> ServeHandle; - -// Make sure every crate used in this struct has its version pinned down to prevent segmentation faults when crossing the FFI. -// Your future self will thank you! -// See https://github.com/shuttle-hq/shuttle/pull/348 -#[allow(dead_code)] -pub struct Bootstrapper { - service: Option>, - builder: Option>>, - binder: Binder, - runtime: Option, -} - -impl Bootstrapper { - pub fn new(builder: StateBuilder>, binder: Binder, runtime: Runtime) -> Self { - Self { - service: None, - builder: Some(builder), - binder, - runtime: Some(runtime), - } - } - - #[cfg(feature = "loader")] - async fn bootstrap(&mut self, factory: &mut dyn Factory, logger: Logger) -> Result<(), Error> { - if let Some(builder) = self.builder.take() { - let service = builder(factory, self.runtime.as_ref().unwrap(), logger).await?; - self.service = Some(service); - } - - Ok(()) - } - - #[cfg(feature = "loader")] - fn into_handle(mut self, addr: SocketAddr) -> Result { - let service = self.service.take().expect("service has already been bound"); - - let handle = (self.binder)(service, addr, self.runtime.as_ref().unwrap()); - - Ok(handle) - } -} - -impl Drop for Bootstrapper { - fn drop(&mut self) { - if let Some(runtime) = self.runtime.take() { - // TODO: find a way to drop the runtime - std::mem::forget(runtime); - } - } -} - #[cfg(feature = "web-rocket")] #[async_trait] impl Service for rocket::Rocket { From a93ba5111700c1067a0a0a357c5ca490a5ad24a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Tue, 7 Mar 2023 09:05:14 +0000 Subject: [PATCH 7/9] Feature/eng 477 make wasm dependencies optional (#688) * feat: make wasm runtime dependencies optional * feat: feature gate next lib, update readme * ci: rename loader feature in ci * refactor: hyper with server feature in next features --- .circleci/config.yml | 6 +++--- runtime/Cargo.toml | 31 +++++++++++++++++++++++-------- runtime/README.md | 4 ++-- runtime/src/legacy/mod.rs | 2 +- runtime/src/lib.rs | 5 +++-- 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8bf6b4686d..c4aa359a79 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -168,7 +168,7 @@ jobs: - run: | cargo clippy --tests \ --all-targets \ - --features="codegen,loader,<< parameters.framework >>" \ + --features="codegen,builder,<< parameters.framework >>" \ --no-deps -- \ --D warnings \ -A clippy::let-unit-value \ @@ -210,10 +210,10 @@ jobs: - restore-cargo-cache - run: name: Run unit tests - command: cargo test --package shuttle-service --features="codegen,loader" --lib -- --nocapture + command: cargo test --package shuttle-service --features="codegen,builder" --lib -- --nocapture - run: name: Run integration tests - command: cargo test --package shuttle-service --features="codegen,loader" --test '*' -- --nocapture + command: cargo test --package shuttle-service --features="codegen,builder" --test '*' -- --nocapture - save-cargo-cache platform-test: parameters: diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 897f97d8b3..b9b1ff8df3 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -11,14 +11,12 @@ name = "rocket" [[bin]] name = "next" +required-features = ["next"] [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } -cap-std = "1.0.2" clap ={ version = "4.0.18", features = ["derive"] } -hyper = { version = "0.14.23", features = ["server"] } -rmp-serde = { version = "1.1.1" } thiserror = { workspace = true } tokio = { version = "1.22.0", features = ["full"] } tokio-stream = "0.1.11" @@ -26,11 +24,15 @@ tonic = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } uuid = { workspace = true, features = ["v4"] } + # TODO: bump these crates to 6.0 when we bump rust to >= 1.66 -wasi-common = "4.0.0" -wasmtime = "4.0.0" -wasmtime-wasi = "4.0.0" -futures = "0.3.25" +cap-std = { version = "1.0.2", optional = true } +futures = { version = "0.3.25", optional = true } +hyper = { version = "0.14.23", optional = true } +rmp-serde = { version = "1.1.1", optional = true } +wasi-common = { version = "4.0.0", optional = true } +wasmtime = { version = "4.0.0", optional = true } +wasmtime-wasi = { version = "4.0.0", optional = true } # For rocket.rs # TODO: remove @@ -39,7 +41,7 @@ rocket = "0.5.0-rc.2" [dependencies.shuttle-common] workspace = true -features = ["wasm", "service"] +features = ["service"] [dependencies.shuttle-proto] workspace = true @@ -47,3 +49,16 @@ workspace = true [dependencies.shuttle-service] workspace = true features = ["builder", "web-rocket"] # TODO: remove web-rocket + +[features] +next = [ + "cap-std", + "futures", + "hyper/server", + "rmp-serde", + "futures", + "wasi-common", + "wasmtime", + "wasmtime-wasi", + "shuttle-common/wasm" +] diff --git a/runtime/README.md b/runtime/README.md index d7caf894db..6299f996a4 100644 --- a/runtime/README.md +++ b/runtime/README.md @@ -21,7 +21,7 @@ make axum Run the test: ```bash -cargo test axum -- --nocapture +cargo test --features next axum -- --nocapture # or, run tests make test @@ -30,7 +30,7 @@ make test Load and run: ```bash -cargo run --bin next -- --port 6001 +cargo run --features next --bin next -- --port 6001 ``` In another terminal: diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index ea042a8561..a040a86401 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -11,7 +11,7 @@ use std::{ use anyhow::Context; use async_trait::async_trait; use clap::Parser; -use futures::Future; +use core::future::Future; use shuttle_common::{ storage_manager::{StorageManager, WorkingDirStorageManager}, LogItem, diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 9d241f933d..33babf1c76 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,8 +1,9 @@ mod legacy; +#[cfg(feature = "next")] mod next; mod provisioner_factory; pub use legacy::{start, Legacy}; -pub use next::AxumWasm; -pub use next::NextArgs; +#[cfg(feature = "next")] +pub use next::{AxumWasm, NextArgs}; pub use provisioner_factory::ProvisionerFactory; From 3699f7f69e3f48d86f9c7b98b6af66c4b32a65c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Tue, 7 Mar 2023 09:35:44 +0000 Subject: [PATCH 8/9] Feature: eng 465 update all the codegens (#686) * feat: initial commit of codegen rewrite * refactor: wrapper to loader * tests: update the unit tests * fix: don't unwrap return type * feat: setup tracing in loader codegen * fix: pass inn logger to loader * feat: mvp of shuttle-next local run * refactor: move logger to runtime * refactor: move storagemanager export to runtime --- Cargo.lock | 3 +- cargo-shuttle/src/lib.rs | 77 ++-- codegen/src/shuttle_main/mod.rs | 388 ++++++------------- proto/src/lib.rs | 23 +- runtime/Cargo.toml | 4 +- runtime/src/bin/rocket.rs | 1 + runtime/src/bin/{next.rs => shuttle-next.rs} | 0 runtime/src/legacy/mod.rs | 23 +- runtime/src/lib.rs | 3 + {service => runtime}/src/logger.rs | 0 service/Cargo.toml | 1 - service/src/builder.rs | 7 +- service/src/lib.rs | 3 - 13 files changed, 210 insertions(+), 323 deletions(-) rename runtime/src/bin/{next.rs => shuttle-next.rs} (100%) rename {service => runtime}/src/logger.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index ce6076eab9..c47655c270 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6401,11 +6401,13 @@ dependencies = [ "anyhow", "async-trait", "cap-std", + "chrono", "clap 4.0.27", "futures", "hyper", "rmp-serde", "rocket", + "serde_json", "shuttle-common", "shuttle-proto", "shuttle-secrets", @@ -6442,7 +6444,6 @@ dependencies = [ "bincode", "cargo", "cargo_metadata", - "chrono", "crossbeam-channel", "futures", "hyper", diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs index 2f5fab49f2..2c2466269b 100644 --- a/cargo-shuttle/src/lib.rs +++ b/cargo-shuttle/src/lib.rs @@ -383,6 +383,7 @@ impl Shuttle { "Building".bold().green(), working_directory.display() ); + let runtime = build_crate(working_directory, false, tx).await?; trace!("loading secrets"); @@ -403,7 +404,7 @@ impl Shuttle { let service_name = self.ctx.project_name().to_string(); - let (is_wasm, so_path) = match runtime { + let (is_wasm, bin_path) = match runtime { Runtime::Next(path) => (true, path), Runtime::Legacy(path) => (false, path), }; @@ -414,44 +415,54 @@ impl Shuttle { run_args.port + 1, )); - let get_runtime_executable = || { - let runtime_path = home::cargo_home() - .expect("failed to find cargo home dir") - .join("bin/shuttle-runtime"); - - if cfg!(debug_assertions) { - // Canonicalized path to shuttle-runtime for dev to work on windows - let path = std::fs::canonicalize(format!("{MANIFEST_DIR}/../runtime")) - .expect("path to shuttle-runtime does not exist or is invalid"); - - std::process::Command::new("cargo") - .arg("install") - .arg("shuttle-runtime") - .arg("--path") - .arg(path) - .output() - .expect("failed to install the shuttle runtime"); - } else { - // If the version of cargo-shuttle is different from shuttle-runtime, - // or it isn't installed, try to install shuttle-runtime from the production - // branch. - if let Err(err) = check_version(&runtime_path) { - trace!("{}", err); + let runtime_path = || { + if is_wasm { + let runtime_path = home::cargo_home() + .expect("failed to find cargo home dir") + .join("bin/shuttle-next"); + + if cfg!(debug_assertions) { + // Canonicalized path to shuttle-runtime for dev to work on windows + let path = std::fs::canonicalize(format!("{MANIFEST_DIR}/../runtime")) + .expect("path to shuttle-runtime does not exist or is invalid"); - trace!("installing shuttle-runtime"); + // TODO: Add --features next here when https://github.com/shuttle-hq/shuttle/pull/688 is merged std::process::Command::new("cargo") .arg("install") .arg("shuttle-runtime") - .arg("--git") - .arg("https://github.com/shuttle-hq/shuttle") - .arg("--branch") - .arg("production") + .arg("--path") + .arg(path) + .arg("--bin") + .arg("shuttle-next") .output() .expect("failed to install the shuttle runtime"); + } else { + // If the version of cargo-shuttle is different from shuttle-runtime, + // or it isn't installed, try to install shuttle-runtime from the production + // branch. + if let Err(err) = check_version(&runtime_path) { + trace!("{}", err); + + trace!("installing shuttle-runtime"); + // TODO: Add --features next here when https://github.com/shuttle-hq/shuttle/pull/688 is merged + std::process::Command::new("cargo") + .arg("install") + .arg("shuttle-runtime") + .arg("--bin") + .arg("shuttle-next") + .arg("--git") + .arg("https://github.com/shuttle-hq/shuttle") + .arg("--branch") + .arg("production") + .output() + .expect("failed to install the shuttle runtime"); + }; }; - }; - runtime_path + runtime_path + } else { + bin_path.clone() + } }; let (mut runtime, mut runtime_client) = runtime::start( @@ -459,7 +470,7 @@ impl Shuttle { runtime::StorageManagerType::WorkingDir(working_directory.to_path_buf()), &format!("http://localhost:{}", run_args.port + 1), run_args.port + 2, - get_runtime_executable, + runtime_path, ) .await .map_err(|err| { @@ -469,7 +480,7 @@ impl Shuttle { })?; let load_request = tonic::Request::new(LoadRequest { - path: so_path + path: bin_path .into_os_string() .into_string() .expect("to convert path to string"), diff --git a/codegen/src/shuttle_main/mod.rs b/codegen/src/shuttle_main/mod.rs index c8a03d1daa..3b73f82a5c 100644 --- a/codegen/src/shuttle_main/mod.rs +++ b/codegen/src/shuttle_main/mod.rs @@ -4,50 +4,32 @@ use quote::{quote, ToTokens}; use syn::{ parenthesized, parse::Parse, parse2, parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Paren, Attribute, Expr, FnArg, Ident, ItemFn, Pat, PatIdent, Path, - ReturnType, Signature, Stmt, Token, Type, + ReturnType, Signature, Stmt, Token, Type, TypePath, }; pub(crate) fn r#impl(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut fn_decl = parse_macro_input!(item as ItemFn); - let wrapper = Wrapper::from_item_fn(&mut fn_decl); + let loader = Loader::from_item_fn(&mut fn_decl); let expanded = quote! { - #wrapper - - fn __binder( - service: Box, - addr: std::net::SocketAddr, - runtime: &shuttle_service::Runtime, - ) -> shuttle_service::ServeHandle { - use shuttle_service::Context; - runtime.spawn(async move { service.bind(addr).await.context("failed to bind service").map_err(Into::into) }) + #[tokio::main] + async fn main() { + shuttle_runtime::start(loader).await; } - #fn_decl - - #[no_mangle] - pub extern "C" fn _create_service() -> *mut shuttle_service::Bootstrapper { - let builder: shuttle_service::StateBuilder> = - |factory, runtime, logger| Box::pin(__shuttle_wrapper(factory, runtime, logger)); + #loader - let bootstrapper = shuttle_service::Bootstrapper::new( - builder, - __binder, - shuttle_service::Runtime::new().unwrap(), - ); - - let boxed = Box::new(bootstrapper); - Box::into_raw(boxed) - } + #fn_decl }; expanded.into() } -struct Wrapper { +struct Loader { fn_ident: Ident, fn_inputs: Vec, + fn_return: TypePath, } #[derive(Debug, PartialEq)] @@ -117,8 +99,8 @@ impl Parse for BuilderOption { } } -impl Wrapper { - pub(crate) fn from_item_fn(item_fn: &mut ItemFn) -> Self { +impl Loader { + pub(crate) fn from_item_fn(item_fn: &mut ItemFn) -> Option { let inputs: Vec<_> = item_fn .sig .inputs @@ -145,31 +127,40 @@ impl Wrapper { }) .collect(); - check_return_type(&item_fn.sig); - - Self { - fn_ident: item_fn.sig.ident.clone(), - fn_inputs: inputs, + if let Some(type_path) = check_return_type(item_fn.sig.clone()) { + Some(Self { + fn_ident: item_fn.sig.ident.clone(), + fn_inputs: inputs, + fn_return: type_path, + }) + } else { + None } } } -fn check_return_type(signature: &Signature) { - match &signature.output { - ReturnType::Default => emit_error!( - signature, - "shuttle_service::main functions need to return a service"; - hint = "See the docs for services with first class support"; - doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), - ReturnType::Type(_, r#type) => match r#type.as_ref() { - Type::Path(_) => {} - _ => emit_error!( - r#type, - "shuttle_service::main functions need to return a first class service or 'Result"; +fn check_return_type(signature: Signature) -> Option { + match signature.output { + ReturnType::Default => { + emit_error!( + signature, + "shuttle_service::main functions need to return a service"; hint = "See the docs for services with first class support"; doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" - ), + ); + None + } + ReturnType::Type(_, r#type) => match *r#type { + Type::Path(path) => Some(path), + _ => { + emit_error!( + r#type, + "shuttle_service::main functions need to return a first class service or 'Result"; + hint = "See the docs for services with first class support"; + doc = "https://docs.rs/shuttle-service/latest/shuttle_service/attr.main.html#shuttle-supported-services" + ); + None + } }, } } @@ -196,9 +187,12 @@ fn attribute_to_builder(pat_ident: &PatIdent, attrs: Vec) -> syn::Res Ok(builder) } -impl ToTokens for Wrapper { +impl ToTokens for Loader { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { let fn_ident = &self.fn_ident; + + let return_type = &self.fn_return; + let mut fn_inputs: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); let mut fn_inputs_builder: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); let mut fn_inputs_builder_options: Vec<_> = Vec::with_capacity(self.fn_inputs.len()); @@ -223,67 +217,32 @@ impl ToTokens for Wrapper { )) }; - let wrapper = quote! { - async fn __shuttle_wrapper( - #factory_ident: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { + let loader = quote! { + async fn loader( + mut #factory_ident: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> #return_type { use shuttle_service::Context; use shuttle_service::tracing_subscriber::prelude::*; #extra_imports - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); // this sets the subscriber as the global default and also adds a compatibility layer for capturing `log::Record`s - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - #(let #fn_inputs = #fn_inputs_builder::new()#fn_inputs_builder_options.build(#factory_ident, runtime).await.context(format!("failed to provision {}", stringify!(#fn_inputs_builder)))?;)* - - runtime.spawn(async { - #fn_ident(#(#fn_inputs),*) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + let filter_layer = + shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_service::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + #(let #fn_inputs = #fn_inputs_builder::new()#fn_inputs_builder_options.build(&mut #factory_ident).await.context(format!("failed to provision {}", stringify!(#fn_inputs_builder)))?;)* + + #fn_ident(#(#fn_inputs),*).await } }; - wrapper.to_tokens(tokens); + loader.to_tokens(tokens); } } @@ -293,7 +252,7 @@ mod tests { use quote::quote; use syn::{parse_quote, Ident}; - use super::{Builder, BuilderOptions, Input, Wrapper}; + use super::{Builder, BuilderOptions, Input, Loader}; #[test] fn from_with_return() { @@ -301,7 +260,7 @@ mod tests { async fn simple() -> ShuttleAxum {} ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(simple); assert_eq!(actual.fn_ident, expected_ident); @@ -310,65 +269,32 @@ mod tests { #[test] fn output_with_return() { - let input = Wrapper { + let input = Loader { fn_ident: parse_quote!(simple), fn_inputs: Vec::new(), + fn_return: parse_quote!(ShuttleSimple), }; let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - _factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { + async fn loader( + mut _factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleSimple { use shuttle_service::Context; use shuttle_service::tracing_subscriber::prelude::*; - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - runtime.spawn(async { - simple() - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + + let filter_layer = + shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_service::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + simple().await } }; @@ -381,7 +307,7 @@ mod tests { async fn complex(#[shuttle_shared_db::Postgres] pool: PgPool) -> ShuttleTide {} ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(complex); let expected_inputs: Vec = vec![Input { ident: parse_quote!(pool), @@ -408,7 +334,7 @@ mod tests { #[test] fn output_with_inputs() { - let input = Wrapper { + let input = Loader { fn_ident: parse_quote!(complex), fn_inputs: vec![ Input { @@ -426,67 +352,33 @@ mod tests { }, }, ], + fn_return: parse_quote!(ShuttleComplex), }; let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { + async fn loader( + mut factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleComplex { use shuttle_service::Context; use shuttle_service::tracing_subscriber::prelude::*; use shuttle_service::ResourceBuilder; - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - let pool = shuttle_shared_db::Postgres::new().build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; - let redis = shuttle_shared_db::Redis::new().build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Redis)))?; - - runtime.spawn(async { - complex(pool, redis) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + let filter_layer = + shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_service::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + let pool = shuttle_shared_db::Postgres::new().build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; + let redis = shuttle_shared_db::Redis::new().build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Redis)))?; + + complex(pool, redis).await } }; @@ -541,7 +433,7 @@ mod tests { } ); - let actual = Wrapper::from_item_fn(&mut input); + let actual = Loader::from_item_fn(&mut input).unwrap(); let expected_ident: Ident = parse_quote!(complex); let mut expected_inputs: Vec = vec![Input { ident: parse_quote!(pool), @@ -568,7 +460,7 @@ mod tests { #[test] fn output_with_input_options() { - let mut input = Wrapper { + let mut input = Loader { fn_ident: parse_quote!(complex), fn_inputs: vec![Input { ident: parse_quote!(pool), @@ -577,6 +469,7 @@ mod tests { options: Default::default(), }, }], + fn_return: parse_quote!(ShuttleComplex), }; input.fn_inputs[0] @@ -592,62 +485,27 @@ mod tests { let actual = quote!(#input); let expected = quote! { - async fn __shuttle_wrapper( - factory: &mut dyn shuttle_service::Factory, - runtime: &shuttle_service::Runtime, - logger: shuttle_service::Logger, - ) -> Result, shuttle_service::Error> { + async fn loader( + mut factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, + ) -> ShuttleComplex { use shuttle_service::Context; use shuttle_service::tracing_subscriber::prelude::*; use shuttle_service::ResourceBuilder; - runtime.spawn_blocking(move || { - let filter_layer = - shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() - .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) - .unwrap(); - - shuttle_service::tracing_subscriber::registry() - .with(filter_layer) - .with(logger) - .init(); - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked setting logger".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to set logger")) - } - })?; - - let pool = shuttle_shared_db::Postgres::new().size("10Gb").public(false).build(factory, runtime).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; - - runtime.spawn(async { - complex(pool) - .await - .map(|ok| Box::new(ok) as Box) - }) - .await - .map_err(|e| { - if e.is_panic() { - let mes = e - .into_panic() - .downcast_ref::<&str>() - .map(|x| x.to_string()) - .unwrap_or_else(|| "panicked calling main".to_string()); - - shuttle_service::Error::BuildPanic(mes) - } else { - shuttle_service::Error::Custom(shuttle_service::error::CustomError::new(e).context("failed to call main")) - } - })? + let filter_layer = + shuttle_service::tracing_subscriber::EnvFilter::try_from_default_env() + .or_else(|_| shuttle_service::tracing_subscriber::EnvFilter::try_new("INFO")) + .unwrap(); + + shuttle_service::tracing_subscriber::registry() + .with(filter_layer) + .with(logger) + .init(); + + let pool = shuttle_shared_db::Postgres::new().size("10Gb").public(false).build(&mut factory).await.context(format!("failed to provision {}", stringify!(shuttle_shared_db::Postgres)))?; + + complex(pool).await } }; diff --git a/proto/src/lib.rs b/proto/src/lib.rs index a4d51a5228..f27281d130 100644 --- a/proto/src/lib.rs +++ b/proto/src/lib.rs @@ -247,27 +247,32 @@ pub mod runtime { port: u16, get_runtime_executable: impl FnOnce() -> PathBuf, ) -> anyhow::Result<(process::Child, runtime_client::RuntimeClient)> { - let runtime_flag = if wasm { "--axum" } else { "--legacy" }; - let (storage_manager_type, storage_manager_path) = match storage_manager_type { StorageManagerType::Artifacts(path) => ("artifacts", path), StorageManagerType::WorkingDir(path) => ("working-dir", path), }; - let runtime_executable = get_runtime_executable(); + let port = &port.to_string(); + let storage_manager_path = &storage_manager_path.display().to_string(); + let runtime_executable_path = get_runtime_executable(); - let runtime = process::Command::new(runtime_executable) - .args([ - runtime_flag, + let args = if wasm { + vec!["--port", port] + } else { + vec![ "--port", - &port.to_string(), + port, "--provisioner-address", provisioner_address, "--storage-manager-type", storage_manager_type, "--storage-manager-path", - &storage_manager_path.display().to_string(), - ]) + storage_manager_path, + ] + }; + + let runtime = process::Command::new(runtime_executable_path) + .args(&args) .spawn() .context("spawning runtime process")?; diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b9b1ff8df3..b8ce5c2358 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -10,13 +10,15 @@ publish = false name = "rocket" [[bin]] -name = "next" +name = "shuttle-next" required-features = ["next"] [dependencies] anyhow = { workspace = true } async-trait = { workspace = true } +chrono = { workspace = true } clap ={ version = "4.0.18", features = ["derive"] } +serde_json = { workspace = true } thiserror = { workspace = true } tokio = { version = "1.22.0", features = ["full"] } tokio-stream = "0.1.11" diff --git a/runtime/src/bin/rocket.rs b/runtime/src/bin/rocket.rs index 8da00a54f7..9222f709d3 100644 --- a/runtime/src/bin/rocket.rs +++ b/runtime/src/bin/rocket.rs @@ -6,6 +6,7 @@ async fn main() { async fn loader( mut factory: shuttle_runtime::ProvisionerFactory, + logger: shuttle_runtime::Logger, ) -> shuttle_service::ShuttleRocket { use shuttle_service::ResourceBuilder; diff --git a/runtime/src/bin/next.rs b/runtime/src/bin/shuttle-next.rs similarity index 100% rename from runtime/src/bin/next.rs rename to runtime/src/bin/shuttle-next.rs diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index a040a86401..9cc3fa4b39 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -36,7 +36,7 @@ use tonic::{ use tracing::{error, instrument, trace}; use uuid::Uuid; -use crate::provisioner_factory::ProvisionerFactory; +use crate::{provisioner_factory::ProvisionerFactory, Logger}; use self::args::Args; @@ -100,21 +100,29 @@ where { type Service: Service; - async fn load(self, factory: Fac) -> Result; + async fn load( + self, + factory: Fac, + logger: Logger, + ) -> Result; } #[async_trait] impl Loader for F where - F: FnOnce(Fac) -> O + Send, + F: FnOnce(Fac, Logger) -> O + Send, O: Future> + Send, Fac: Factory + 'static, S: Service, { type Service = S; - async fn load(self, factory: Fac) -> Result { - (self)(factory).await + async fn load( + self, + factory: Fac, + logger: Logger, + ) -> Result { + (self)(factory, logger).await } } @@ -154,9 +162,12 @@ where ); trace!("got factory"); + let logs_tx = self.logs_tx.clone(); + let logger = Logger::new(logs_tx, deployment_id); + let loader = self.loader.lock().unwrap().deref_mut().take().unwrap(); - let service = loader.load(factory).await.unwrap(); + let service = loader.load(factory, logger).await.unwrap(); *self.service.lock().unwrap() = Some(service); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 33babf1c76..6e51ff0f49 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1,9 +1,12 @@ mod legacy; +mod logger; #[cfg(feature = "next")] mod next; mod provisioner_factory; pub use legacy::{start, Legacy}; +pub use logger::Logger; #[cfg(feature = "next")] pub use next::{AxumWasm, NextArgs}; pub use provisioner_factory::ProvisionerFactory; +pub use shuttle_common::storage_manager::StorageManager; diff --git a/service/src/logger.rs b/runtime/src/logger.rs similarity index 100% rename from service/src/logger.rs rename to runtime/src/logger.rs diff --git a/service/Cargo.toml b/service/Cargo.toml index ae3a29eb89..f9943a4d24 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -18,7 +18,6 @@ bincode = { version = "1.3.3", optional = true } # TODO: debug the libgit2-sys conflict with cargo-edit when upgrading cargo to 0.66 cargo = { version = "0.65.0", optional = true } cargo_metadata = "0.15.2" -chrono = { workspace = true } crossbeam-channel = "0.5.6" futures = { version = "0.3.25", features = ["std"] } hyper = { version = "0.14.23", features = ["server", "tcp", "http1"], optional = true } diff --git a/service/src/builder.rs b/service/src/builder.rs index 8702842066..3fcd8c67f6 100644 --- a/service/src/builder.rs +++ b/service/src/builder.rs @@ -62,13 +62,12 @@ pub async fn build_crate( check_no_panic(&ws)?; let opts = get_compile_options(&config, release_mode, is_next)?; - let compilation = compile(&ws, &opts); + let compilation = compile(&ws, &opts)?; - let path = compilation?.binaries[0].path.clone(); Ok(if is_next { - Runtime::Next(path) + Runtime::Next(compilation.cdylibs[0].path.clone()) } else { - Runtime::Legacy(path) + Runtime::Legacy(compilation.binaries[0].path.clone()) }) } diff --git a/service/src/lib.rs b/service/src/lib.rs index c7a6e189cd..de58ad0461 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -223,9 +223,6 @@ pub use tracing_subscriber; pub mod error; pub use error::Error; -mod logger; -pub use logger::Logger; - pub use shuttle_common::database; #[cfg(feature = "codegen")] From 4e1690d8cc5da7986513014d7bbd382303e741a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Thu, 9 Mar 2023 13:34:16 +0000 Subject: [PATCH 9/9] Feature: eng 483 trim and fix the tests in shuttle-service (#693) * tests: update build_crate tests * tests: remove loader tests * feat: cleanup service deps * feat: setup integration tests in runtime * feat: expected panic message for not_shuttle * refactor: simplify dummy provisioner * feat: re-export main and service from runtime --- Cargo.lock | 6 +- runtime/Cargo.toml | 5 + runtime/src/bin/rocket.rs | 2 +- runtime/src/legacy/mod.rs | 2 +- runtime/src/lib.rs | 1 + runtime/src/next/mod.rs | 2 +- runtime/tests/integration/helpers.rs | 95 +++++++++ runtime/tests/integration/loader.rs | 48 +++++ runtime/tests/integration/main.rs | 2 + .../tests/resources/bind-panic/Cargo.toml | 5 +- .../tests/resources/bind-panic/src/main.rs | 13 ++ service/Cargo.toml | 5 - service/src/builder.rs | 43 +++- service/tests/integration/build_crate.rs | 61 ++---- service/tests/integration/helpers/loader.rs | 28 --- service/tests/integration/helpers/mod.rs | 4 - service/tests/integration/helpers/sqlx.rs | 134 ------------ service/tests/integration/loader.rs | 192 ------------------ service/tests/integration/main.rs | 7 +- service/tests/resources/bind-panic/src/lib.rs | 18 -- .../tests/resources/build-panic/Cargo.toml | 12 -- .../tests/resources/build-panic/src/lib.rs | 18 -- service/tests/resources/is-bin/Cargo.toml | 12 ++ service/tests/resources/is-bin/src/main.rs | 6 + service/tests/resources/is-cdylib/src/lib.rs | 5 - .../{is-cdylib => not-bin}/Cargo.toml | 3 +- .../{not-cdylib => not-bin}/src/lib.rs | 2 + service/tests/resources/not-cdylib/Cargo.toml | 13 -- service/tests/resources/not-lib/Cargo.toml | 6 - service/tests/resources/not-lib/src/main.rs | 5 - .../tests/resources/not-shuttle/Cargo.toml | 8 +- .../tests/resources/not-shuttle/src/lib.rs | 1 - .../tests/resources/not-shuttle/src/main.rs | 6 + .../tests/resources/sleep-async/Cargo.toml | 13 -- .../tests/resources/sleep-async/src/lib.rs | 26 --- service/tests/resources/sleep/Cargo.toml | 12 -- service/tests/resources/sleep/src/lib.rs | 25 --- service/tests/resources/sqlx-pool/Cargo.toml | 14 -- service/tests/resources/sqlx-pool/src/lib.rs | 39 ---- 39 files changed, 261 insertions(+), 638 deletions(-) create mode 100644 runtime/tests/integration/helpers.rs create mode 100644 runtime/tests/integration/loader.rs create mode 100644 runtime/tests/integration/main.rs rename {service => runtime}/tests/resources/bind-panic/Cargo.toml (56%) create mode 100644 runtime/tests/resources/bind-panic/src/main.rs delete mode 100644 service/tests/integration/helpers/loader.rs delete mode 100644 service/tests/integration/helpers/mod.rs delete mode 100644 service/tests/integration/helpers/sqlx.rs delete mode 100644 service/tests/integration/loader.rs delete mode 100644 service/tests/resources/bind-panic/src/lib.rs delete mode 100644 service/tests/resources/build-panic/Cargo.toml delete mode 100644 service/tests/resources/build-panic/src/lib.rs create mode 100644 service/tests/resources/is-bin/Cargo.toml create mode 100644 service/tests/resources/is-bin/src/main.rs delete mode 100644 service/tests/resources/is-cdylib/src/lib.rs rename service/tests/resources/{is-cdylib => not-bin}/Cargo.toml (75%) rename service/tests/resources/{not-cdylib => not-bin}/src/lib.rs (71%) delete mode 100644 service/tests/resources/not-cdylib/Cargo.toml delete mode 100644 service/tests/resources/not-lib/Cargo.toml delete mode 100644 service/tests/resources/not-lib/src/main.rs delete mode 100644 service/tests/resources/not-shuttle/src/lib.rs create mode 100644 service/tests/resources/not-shuttle/src/main.rs delete mode 100644 service/tests/resources/sleep-async/Cargo.toml delete mode 100644 service/tests/resources/sleep-async/src/lib.rs delete mode 100644 service/tests/resources/sleep/Cargo.toml delete mode 100644 service/tests/resources/sleep/src/lib.rs delete mode 100644 service/tests/resources/sqlx-pool/Cargo.toml delete mode 100644 service/tests/resources/sqlx-pool/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c47655c270..5bfd7393de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6403,8 +6403,10 @@ dependencies = [ "cap-std", "chrono", "clap 4.0.27", + "crossbeam-channel", "futures", "hyper", + "portpicker", "rmp-serde", "rocket", "serde_json", @@ -6445,20 +6447,17 @@ dependencies = [ "cargo", "cargo_metadata", "crossbeam-channel", - "futures", "hyper", "num_cpus", "pipe", "poem", "poise", - "portpicker", "rocket", "salvo", "serde_json", "serenity", "shuttle-codegen", "shuttle-common", - "sqlx", "thiserror", "thruster", "tide", @@ -6466,7 +6465,6 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", - "uuid 1.2.2", "warp", ] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b8ce5c2358..36f83ee996 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -52,6 +52,11 @@ workspace = true workspace = true features = ["builder", "web-rocket"] # TODO: remove web-rocket +[dev-dependencies] +crossbeam-channel = "0.5.6" +portpicker = "0.1.1" +futures = { version = "0.3.25" } + [features] next = [ "cap-std", diff --git a/runtime/src/bin/rocket.rs b/runtime/src/bin/rocket.rs index 9222f709d3..db3836826b 100644 --- a/runtime/src/bin/rocket.rs +++ b/runtime/src/bin/rocket.rs @@ -6,7 +6,7 @@ async fn main() { async fn loader( mut factory: shuttle_runtime::ProvisionerFactory, - logger: shuttle_runtime::Logger, + _logger: shuttle_runtime::Logger, ) -> shuttle_service::ShuttleRocket { use shuttle_service::ResourceBuilder; diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index 9cc3fa4b39..cad51259c3 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -231,7 +231,7 @@ where let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx.send(format!("stopping deployment")).is_err() { + if kill_tx.send("stopping deployment".to_owned()).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6e51ff0f49..233616493a 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,3 +10,4 @@ pub use logger::Logger; pub use next::{AxumWasm, NextArgs}; pub use provisioner_factory::ProvisionerFactory; pub use shuttle_common::storage_manager::StorageManager; +pub use shuttle_service::{main, Error, Service}; diff --git a/runtime/src/next/mod.rs b/runtime/src/next/mod.rs index ccd30e321e..41f50eaac4 100644 --- a/runtime/src/next/mod.rs +++ b/runtime/src/next/mod.rs @@ -155,7 +155,7 @@ impl Runtime for AxumWasm { let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx.send(format!("stopping deployment")).is_err() { + if kill_tx.send("stopping deployment".to_owned()).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } diff --git a/runtime/tests/integration/helpers.rs b/runtime/tests/integration/helpers.rs new file mode 100644 index 0000000000..afa093f82f --- /dev/null +++ b/runtime/tests/integration/helpers.rs @@ -0,0 +1,95 @@ +use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use async_trait::async_trait; +use shuttle_proto::{ + provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseRequest, DatabaseResponse, + }, + runtime::{self, runtime_client::RuntimeClient}, +}; +use shuttle_service::builder::{build_crate, Runtime}; +use tonic::{ + transport::{Channel, Server}, + Request, Response, Status, +}; + +pub struct TestRuntime { + pub runtime_client: RuntimeClient, + pub bin_path: String, + pub service_name: String, + pub runtime_address: SocketAddr, + pub secrets: HashMap, +} + +pub async fn spawn_runtime(project_path: String, service_name: &str) -> Result { + let provisioner_address = SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + portpicker::pick_unused_port().unwrap(), + ); + let runtime_port = portpicker::pick_unused_port().unwrap(); + let runtime_address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), runtime_port); + + let (tx, _) = crossbeam_channel::unbounded(); + let runtime = build_crate(Path::new(&project_path), false, tx).await?; + + let secrets: HashMap = Default::default(); + + let (is_wasm, bin_path) = match runtime { + Runtime::Next(path) => (true, path), + Runtime::Legacy(path) => (false, path), + }; + + start_provisioner(DummyProvisioner, provisioner_address); + + // TODO: update this to work with shuttle-next projects, see cargo-shuttle local run + let runtime_path = || bin_path.clone(); + + let (_, runtime_client) = runtime::start( + is_wasm, + runtime::StorageManagerType::WorkingDir(PathBuf::from(project_path.clone())), + &format!("http://{}", provisioner_address), + runtime_port, + runtime_path, + ) + .await?; + + Ok(TestRuntime { + runtime_client, + bin_path: bin_path + .into_os_string() + .into_string() + .expect("to convert path to string"), + service_name: service_name.to_string(), + runtime_address, + secrets, + }) +} + +/// A dummy provisioner for tests, a provisioner connection is required +/// to start a project runtime. +pub struct DummyProvisioner; + +fn start_provisioner(provisioner: DummyProvisioner, address: SocketAddr) { + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(provisioner)) + .serve(address) + .await + }); +} + +#[async_trait] +impl Provisioner for DummyProvisioner { + async fn provision_database( + &self, + _request: Request, + ) -> Result, Status> { + panic!("did not expect any runtime test to use dbs") + } +} diff --git a/runtime/tests/integration/loader.rs b/runtime/tests/integration/loader.rs new file mode 100644 index 0000000000..d4051818a6 --- /dev/null +++ b/runtime/tests/integration/loader.rs @@ -0,0 +1,48 @@ +use std::time::Duration; + +use shuttle_proto::runtime::{LoadRequest, StartRequest}; +use uuid::Uuid; + +use crate::helpers::{spawn_runtime, TestRuntime}; + +/// This test does panic, but the panic happens in a spawned task inside the project runtime, +/// so we get this output: `thread 'tokio-runtime-worker' panicked at 'panic in bind', src/main.rs:6:9`, +/// but `should_panic(expected = "panic in bind")` doesn't catch it. +#[tokio::test] +#[should_panic(expected = "panic in bind")] +async fn bind_panic() { + let project_path = format!("{}/tests/resources/bind-panic", env!("CARGO_MANIFEST_DIR")); + + let TestRuntime { + bin_path, + service_name, + secrets, + mut runtime_client, + runtime_address, + } = spawn_runtime(project_path, "bind-panic").await.unwrap(); + + let load_request = tonic::Request::new(LoadRequest { + path: bin_path, + service_name, + secrets, + }); + + let _ = runtime_client.load(load_request).await.unwrap(); + + let start_request = StartRequest { + deployment_id: Uuid::default().as_bytes().to_vec(), + ip: runtime_address.to_string(), + }; + + // I also tried this without spawning, but it gave the same result. Panic but it isn't caught. + tokio::spawn(async move { + runtime_client + .start(tonic::Request::new(start_request)) + .await + .unwrap(); + // Give it a second to panic. + tokio::time::sleep(Duration::from_secs(1)).await; + }) + .await + .unwrap(); +} diff --git a/runtime/tests/integration/main.rs b/runtime/tests/integration/main.rs new file mode 100644 index 0000000000..e52d9c4830 --- /dev/null +++ b/runtime/tests/integration/main.rs @@ -0,0 +1,2 @@ +pub mod helpers; +pub mod loader; diff --git a/service/tests/resources/bind-panic/Cargo.toml b/runtime/tests/resources/bind-panic/Cargo.toml similarity index 56% rename from service/tests/resources/bind-panic/Cargo.toml rename to runtime/tests/resources/bind-panic/Cargo.toml index eb33318483..05c7925842 100644 --- a/service/tests/resources/bind-panic/Cargo.toml +++ b/runtime/tests/resources/bind-panic/Cargo.toml @@ -3,10 +3,9 @@ name = "bind-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] [workspace] [dependencies] -shuttle-service = { path = "../../../" } +shuttle-runtime = { path = "../../../" } +tokio = { version = "1.22.0" } diff --git a/runtime/tests/resources/bind-panic/src/main.rs b/runtime/tests/resources/bind-panic/src/main.rs new file mode 100644 index 0000000000..d7badcd695 --- /dev/null +++ b/runtime/tests/resources/bind-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + panic!("panic in bind"); + } +} + +#[shuttle_runtime::main] +async fn bind_panic() -> Result { + Ok(MyService) +} diff --git a/service/Cargo.toml b/service/Cargo.toml index f9943a4d24..ceb48a7efc 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -19,7 +19,6 @@ bincode = { version = "1.3.3", optional = true } cargo = { version = "0.65.0", optional = true } cargo_metadata = "0.15.2" crossbeam-channel = "0.5.6" -futures = { version = "0.3.25", features = ["std"] } hyper = { version = "0.14.23", features = ["server", "tcp", "http1"], optional = true } num_cpus = { version = "1.14.0", optional = true } pipe = "0.4.0" @@ -36,7 +35,6 @@ tokio = { version = "1.22.0", features = ["sync"] } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } warp = { version = "0.3.3", optional = true } # Tide does not have tokio support. So make sure async-std is compatible with tokio @@ -55,10 +53,7 @@ workspace = true features = ["tracing", "service"] [dev-dependencies] -portpicker = "0.1.1" -sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } tokio = { version = "1.22.0", features = ["macros", "rt"] } -uuid = { workspace = true, features = ["v4"] } [features] default = ["codegen"] diff --git a/service/src/builder.rs b/service/src/builder.rs index 3fcd8c67f6..1d1358a8b7 100644 --- a/service/src/builder.rs +++ b/service/src/builder.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, bail, Context}; use cargo::core::compiler::{CompileKind, CompileMode, CompileTarget, MessageFormat}; -use cargo::core::{Shell, Summary, Verbosity, Workspace}; +use cargo::core::{Manifest, Shell, Summary, Verbosity, Workspace}; use cargo::ops::{clean, compile, CleanOptions, CompileOptions}; use cargo::util::interning::InternedString; use cargo::util::{homedir, ToSemver}; @@ -51,14 +51,18 @@ pub async fn build_crate( let manifest_path = project_path.join("Cargo.toml"); let mut ws = Workspace::new(&manifest_path, &config)?; - let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure your Cargo.toml file specifies it as a library."))?; + let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure the `package` table is present in your Cargo.toml file."))?; let summary = current.manifest_mut().summary_mut(); - let is_next = is_next(summary); + if !is_next { check_version(summary)?; + ensure_binary(current.manifest())?; + } else { + ensure_cdylib(current.manifest_mut())?; } + check_no_panic(&ws)?; let opts = get_compile_options(&config, release_mode, is_next)?; @@ -173,6 +177,37 @@ fn is_next(summary: &Summary) -> bool { .any(|dependency| dependency.package_name() == NEXT_NAME) } +/// Make sure the project is a binary for legacy projects. +fn ensure_binary(manifest: &Manifest) -> anyhow::Result<()> { + if manifest.targets().iter().any(|target| target.is_bin()) { + Ok(()) + } else { + bail!("Your Shuttle project must be a binary.") + } +} + +/// Make sure "cdylib" is set for shuttle-next projects, else set it if possible. +fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> { + if let Some(target) = manifest + .targets_mut() + .iter_mut() + .find(|target| target.is_lib()) + { + if !target.is_cdylib() { + *target = cargo::core::manifest::Target::lib_target( + target.name(), + vec![cargo::core::compiler::CrateType::Cdylib], + target.src_path().path().unwrap().to_path_buf(), + target.edition(), + ); + } + + Ok(()) + } else { + bail!("Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file.") + } +} + /// Check that the crate being build is compatible with this version of loader fn check_version(summary: &Summary) -> anyhow::Result<()> { let valid_version = VERSION.to_semver().unwrap(); diff --git a/service/tests/integration/build_crate.rs b/service/tests/integration/build_crate.rs index 6c7e289eb1..f36a46d5aa 100644 --- a/service/tests/integration/build_crate.rs +++ b/service/tests/integration/build_crate.rs @@ -1,64 +1,39 @@ use std::path::{Path, PathBuf}; -use shuttle_service::loader::{build_crate, Runtime}; +use shuttle_service::builder::{build_crate, Runtime}; -#[tokio::test(flavor = "multi_thread")] +#[tokio::test] +#[should_panic(expected = "1 job failed")] async fn not_shuttle() { let (tx, _) = crossbeam_channel::unbounded(); let project_path = format!("{}/tests/resources/not-shuttle", env!("CARGO_MANIFEST_DIR")); - let so_path = match build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await - .unwrap() - { - Runtime::Legacy(path) => path, - _ => unreachable!(), - }; - - assert!( - so_path - .display() - .to_string() - .ends_with("tests/resources/not-shuttle/target/debug/libnot_shuttle.so"), - "did not get expected so_path: {}", - so_path.display() - ); + .unwrap(); } #[tokio::test] -#[should_panic( - expected = "Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file." -)] -async fn not_lib() { +#[should_panic(expected = "Your Shuttle project must be a binary.")] +async fn not_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-lib", env!("CARGO_MANIFEST_DIR")); - build_crate(Default::default(), Path::new(&project_path), false, tx) - .await - .unwrap(); + let project_path = format!("{}/tests/resources/not-bin", env!("CARGO_MANIFEST_DIR")); + match build_crate(Path::new(&project_path), false, tx).await { + Ok(_) => {} + Err(e) => panic!("{}", e.to_string()), + } } -#[tokio::test(flavor = "multi_thread")] -async fn not_cdylib() { +#[tokio::test] +async fn is_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-cdylib", env!("CARGO_MANIFEST_DIR")); - assert!(matches!( - build_crate(Default::default(), Path::new(&project_path), false, tx).await, - Ok(Runtime::Legacy(_)) - )); - assert!(PathBuf::from(project_path) - .join("target/debug/libnot_cdylib.so") - .exists()); -} + let project_path = format!("{}/tests/resources/is-bin", env!("CARGO_MANIFEST_DIR")); -#[tokio::test(flavor = "multi_thread")] -async fn is_cdylib() { - let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/is-cdylib", env!("CARGO_MANIFEST_DIR")); assert!(matches!( - build_crate(Default::default(), Path::new(&project_path), false, tx).await, + build_crate(Path::new(&project_path), false, tx).await, Ok(Runtime::Legacy(_)) )); assert!(PathBuf::from(project_path) - .join("target/debug/libis_cdylib.so") + .join("target/debug/is-bin") .exists()); } @@ -70,7 +45,7 @@ async fn not_found() { "{}/tests/resources/non-existing", env!("CARGO_MANIFEST_DIR") ); - build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await .unwrap(); } diff --git a/service/tests/integration/helpers/loader.rs b/service/tests/integration/helpers/loader.rs deleted file mode 100644 index 2d09238c24..0000000000 --- a/service/tests/integration/helpers/loader.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; - -use shuttle_service::loader::{Loader, LoaderError}; - -pub fn build_so_create_loader(resources: &str, crate_name: &str) -> Result { - let crate_dir: PathBuf = [resources, crate_name].iter().collect(); - - Command::new("cargo") - .args(["build", "--release"]) - .current_dir(&crate_dir) - .spawn() - .unwrap() - .wait() - .unwrap(); - - let dashes_replaced = crate_name.replace('-', "_"); - - let lib_name = if cfg!(target_os = "windows") { - format!("{}.dll", dashes_replaced) - } else { - format!("lib{}.so", dashes_replaced) - }; - - let so_path = crate_dir.join("target/release").join(lib_name); - - Loader::from_so_file(so_path) -} diff --git a/service/tests/integration/helpers/mod.rs b/service/tests/integration/helpers/mod.rs deleted file mode 100644 index e9b2e29f0c..0000000000 --- a/service/tests/integration/helpers/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod sqlx; - -#[cfg(feature = "loader")] -pub mod loader; diff --git a/service/tests/integration/helpers/sqlx.rs b/service/tests/integration/helpers/sqlx.rs deleted file mode 100644 index 79632e9676..0000000000 --- a/service/tests/integration/helpers/sqlx.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::future::Future; -use std::process::Command; -use std::thread::sleep; -use std::time::Duration; - -use portpicker::pick_unused_port; -use sqlx::Connection; - -pub struct PostgresInstance { - port: u16, - container: String, - password: String, -} - -impl Default for PostgresInstance { - fn default() -> Self { - Self::new() - } -} - -impl PostgresInstance { - /// Creates a new [`PostgresInstance`] using the official postgres:11 docker image - /// - /// Does not wait for the container to be ready. Use [`PostgresInstance::wait_for_ready`] and - /// [`PostgresInstance::wait_for_connectable`] for that. - pub fn new() -> Self { - let port = pick_unused_port().expect("could not find a free port for postgres"); - let container = "postgres-shuttle-service-integration-test".to_string(); - let password = "password".to_string(); - - Command::new("docker") - .args([ - "run", - "--name", - &container, - "-e", - &format!("POSTGRES_PASSWORD={}", password), - "-p", - &format!("{}:5432", port), - "postgres:11", // Our Containerfile image is based on buster which has postgres version 11 - ]) - .spawn() - .expect("failed to start a postgres instance"); - - Self { - port, - container, - password, - } - } - - pub fn get_uri(&self) -> String { - format!( - "postgres://postgres:{}@localhost:{}", - self.password, self.port - ) - } - - pub fn wait_for_connectable(&self) -> impl Future + '_ { - self.async_wait_for(|instance| { - let uri = instance.get_uri().as_str().to_string(); - async move { sqlx::PgConnection::connect(uri.as_str()).await.is_ok() } - }) - } - - pub async fn async_wait_for(&self, f: F) - where - F: Fn(&Self) -> Fut, - Fut: Future, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self).await { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } - - pub fn wait_for_ready(&self) { - self.wait_for(|instance| { - let status = Command::new("docker") - .args(["exec", &instance.container, "pg_isready"]) - .output() - .expect("failed to get postgres ready status") - .status; - - status.success() - }) - } - - pub fn wait_for(&self, f: F) - where - F: Fn(&Self) -> bool, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self) { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } -} - -impl Drop for PostgresInstance { - fn drop(&mut self) { - Command::new("docker") - .args(["stop", &self.container]) - .spawn() - .expect("failed to spawn stop for postgres container") - .wait() - .expect("postgres container stop failed"); - - Command::new("docker") - .args(["rm", &self.container]) - .spawn() - .expect("failed to spawn stop for remove container") - .wait() - .expect("postgres container remove failed"); - } -} diff --git a/service/tests/integration/loader.rs b/service/tests/integration/loader.rs deleted file mode 100644 index ba42c3807f..0000000000 --- a/service/tests/integration/loader.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::helpers::{loader::build_so_create_loader, sqlx::PostgresInstance}; - -use shuttle_common::log::Level; -use shuttle_common::LogItem; -use shuttle_service::loader::LoaderError; -use shuttle_service::{database, Error, Factory, Logger, ServiceName}; -use std::collections::BTreeMap; -use std::net::{Ipv4Addr, SocketAddr}; -use std::process::exit; -use std::str::FromStr; -use std::time::Duration; -use tokio::sync::mpsc::{self, UnboundedReceiver}; - -use async_trait::async_trait; - -const RESOURCES_PATH: &str = "tests/resources"; - -struct DummyFactory { - postgres_instance: Option, - service_name: ServiceName, -} - -impl DummyFactory { - fn new() -> Self { - Self { - postgres_instance: None, - service_name: ServiceName::from_str("test").unwrap(), - } - } -} - -fn get_logger() -> (Logger, UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - let logger = Logger::new(tx, Default::default()); - - (logger, rx) -} - -#[async_trait] -impl Factory for DummyFactory { - fn get_service_name(&self) -> ServiceName { - self.service_name.clone() - } - - async fn get_db_connection_string(&mut self, _: database::Type) -> Result { - let uri = if let Some(postgres_instance) = &self.postgres_instance { - postgres_instance.get_uri() - } else { - let postgres_instance = PostgresInstance::new(); - postgres_instance.wait_for_ready(); - postgres_instance.wait_for_connectable().await; - let uri = postgres_instance.get_uri(); - self.postgres_instance = Some(postgres_instance); - uri - }; - - Ok(uri) - } - - async fn get_secrets(&mut self) -> Result, Error> { - panic!("did not expect any loader test to get secrets") - } - - fn get_build_path(&self) -> Result { - panic!("did not expect any loader test to get the build path") - } - - fn get_storage_path(&self) -> Result { - panic!("did not expect any loader test to get the storage path") - } -} - -#[test] -fn not_shuttle() { - let result = build_so_create_loader(RESOURCES_PATH, "not-shuttle"); - assert!(matches!(result, Err(LoaderError::GetEntrypoint(_)))); -} - -#[tokio::test] -async fn sleep_async() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep-async").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as async service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sleep() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as blocking service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sqlx_pool() { - let loader = build_so_create_loader(RESOURCES_PATH, "sqlx-pool").unwrap(); - - // Make sure we'll get a log entry - std::env::set_var("RUST_LOG", "info"); - - // Don't initialize a pre-existing PostgresInstance here because the `PostgresInstance::wait_for_connectable()` - // code has `awaits` and we want to make sure they do not block inside `Service::build()`. - // At the same time we also want to test the PgPool is created on the correct runtime (ie does not cause a - // "has to run on a tokio runtime" error) - let mut factory = DummyFactory::new(); - - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, mut rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - handler.await.unwrap().unwrap(); - - let log = rx.recv().await.unwrap(); - let value = serde_json::from_slice::(&log.fields).unwrap(); - let message = value - .as_object() - .unwrap() - .get("message") - .unwrap() - .as_str() - .unwrap(); - assert!( - message.starts_with("SELECT 'Hello world';"), - "got: {}", - message - ); - assert_eq!(log.target, "sqlx::query"); - assert_eq!(log.level, Level::Info); -} - -#[tokio::test] -async fn build_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "build-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - if let Err(Error::BuildPanic(msg)) = loader.load(&mut factory, addr, logger).await { - assert_eq!(&msg, "panic in build"); - } else { - panic!("expected `Err(Error::BuildPanic(_))`"); - } -} - -#[tokio::test] -async fn bind_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "bind-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - let (handle, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - if let Err(Error::BindPanic(msg)) = handle.await.unwrap() { - assert_eq!(&msg, "panic in bind"); - } else { - panic!("expected `Err(Error::BindPanic(_))`"); - } -} diff --git a/service/tests/integration/main.rs b/service/tests/integration/main.rs index b09409f82f..f8f5ab1f1c 100644 --- a/service/tests/integration/main.rs +++ b/service/tests/integration/main.rs @@ -1,7 +1,2 @@ -pub mod helpers; - -#[cfg(feature = "loader")] -mod loader; - -#[cfg(feature = "loader")] +#[cfg(feature = "builder")] mod build_crate; diff --git a/service/tests/resources/bind-panic/src/lib.rs b/service/tests/resources/bind-panic/src/lib.rs deleted file mode 100644 index 1ecd700bf6..0000000000 --- a/service/tests/resources/bind-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - panic!("panic in bind"); - } -} - -#[shuttle_service::main] -async fn bind_panic() -> Result { - Ok(MyService) -} diff --git a/service/tests/resources/build-panic/Cargo.toml b/service/tests/resources/build-panic/Cargo.toml deleted file mode 100644 index 93a0b14e88..0000000000 --- a/service/tests/resources/build-panic/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "build-panic" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/build-panic/src/lib.rs b/service/tests/resources/build-panic/src/lib.rs deleted file mode 100644 index 084e5c8027..0000000000 --- a/service/tests/resources/build-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn build_panic() -> Result { - panic!("panic in build"); -} diff --git a/service/tests/resources/is-bin/Cargo.toml b/service/tests/resources/is-bin/Cargo.toml new file mode 100644 index 0000000000..52abbeab95 --- /dev/null +++ b/service/tests/resources/is-bin/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "is-bin" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +axum = "0.6.0" +shuttle-runtime = { path = "../../../../runtime" } +shuttle-service = { path = "../../../", features = ["web-axum"] } +tokio = { version = "1.22.0" } diff --git a/service/tests/resources/is-bin/src/main.rs b/service/tests/resources/is-bin/src/main.rs new file mode 100644 index 0000000000..21ffd20904 --- /dev/null +++ b/service/tests/resources/is-bin/src/main.rs @@ -0,0 +1,6 @@ +#[shuttle_service::main] +async fn axum() -> shuttle_service::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router) +} diff --git a/service/tests/resources/is-cdylib/src/lib.rs b/service/tests/resources/is-cdylib/src/lib.rs deleted file mode 100644 index 80a1cf6663..0000000000 --- a/service/tests/resources/is-cdylib/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { - let rocket = rocket::build(); - Ok(rocket) -} diff --git a/service/tests/resources/is-cdylib/Cargo.toml b/service/tests/resources/not-bin/Cargo.toml similarity index 75% rename from service/tests/resources/is-cdylib/Cargo.toml rename to service/tests/resources/not-bin/Cargo.toml index 99083de80c..8d34c0b740 100644 --- a/service/tests/resources/is-cdylib/Cargo.toml +++ b/service/tests/resources/not-bin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "is-cdylib" +name = "not-bin" version = "0.1.0" edition = "2021" @@ -11,3 +11,4 @@ crate-type = ["cdylib", "staticlib"] [dependencies] rocket = "0.5.0-rc.2" shuttle-service = { path = "../../../", features = ["web-rocket"] } +shuttle-runtime = { path = "../../../../runtime" } diff --git a/service/tests/resources/not-cdylib/src/lib.rs b/service/tests/resources/not-bin/src/lib.rs similarity index 71% rename from service/tests/resources/not-cdylib/src/lib.rs rename to service/tests/resources/not-bin/src/lib.rs index 80a1cf6663..329aed72cb 100644 --- a/service/tests/resources/not-cdylib/src/lib.rs +++ b/service/tests/resources/not-bin/src/lib.rs @@ -1,3 +1,5 @@ +// This will fail to compile since it's a library. + #[shuttle_service::main] async fn rocket() -> shuttle_service::ShuttleRocket { let rocket = rocket::build(); diff --git a/service/tests/resources/not-cdylib/Cargo.toml b/service/tests/resources/not-cdylib/Cargo.toml deleted file mode 100644 index 47a2afa537..0000000000 --- a/service/tests/resources/not-cdylib/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "not-cdylib" -version = "0.1.0" -edition = "2021" - -[workspace] - -[lib] -crate-type = ["staticlib"] - -[dependencies] -rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../", features = ["web-rocket"] } diff --git a/service/tests/resources/not-lib/Cargo.toml b/service/tests/resources/not-lib/Cargo.toml deleted file mode 100644 index 3c2b2f01cd..0000000000 --- a/service/tests/resources/not-lib/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "not-lib" -version = "0.1.0" -edition = "2021" - -[workspace] diff --git a/service/tests/resources/not-lib/src/main.rs b/service/tests/resources/not-lib/src/main.rs deleted file mode 100644 index 2fc1633f9f..0000000000 --- a/service/tests/resources/not-lib/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -// This will fail to compile since it is missing a `[lib]` section in its Cargo.toml file. - -fn main() { - println!("this is not valid as it is not a library!"); -} diff --git a/service/tests/resources/not-shuttle/Cargo.toml b/service/tests/resources/not-shuttle/Cargo.toml index b7fe259013..86af40f545 100644 --- a/service/tests/resources/not-shuttle/Cargo.toml +++ b/service/tests/resources/not-shuttle/Cargo.toml @@ -3,10 +3,10 @@ name = "not-shuttle" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - [workspace] [dependencies] -shuttle-service = "0.8.0" +axum = "0.6.0" +shuttle-runtime = { path = "../../../../runtime" } +shuttle-service = { path = "../../../", features = ["web-axum"] } +tokio = { version = "1.22.0" } diff --git a/service/tests/resources/not-shuttle/src/lib.rs b/service/tests/resources/not-shuttle/src/lib.rs deleted file mode 100644 index b25ff369ec..0000000000 --- a/service/tests/resources/not-shuttle/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// This service cannot be hosted on shuttle since it is missing the entrypoint the shutlle macros would have added!!! diff --git a/service/tests/resources/not-shuttle/src/main.rs b/service/tests/resources/not-shuttle/src/main.rs new file mode 100644 index 0000000000..9cf9d865bf --- /dev/null +++ b/service/tests/resources/not-shuttle/src/main.rs @@ -0,0 +1,6 @@ +// This service cannot be hosted on shuttle since it is missing the runtime the shuttle main macro would have added!!! +async fn axum() -> shuttle_service::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router) +} diff --git a/service/tests/resources/sleep-async/Cargo.toml b/service/tests/resources/sleep-async/Cargo.toml deleted file mode 100644 index e9fa47aae4..0000000000 --- a/service/tests/resources/sleep-async/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "sleep-async" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -tokio = { version = "1.22.0", features = ["time"] } -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep-async/src/lib.rs b/service/tests/resources/sleep-async/src/lib.rs deleted file mode 100644 index 585537560f..0000000000 --- a/service/tests/resources/sleep-async/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::time::Duration; - -use shuttle_service::Service; -use tokio::time::sleep; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration).await; - Ok(()) - } -} diff --git a/service/tests/resources/sleep/Cargo.toml b/service/tests/resources/sleep/Cargo.toml deleted file mode 100644 index 40b0f6d1fb..0000000000 --- a/service/tests/resources/sleep/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "sleep" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep/src/lib.rs b/service/tests/resources/sleep/src/lib.rs deleted file mode 100644 index ac67c50aec..0000000000 --- a/service/tests/resources/sleep/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::{thread::sleep, time::Duration}; - -use shuttle_service::Service; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration); - Ok(()) - } -} diff --git a/service/tests/resources/sqlx-pool/Cargo.toml b/service/tests/resources/sqlx-pool/Cargo.toml deleted file mode 100644 index 14487f75a8..0000000000 --- a/service/tests/resources/sqlx-pool/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "sqlx-pool" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } -shuttle-shared-db = { path = "../../../../resources/shared-db", features = ["postgres"] } -sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls" ] } diff --git a/service/tests/resources/sqlx-pool/src/lib.rs b/service/tests/resources/sqlx-pool/src/lib.rs deleted file mode 100644 index a3ae35690f..0000000000 --- a/service/tests/resources/sqlx-pool/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -use shuttle_service::error::CustomError; -use shuttle_service::Service; -use sqlx::PgPool; - -struct PoolService { - pool: PgPool, -} - -#[shuttle_service::main] -async fn init( - #[shuttle_shared_db::Postgres] pool: PgPool, -) -> Result { - Ok(PoolService { pool }) -} - -impl PoolService { - async fn start(&self) -> Result<(), shuttle_service::error::CustomError> { - let (rec,): (String,) = sqlx::query_as("SELECT 'Hello world'") - .fetch_one(&self.pool) - .await - .map_err(CustomError::new)?; - - assert_eq!(rec, "Hello world"); - - Ok(()) - } -} - -#[shuttle_service::async_trait] -impl Service for PoolService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - self.start().await?; - - Ok(()) - } -}