Skip to content

Commit 37ade4c

Browse files
authored
feat: create a control plane interface (part 1) (#436)
* feat: add proto for runtimes * refactor: legacy move main to lib * feat: impl Runtime server for legacy * feat: impl Runtime server for next
1 parent e773225 commit 37ade4c

File tree

15 files changed

+335
-105
lines changed

15 files changed

+335
-105
lines changed

runtimes/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
members = [
33
"legacy",
44
"next",
5+
"proto",
56
"wasm"
67
]

runtimes/legacy/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ publish = false
66
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
77

88
[dependencies]
9+
anyhow = "1.0.62"
910
async-trait = "0.1.58"
1011
clap ={ version = "4.0.18", features = ["derive"] }
1112
thiserror = "1.0.37"
@@ -18,6 +19,10 @@ tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
1819
version = "0.7.0"
1920
path = "../../common"
2021

22+
[dependencies.shuttle-runtime-proto]
23+
version = "0.1.0"
24+
path = "../proto"
25+
2126
[dependencies.shuttle-service]
2227
version = "0.7.0"
2328
default-features = false

runtimes/legacy/README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,15 @@
22

33
Load and run an .so library that implements `shuttle_service::Service`.
44

5-
To load and run, pass the path to the .so file to load as an argument to the shuttle-next binary:
5+
To test, first start this binary using:
66

77
```bash
8-
cargo run -- -f "src/libhello_world.so"
8+
cargo run --
9+
```
10+
11+
Then in another shell, load a `.so` file and start it up:
12+
13+
``` bash
14+
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "../../examples/rocket/hello-world/target/debug/libhello_world.so"}' localhost:8000 runtime.Runtime/load
15+
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/start
916
```

runtimes/legacy/src/args.rs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,6 @@ use tonic::transport::Endpoint;
33

44
#[derive(Parser, Debug)]
55
pub struct Args {
6-
/// Uri to the `.so` file to load
7-
#[arg(long, short)]
8-
pub file_path: String,
9-
106
/// Address to reach provisioner at
117
#[clap(long, default_value = "localhost:5000")]
128
pub provisioner_address: Endpoint,

runtimes/legacy/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ pub enum Error {
77
Load(#[from] LoaderError),
88
#[error("Run error: {0}")]
99
Run(#[from] shuttle_service::Error),
10+
#[error("Start error: {0}")]
11+
Start(#[from] shuttle_service::error::CustomError),
1012
}
1113

1214
pub type Result<T> = std::result::Result<T, Error>;

runtimes/legacy/src/lib.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,151 @@
1+
use std::{
2+
collections::BTreeMap,
3+
net::{Ipv4Addr, SocketAddr},
4+
path::PathBuf,
5+
str::FromStr,
6+
sync::Mutex,
7+
};
8+
9+
use anyhow::anyhow;
10+
use async_trait::async_trait;
11+
use shuttle_common::{database, LogItem};
12+
use shuttle_runtime_proto::runtime::{
13+
runtime_server::Runtime, LoadRequest, LoadResponse, StartRequest, StartResponse,
14+
};
15+
use shuttle_service::{
16+
loader::{LoadedService, Loader},
17+
Factory, Logger, ServiceName,
18+
};
19+
use tokio::sync::mpsc::{self, UnboundedReceiver};
20+
use tonic::{Request, Response, Status};
21+
use tracing::{info, instrument, trace};
22+
123
pub mod args;
224
pub mod error;
25+
26+
pub struct Legacy {
27+
// Mutexes are for interior mutability
28+
so_path: Mutex<Option<PathBuf>>,
29+
port: Mutex<Option<u16>>,
30+
}
31+
32+
impl Legacy {
33+
pub fn new() -> Self {
34+
Self {
35+
so_path: Mutex::new(None),
36+
port: Mutex::new(None),
37+
}
38+
}
39+
}
40+
41+
#[async_trait]
42+
impl Runtime for Legacy {
43+
async fn load(&self, request: Request<LoadRequest>) -> Result<Response<LoadResponse>, Status> {
44+
let so_path = request.into_inner().path;
45+
trace!(so_path, "loading");
46+
47+
let so_path = PathBuf::from(so_path);
48+
*self.so_path.lock().unwrap() = Some(so_path);
49+
50+
let message = LoadResponse { success: true };
51+
Ok(Response::new(message))
52+
}
53+
54+
async fn start(
55+
&self,
56+
_request: Request<StartRequest>,
57+
) -> Result<Response<StartResponse>, Status> {
58+
let port = 8001;
59+
let address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
60+
let mut factory = DummyFactory::new();
61+
let (logger, _rx) = get_logger();
62+
let so_path = self
63+
.so_path
64+
.lock()
65+
.unwrap()
66+
.as_ref()
67+
.ok_or_else(|| -> error::Error {
68+
error::Error::Start(anyhow!("trying to start a service that was not loaded"))
69+
})
70+
.map_err(|err| Status::from_error(Box::new(err)))?
71+
.clone();
72+
73+
trace!(%address, "starting");
74+
let service = load_service(address, so_path, &mut factory, logger)
75+
.await
76+
.unwrap();
77+
78+
_ = tokio::spawn(run(service, address));
79+
80+
*self.port.lock().unwrap() = Some(port);
81+
82+
let message = StartResponse {
83+
success: true,
84+
port: Some(port as u32),
85+
};
86+
87+
Ok(Response::new(message))
88+
}
89+
}
90+
91+
#[instrument(skip(service))]
92+
async fn run(service: LoadedService, addr: SocketAddr) {
93+
let (handle, library) = service;
94+
95+
info!("starting deployment on {}", addr);
96+
handle.await.unwrap().unwrap();
97+
98+
tokio::spawn(async move {
99+
trace!("closing .so file");
100+
library.close().unwrap();
101+
});
102+
}
103+
104+
#[instrument(skip(addr, so_path, factory, logger))]
105+
async fn load_service(
106+
addr: SocketAddr,
107+
so_path: PathBuf,
108+
factory: &mut dyn Factory,
109+
logger: Logger,
110+
) -> error::Result<LoadedService> {
111+
let loader = Loader::from_so_file(so_path)?;
112+
113+
Ok(loader.load(factory, addr, logger).await?)
114+
}
115+
116+
struct DummyFactory {
117+
service_name: ServiceName,
118+
}
119+
120+
impl DummyFactory {
121+
fn new() -> Self {
122+
Self {
123+
service_name: ServiceName::from_str("legacy").unwrap(),
124+
}
125+
}
126+
}
127+
128+
#[async_trait]
129+
impl Factory for DummyFactory {
130+
fn get_service_name(&self) -> ServiceName {
131+
self.service_name.clone()
132+
}
133+
134+
async fn get_db_connection_string(
135+
&mut self,
136+
_: database::Type,
137+
) -> Result<String, shuttle_service::Error> {
138+
todo!()
139+
}
140+
141+
async fn get_secrets(&mut self) -> Result<BTreeMap<String, String>, shuttle_service::Error> {
142+
todo!()
143+
}
144+
}
145+
146+
fn get_logger() -> (Logger, UnboundedReceiver<LogItem>) {
147+
let (tx, rx) = mpsc::unbounded_channel();
148+
let logger = Logger::new(tx, Default::default());
149+
150+
(logger, rx)
151+
}

runtimes/legacy/src/main.rs

Lines changed: 10 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
use std::{collections::BTreeMap, net::SocketAddr, path::PathBuf, str::FromStr};
1+
use std::net::{Ipv4Addr, SocketAddr};
22

3-
use async_trait::async_trait;
43
use clap::Parser;
5-
use shuttle_common::{database, LogItem};
6-
use shuttle_legacy::args::Args;
7-
use shuttle_service::{
8-
loader::{LoadedService, Loader},
9-
Factory, Logger, ServiceName,
10-
};
11-
use tokio::sync::mpsc::{self, UnboundedReceiver};
12-
use tracing::{info, instrument, trace};
4+
use shuttle_legacy::{args::Args, Legacy};
5+
use shuttle_runtime_proto::runtime::runtime_server::RuntimeServer;
6+
use tonic::transport::Server;
7+
use tracing::trace;
138
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
149

1510
#[tokio::main(flavor = "multi_thread")]
@@ -28,76 +23,11 @@ async fn main() {
2823

2924
trace!(args = ?args, "parsed args");
3025

31-
let address: SocketAddr = "127.0.0.1:8000".parse().unwrap();
32-
let mut factory = DummyFactory::new();
33-
let (logger, _rx) = get_logger();
34-
let so_path = PathBuf::from(args.file_path.as_str());
35-
36-
let service = load_service(address, so_path, &mut factory, logger)
26+
let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8000);
27+
let legacy = Legacy::new();
28+
Server::builder()
29+
.add_service(RuntimeServer::new(legacy))
30+
.serve(addr)
3731
.await
3832
.unwrap();
39-
40-
_ = tokio::spawn(run(service, address)).await;
41-
}
42-
43-
#[instrument(skip(service))]
44-
async fn run(service: LoadedService, addr: SocketAddr) {
45-
let (handle, library) = service;
46-
47-
info!("starting deployment on {}", addr);
48-
handle.await.unwrap().unwrap();
49-
50-
tokio::spawn(async move {
51-
trace!("closing .so file");
52-
library.close().unwrap();
53-
});
54-
}
55-
56-
#[instrument(skip(addr, so_path, factory, logger))]
57-
async fn load_service(
58-
addr: SocketAddr,
59-
so_path: PathBuf,
60-
factory: &mut dyn Factory,
61-
logger: Logger,
62-
) -> shuttle_legacy::error::Result<LoadedService> {
63-
let loader = Loader::from_so_file(so_path)?;
64-
65-
Ok(loader.load(factory, addr, logger).await?)
66-
}
67-
68-
struct DummyFactory {
69-
service_name: ServiceName,
70-
}
71-
72-
impl DummyFactory {
73-
fn new() -> Self {
74-
Self {
75-
service_name: ServiceName::from_str("next").unwrap(),
76-
}
77-
}
78-
}
79-
80-
#[async_trait]
81-
impl Factory for DummyFactory {
82-
fn get_service_name(&self) -> ServiceName {
83-
self.service_name.clone()
84-
}
85-
86-
async fn get_db_connection_string(
87-
&mut self,
88-
_: database::Type,
89-
) -> Result<String, shuttle_service::Error> {
90-
todo!()
91-
}
92-
93-
async fn get_secrets(&mut self) -> Result<BTreeMap<String, String>, shuttle_service::Error> {
94-
todo!()
95-
}
96-
}
97-
98-
fn get_logger() -> (Logger, UnboundedReceiver<LogItem>) {
99-
let (tx, rx) = mpsc::unbounded_channel();
100-
let logger = Logger::new(tx, Default::default());
101-
102-
(logger, rx)
10333
}

runtimes/next/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@ async-trait = "0.1.58"
1313
clap ={ version = "4.0.18", features = ["derive"] }
1414
tokio = { version = "1.20.1", features = [ "full" ] }
1515
tonic = "0.8.0"
16+
tracing = "0.1.37"
17+
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
1618

1719
cap-std = "*"
1820
wasmtime = "*"
1921
wasmtime-wasi = "*"
2022
wasi-common = "*"
2123

2224
serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"] }
25+
26+
[dependencies.shuttle-runtime-proto]
27+
version = "0.1.0"
28+
path = "../proto"

runtimes/next/README.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44

55
```bash
66
$ cd ..; make wasm
7-
$ DISCORD_TOKEN=xxx BOT_SRC=bot.wasm cargo run
7+
$ DISCORD_TOKEN=xxx cargo run
8+
```
9+
10+
In another terminal:
11+
12+
``` bash
13+
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic", "path": "bot.wasm"}' localhost:8000 runtime.Runtime/load
14+
grpcurl -plaintext -import-path ../proto -proto runtime.proto -d '{"service_name": "Tonic"}' localhost:8000 runtime.Runtime/start
815
```
916

1017
## Running the tests

0 commit comments

Comments
 (0)