feat: live reload runtime connections
This commit is contained in:
+221
-104
@@ -1,8 +1,8 @@
|
||||
//! Minimal Runtime REST process wrapper.
|
||||
//! Worker-backed Runtime REST process wrapper.
|
||||
//!
|
||||
//! This binary is available only when the `http-server` feature is enabled. It
|
||||
//! starts a Runtime-local command API intended for a trusted backend/proxy;
|
||||
//! browsers must not connect to this Runtime process directly.
|
||||
//! This binary starts a Runtime command API with a real worker execution backend.
|
||||
//! A REST Runtime process that cannot spawn Workers is not a valid Runtime for the
|
||||
//! Workspace Browser.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::env;
|
||||
@@ -11,11 +11,16 @@ use std::fmt;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
|
||||
use worker_runtime::error::RuntimeError;
|
||||
use worker_runtime::fs_store::FsRuntimeStoreOptions;
|
||||
use worker_runtime::http_server::{
|
||||
RuntimeHttpServer, RuntimeHttpServerConfig, RuntimeHttpServerError, RuntimeHttpStoreSelection,
|
||||
RuntimeHttpServerConfig, RuntimeHttpServerError, RuntimeHttpStoreSelection,
|
||||
};
|
||||
use worker_runtime::identity::RuntimeId;
|
||||
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
|
||||
use worker_runtime::{Runtime, RuntimeOptions};
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
@@ -40,25 +45,77 @@ fn run() -> Result<(), ProcessError> {
|
||||
};
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
runtime.block_on(async move {
|
||||
let server = RuntimeHttpServer::bind(config).await?;
|
||||
let local_addr = server.local_addr()?;
|
||||
let listener = tokio::net::TcpListener::bind(config.http.bind_addr).await?;
|
||||
let local_addr = listener.local_addr()?;
|
||||
let worker_runtime = build_runtime(&config)?;
|
||||
eprintln!(
|
||||
"worker-runtime REST server listening on {local_addr}; intended client is a trusted backend/proxy, not a browser"
|
||||
);
|
||||
server.serve().await
|
||||
worker_runtime::http_server::serve_runtime_http(
|
||||
worker_runtime,
|
||||
listener,
|
||||
config.http.local_token,
|
||||
)
|
||||
.await
|
||||
.map_err(ProcessError::from)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_args<I, S>(args: I) -> Result<Option<RuntimeHttpServerConfig>, ProcessError>
|
||||
fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
|
||||
let mut factory = ProfileRuntimeWorkerFactory::new(config.workspace_root.clone())
|
||||
.with_cwd(config.cwd.clone());
|
||||
if let Some(store_dir) = config.worker_store_dir.clone() {
|
||||
factory = factory.with_store_dir(store_dir);
|
||||
}
|
||||
if let Some(metadata_dir) = config.worker_metadata_dir.clone() {
|
||||
factory = factory.with_worker_metadata_dir(metadata_dir);
|
||||
}
|
||||
if let Some(runtime_base_dir) = config.worker_runtime_base_dir.clone() {
|
||||
factory = factory.with_runtime_base_dir(runtime_base_dir);
|
||||
}
|
||||
if let Some(profile) = config.profile.clone() {
|
||||
factory = factory.with_profile(profile);
|
||||
}
|
||||
let backend =
|
||||
Arc::new(WorkerRuntimeExecutionBackend::new(factory).map_err(ProcessError::WorkerAdapter)?);
|
||||
|
||||
match &config.http.store {
|
||||
RuntimeHttpStoreSelection::Memory => {
|
||||
Runtime::with_execution_backend(runtime_options_from_http(&config.http), backend)
|
||||
.map_err(ProcessError::Runtime)
|
||||
}
|
||||
RuntimeHttpStoreSelection::Fs { root } => {
|
||||
let mut options = FsRuntimeStoreOptions::new(root.clone());
|
||||
options.runtime_id = config.http.runtime_id.clone();
|
||||
options.display_name = config.http.display_name.clone();
|
||||
options.limits = config.http.limits.clone();
|
||||
Runtime::with_fs_store_and_execution_backend(options, backend)
|
||||
.map_err(ProcessError::Runtime)
|
||||
}
|
||||
_ => Err(ProcessError::usage(
|
||||
"unsupported Runtime catalog store selection".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_options_from_http(config: &RuntimeHttpServerConfig) -> RuntimeOptions {
|
||||
RuntimeOptions {
|
||||
runtime_id: config.runtime_id.clone(),
|
||||
display_name: config.display_name.clone(),
|
||||
limits: config.limits.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_args<I, S>(args: I) -> Result<Option<ProcessConfig>, ProcessError>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: Into<String>,
|
||||
{
|
||||
let mut config = RuntimeHttpServerConfig::default();
|
||||
let mut config = ProcessConfig::default()?;
|
||||
let mut store = StoreArg::Memory;
|
||||
let mut args = args.into_iter().map(Into::into).collect::<VecDeque<_>>();
|
||||
|
||||
@@ -68,18 +125,39 @@ where
|
||||
"--help" | "-h" => return Ok(None),
|
||||
"--bind" => {
|
||||
let value = take_value(&flag, inline_value, &mut args)?;
|
||||
config.bind_addr = value.parse::<SocketAddr>().map_err(|error| {
|
||||
config.http.bind_addr = value.parse::<SocketAddr>().map_err(|error| {
|
||||
ProcessError::usage(format!("invalid --bind socket address `{value}`: {error}"))
|
||||
})?;
|
||||
}
|
||||
"--runtime-id" => {
|
||||
let value = take_value(&flag, inline_value, &mut args)?;
|
||||
config.runtime_id = Some(RuntimeId::new(value).ok_or_else(|| {
|
||||
config.http.runtime_id = Some(RuntimeId::new(value).ok_or_else(|| {
|
||||
ProcessError::usage("--runtime-id must not be empty".to_string())
|
||||
})?);
|
||||
}
|
||||
"--display-name" => {
|
||||
config.display_name = Some(take_value(&flag, inline_value, &mut args)?);
|
||||
config.http.display_name = Some(take_value(&flag, inline_value, &mut args)?);
|
||||
}
|
||||
"--workspace" => {
|
||||
config.workspace_root = PathBuf::from(take_value(&flag, inline_value, &mut args)?);
|
||||
}
|
||||
"--cwd" => {
|
||||
config.cwd = PathBuf::from(take_value(&flag, inline_value, &mut args)?);
|
||||
}
|
||||
"--worker-store-dir" => {
|
||||
config.worker_store_dir =
|
||||
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
||||
}
|
||||
"--worker-metadata-dir" => {
|
||||
config.worker_metadata_dir =
|
||||
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
||||
}
|
||||
"--worker-runtime-base-dir" => {
|
||||
config.worker_runtime_base_dir =
|
||||
Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?));
|
||||
}
|
||||
"--profile" => {
|
||||
config.profile = Some(take_value(&flag, inline_value, &mut args)?);
|
||||
}
|
||||
"--store" => {
|
||||
let value = take_value(&flag, inline_value, &mut args)?;
|
||||
@@ -106,7 +184,7 @@ where
|
||||
"--local-token must not be empty when provided".to_string(),
|
||||
));
|
||||
}
|
||||
config.local_token = Some(value);
|
||||
config.http.local_token = Some(value);
|
||||
}
|
||||
"--local-token-env" => {
|
||||
let name = take_value(&flag, inline_value, &mut args)?;
|
||||
@@ -120,14 +198,14 @@ where
|
||||
"--local-token-env `{name}` resolved to an empty value"
|
||||
)));
|
||||
}
|
||||
config.local_token = Some(value);
|
||||
config.http.local_token = Some(value);
|
||||
}
|
||||
"--max-transcript-projection-items" => {
|
||||
config.limits.max_transcript_projection_items =
|
||||
config.http.limits.max_transcript_projection_items =
|
||||
parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?;
|
||||
}
|
||||
"--max-event-batch-items" => {
|
||||
config.limits.max_event_batch_items =
|
||||
config.http.limits.max_event_batch_items =
|
||||
parse_usize_flag(&flag, take_value(&flag, inline_value, &mut args)?)?;
|
||||
}
|
||||
_ => {
|
||||
@@ -136,7 +214,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
apply_store_selection(&mut config, store)?;
|
||||
apply_store_selection(&mut config.http, store)?;
|
||||
if config.cwd.as_os_str().is_empty() {
|
||||
config.cwd = config.workspace_root.clone();
|
||||
}
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
@@ -180,31 +261,40 @@ fn apply_store_selection(
|
||||
config.store = RuntimeHttpStoreSelection::Memory;
|
||||
Ok(())
|
||||
}
|
||||
StoreArg::Fs { root } => apply_fs_store_selection(config, root),
|
||||
StoreArg::Fs { root } => {
|
||||
let root = root.ok_or_else(|| {
|
||||
ProcessError::usage("--store fs requires --fs-root <PATH>".to_string())
|
||||
})?;
|
||||
config.store = RuntimeHttpStoreSelection::Fs { root };
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
fn apply_fs_store_selection(
|
||||
config: &mut RuntimeHttpServerConfig,
|
||||
root: Option<PathBuf>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let root = root
|
||||
.ok_or_else(|| ProcessError::usage("--store fs requires --fs-root <PATH>".to_string()))?;
|
||||
config.store = RuntimeHttpStoreSelection::Fs { root };
|
||||
Ok(())
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ProcessConfig {
|
||||
http: RuntimeHttpServerConfig,
|
||||
workspace_root: PathBuf,
|
||||
cwd: PathBuf,
|
||||
worker_store_dir: Option<PathBuf>,
|
||||
worker_metadata_dir: Option<PathBuf>,
|
||||
worker_runtime_base_dir: Option<PathBuf>,
|
||||
profile: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
fn apply_fs_store_selection(
|
||||
_config: &mut RuntimeHttpServerConfig,
|
||||
root: Option<PathBuf>,
|
||||
) -> Result<(), ProcessError> {
|
||||
let _ = root;
|
||||
Err(ProcessError::usage(
|
||||
"fs store selection requires building worker-runtime with features `http-server,fs-store`"
|
||||
.to_string(),
|
||||
))
|
||||
impl ProcessConfig {
|
||||
fn default() -> Result<Self, ProcessError> {
|
||||
let workspace_root = env::current_dir()?;
|
||||
Ok(Self {
|
||||
http: RuntimeHttpServerConfig::default(),
|
||||
cwd: workspace_root.clone(),
|
||||
workspace_root,
|
||||
worker_store_dir: None,
|
||||
worker_metadata_dir: None,
|
||||
worker_runtime_base_dir: None,
|
||||
profile: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -217,6 +307,8 @@ enum StoreArg {
|
||||
enum ProcessError {
|
||||
Usage(String),
|
||||
Server(RuntimeHttpServerError),
|
||||
Runtime(RuntimeError),
|
||||
WorkerAdapter(String),
|
||||
Io(std::io::Error),
|
||||
}
|
||||
|
||||
@@ -231,6 +323,8 @@ impl fmt::Display for ProcessError {
|
||||
match self {
|
||||
Self::Usage(message) => message.fmt(f),
|
||||
Self::Server(error) => error.fmt(f),
|
||||
Self::Runtime(error) => error.fmt(f),
|
||||
Self::WorkerAdapter(error) => error.fmt(f),
|
||||
Self::Io(error) => error.fmt(f),
|
||||
}
|
||||
}
|
||||
@@ -239,8 +333,9 @@ impl fmt::Display for ProcessError {
|
||||
impl Error for ProcessError {
|
||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
||||
match self {
|
||||
Self::Usage(_) => None,
|
||||
Self::Usage(_) | Self::WorkerAdapter(_) => None,
|
||||
Self::Server(error) => Some(error),
|
||||
Self::Runtime(error) => Some(error),
|
||||
Self::Io(error) => Some(error),
|
||||
}
|
||||
}
|
||||
@@ -260,14 +355,20 @@ impl From<std::io::Error> for ProcessError {
|
||||
|
||||
fn usage() -> &'static str {
|
||||
"Usage: worker-runtime-rest-server [OPTIONS]\n\n\
|
||||
Starts the worker-runtime REST command API for a trusted backend/proxy.\n\
|
||||
Starts a worker-backed Runtime REST command API for a trusted backend/proxy.\n\
|
||||
Browsers must not connect to this Runtime process directly.\n\n\
|
||||
Options:\n\
|
||||
--bind <ADDR> Bind socket address (default: 127.0.0.1:0)\n\
|
||||
--bind <ADDR> Bind socket address (default: 127.0.0.1:38800)\n\
|
||||
--runtime-id <ID> Runtime authority id (default: generated)\n\
|
||||
--display-name <NAME> Runtime display name\n\
|
||||
--store <memory|fs> Store selection (default: memory)\n\
|
||||
--fs-root <PATH> Filesystem store root; requires fs-store feature\n\
|
||||
--workspace <PATH> Workspace root used for spawned Workers (default: cwd)\n\
|
||||
--cwd <PATH> Process cwd used for spawned Workers (default: workspace)\n\
|
||||
--profile <SELECTOR> Force spawned Workers to use a Profile selector\n\
|
||||
--worker-store-dir <PATH> Worker session store directory\n\
|
||||
--worker-metadata-dir <PATH> Worker metadata directory\n\
|
||||
--worker-runtime-base-dir <PATH> Worker controller runtime directory\n\
|
||||
--store <memory|fs> Runtime catalog store selection (default: memory)\n\
|
||||
--fs-root <PATH> Runtime catalog filesystem store root\n\
|
||||
--local-token <TOKEN> Minimal local bearer token placeholder\n\
|
||||
--local-token-env <ENV> Read local bearer token placeholder from env\n\
|
||||
--max-transcript-projection-items <N> Override transcript projection limit\n\
|
||||
@@ -279,68 +380,84 @@ Options:\n\
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_memory_runtime_process_config() {
|
||||
let config = parse_args([
|
||||
"--bind",
|
||||
"127.0.0.1:48181",
|
||||
"--runtime-id=runtime-review",
|
||||
"--display-name",
|
||||
"review runtime",
|
||||
"--store",
|
||||
"memory",
|
||||
"--local-token",
|
||||
"local-placeholder",
|
||||
"--max-transcript-projection-items",
|
||||
"32",
|
||||
"--max-event-batch-items=16",
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.bind_addr,
|
||||
"127.0.0.1:48181".parse::<SocketAddr>().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
config.runtime_id.as_ref().map(RuntimeId::as_str),
|
||||
Some("runtime-review")
|
||||
);
|
||||
assert_eq!(config.display_name.as_deref(), Some("review runtime"));
|
||||
assert!(matches!(config.store, RuntimeHttpStoreSelection::Memory));
|
||||
assert_eq!(config.local_token.as_deref(), Some("local-placeholder"));
|
||||
assert_eq!(config.limits.max_transcript_projection_items, 32);
|
||||
assert_eq!(config.limits.max_event_batch_items, 16);
|
||||
}
|
||||
|
||||
#[cfg(feature = "fs-store")]
|
||||
#[test]
|
||||
fn parses_fs_store_runtime_process_config_when_feature_enabled() {
|
||||
let config = parse_args(["--fs-root", "/tmp/yoi-worker-runtime-store"])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
config.store,
|
||||
RuntimeHttpStoreSelection::Fs { ref root }
|
||||
if root == &PathBuf::from("/tmp/yoi-worker-runtime-store")
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "fs-store"))]
|
||||
#[test]
|
||||
fn rejects_fs_store_runtime_process_config_without_feature() {
|
||||
let error = parse_args(["--store", "fs", "--fs-root", "/tmp/store"]).unwrap_err();
|
||||
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("requires building worker-runtime with features")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_does_not_start_server() {
|
||||
assert!(parse_args(["--help"]).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_bind_addr_is_stable_for_local_dev() {
|
||||
let config = parse_args([] as [&str; 0]).unwrap().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
config.http.bind_addr,
|
||||
"127.0.0.1:38800".parse::<SocketAddr>().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_memory_runtime_process_config() {
|
||||
let config = parse_args([
|
||||
"--bind",
|
||||
"127.0.0.1:0",
|
||||
"--runtime-id=runtime-alpha",
|
||||
"--display-name",
|
||||
"Runtime Alpha",
|
||||
"--workspace",
|
||||
"/tmp/workspace",
|
||||
"--cwd",
|
||||
"/tmp/workspace/subdir",
|
||||
"--profile",
|
||||
"builtin:coder",
|
||||
"--local-token-env",
|
||||
"TEST_RUNTIME_TOKEN",
|
||||
]);
|
||||
assert!(config.is_err());
|
||||
|
||||
unsafe {
|
||||
env::set_var("TEST_RUNTIME_TOKEN", "secret-token");
|
||||
}
|
||||
let config = parse_args([
|
||||
"--bind",
|
||||
"127.0.0.1:0",
|
||||
"--runtime-id=runtime-alpha",
|
||||
"--display-name",
|
||||
"Runtime Alpha",
|
||||
"--workspace",
|
||||
"/tmp/workspace",
|
||||
"--cwd",
|
||||
"/tmp/workspace/subdir",
|
||||
"--profile",
|
||||
"builtin:coder",
|
||||
"--local-token-env",
|
||||
"TEST_RUNTIME_TOKEN",
|
||||
])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
unsafe {
|
||||
env::remove_var("TEST_RUNTIME_TOKEN");
|
||||
}
|
||||
|
||||
assert_eq!(config.http.bind_addr, "127.0.0.1:0".parse().unwrap());
|
||||
assert_eq!(
|
||||
config.http.runtime_id.as_ref().map(ToString::to_string),
|
||||
Some("runtime-alpha".to_string())
|
||||
);
|
||||
assert_eq!(config.http.display_name.as_deref(), Some("Runtime Alpha"));
|
||||
assert_eq!(config.workspace_root, PathBuf::from("/tmp/workspace"));
|
||||
assert_eq!(config.cwd, PathBuf::from("/tmp/workspace/subdir"));
|
||||
assert_eq!(config.profile.as_deref(), Some("builtin:coder"));
|
||||
assert_eq!(config.http.local_token.as_deref(), Some("secret-token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_fs_store_runtime_process_config() {
|
||||
let config = parse_args(["--store", "fs", "--fs-root", "/tmp/runtime-store"])
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
config.http.store,
|
||||
RuntimeHttpStoreSelection::Fs { ref root } if root == &PathBuf::from("/tmp/runtime-store")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user