From 278058ba935e1e55923dd8d4ea88b7ad321082b1 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Thu, 30 Jul 2026 12:57:16 -0700 Subject: [PATCH] feat(engine-process): support caller-defined engine instance paths --- engine/packages/cli/src/commands/dev.rs | 1 + .../packages/engine-process/src/lib.rs | 86 ++++++++++++++----- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/engine/packages/cli/src/commands/dev.rs b/engine/packages/cli/src/commands/dev.rs index 893f816eb4..996856323e 100644 --- a/engine/packages/cli/src/commands/dev.rs +++ b/engine/packages/cli/src/commands/dev.rs @@ -510,6 +510,7 @@ mod tests { endpoint: DEFAULT_ENGINE_ENDPOINT.to_string(), bind_host: bind_host.to_string(), public_url: public_url.map(str::to_string), + instance_path: None, } } diff --git a/rivetkit-rust/packages/engine-process/src/lib.rs b/rivetkit-rust/packages/engine-process/src/lib.rs index 7e96885e48..503567c7b5 100644 --- a/rivetkit-rust/packages/engine-process/src/lib.rs +++ b/rivetkit-rust/packages/engine-process/src/lib.rs @@ -31,6 +31,11 @@ struct EngineHealthResponse { pub struct EngineResolverConfig { pub endpoint: String, pub explicit_binary_path: Option, + /// Directory for mutable state owned by this managed engine instance. + /// + /// Embedders can set this to isolate multiple managed engines. When unset, + /// the legacy shared engine paths are used. + pub instance_path: Option, pub bind_host: Option, pub bind_port: Option, pub public_url: Option, @@ -50,6 +55,7 @@ impl EngineResolverConfig { Self { endpoint: endpoint.to_owned(), explicit_binary_path, + instance_path: None, bind_host, bind_port, public_url: None, @@ -77,6 +83,8 @@ pub struct EngineRuntimeStamp { pub endpoint: String, pub bind_host: String, pub public_url: Option, + #[serde(default)] + pub instance_path: Option, } impl EngineRuntimeStamp { @@ -155,7 +163,7 @@ impl EngineProcessManager { return Ok(Self { watcher: None, startup: EngineStartup::Reused { - stamp: read_engine_stamp(), + stamp: read_engine_stamp(config), }, }); } @@ -174,7 +182,7 @@ impl EngineProcessManager { return Ok(Self { watcher: None, startup: EngineStartup::Reused { - stamp: read_engine_stamp(), + stamp: read_engine_stamp(config), }, }); } @@ -188,11 +196,8 @@ impl EngineProcessManager { let env = engine_env(config)?; let config_path = write_engine_config(config)?; - let db_path = engine_db_path()?; - let logs_dir = storage_root()? - .join("var") - .join("logs") - .join("rivet-engine"); + let db_path = engine_db_path(config)?; + let logs_dir = engine_logs_dir(config)?; ensure_dir(&db_path).context("create engine db directory")?; ensure_dir(&logs_dir).context("create engine logs directory")?; @@ -284,8 +289,9 @@ impl EngineProcessManager { endpoint: endpoint.clone(), bind_host: resolve_bind_host(config)?, public_url: config.public_url.clone(), + instance_path: config.instance_path.clone(), }; - if let Err(error) = write_engine_stamp(&stamp) { + if let Err(error) = write_engine_stamp(config, &stamp) { tracing::warn!(?error, "failed to write engine runtime stamp"); } @@ -296,16 +302,35 @@ impl EngineProcessManager { } } -/// Path to the rivet-engine database directory under the shared storage root. -pub fn engine_db_path() -> Result { - Ok(storage_root()?.join("var").join("engine").join("db")) +/// Path to the rivet-engine database directory. +pub fn engine_db_path(config: &EngineResolverConfig) -> Result { + Ok(engine_instance_path(config)?.join("db")) } -fn engine_stamp_path() -> Result { - Ok(storage_root()? - .join("var") - .join("engine") - .join("runtime.json")) +/// Default directory for mutable state owned by a managed engine instance. +pub fn default_engine_instance_path() -> Result { + Ok(storage_root()?.join("var").join("engine")) +} + +fn engine_instance_path(config: &EngineResolverConfig) -> Result { + match &config.instance_path { + Some(path) => Ok(path.clone()), + None => default_engine_instance_path(), + } +} + +fn engine_stamp_path(config: &EngineResolverConfig) -> Result { + Ok(engine_instance_path(config)?.join("runtime.json")) +} + +fn engine_logs_dir(config: &EngineResolverConfig) -> Result { + match &config.instance_path { + Some(path) => Ok(path.join("logs")), + None => Ok(storage_root()? + .join("var") + .join("logs") + .join("rivet-engine")), + } } /// The address the engine actually binds: the explicit `bind_host` override, or @@ -323,8 +348,8 @@ fn resolve_bind_host(config: &EngineResolverConfig) -> Result { .to_owned()) } -fn write_engine_stamp(stamp: &EngineRuntimeStamp) -> Result<()> { - let path = engine_stamp_path()?; +fn write_engine_stamp(config: &EngineResolverConfig, stamp: &EngineRuntimeStamp) -> Result<()> { + let path = engine_stamp_path(config)?; if let Some(dir) = path.parent() { ensure_dir(dir)?; } @@ -339,8 +364,8 @@ fn write_engine_stamp(stamp: &EngineRuntimeStamp) -> Result<()> { /// callers treat the binding as unknown rather than trusting old data. On /// platforms without a liveness check (see `pid_is_alive`) the stamp is always /// treated as unknown for the same reason. -fn read_engine_stamp() -> Option { - let path = engine_stamp_path().ok()?; +fn read_engine_stamp(config: &EngineResolverConfig) -> Option { + let path = engine_stamp_path(config).ok()?; let contents = std::fs::read(&path).ok()?; let stamp: EngineRuntimeStamp = serde_json::from_slice(&contents).ok()?; pid_is_alive(stamp.pid).then_some(stamp) @@ -384,7 +409,7 @@ pub fn engine_env(config: &EngineResolverConfig) -> Result .checked_add(10) .ok_or_else(|| invalid_endpoint(endpoint, "port is too large"))?; - let db_path = engine_db_path()?; + let db_path = engine_db_path(config)?; Ok(vec![ ("RIVET__GUARD__HOST".to_owned(), guard_host.clone()), @@ -411,7 +436,7 @@ fn write_engine_config(config: &EngineResolverConfig) -> Result> let public_url = Url::parse(public_url) .with_context(|| format!("parse engine public URL `{public_url}`"))?; let peer_url = peer_url_for_public_url(&public_url)?; - let dir = storage_root()?.join("var").join("engine"); + let dir = engine_instance_path(config)?; ensure_dir(&dir)?; let path = dir.join("config.json"); let config = serde_json::json!({ @@ -950,6 +975,7 @@ mod tests { EngineResolverConfig { endpoint: "http://127.0.0.1:1".to_owned(), explicit_binary_path: None, + instance_path: None, bind_host: None, bind_port: None, public_url: None, @@ -965,9 +991,25 @@ mod tests { endpoint: "http://127.0.0.1:6420".to_owned(), bind_host: bind_host.to_owned(), public_url: None, + instance_path: None, } } + #[test] + fn caller_defined_instance_path_scopes_mutable_state() { + let temp = tempfile::tempdir().expect("create temp dir"); + let instance_path = temp.path().join("engine-6421"); + let mut config = test_config(String::new(), false); + config.instance_path = Some(instance_path.clone()); + + assert_eq!(engine_db_path(&config).unwrap(), instance_path.join("db")); + assert_eq!( + engine_stamp_path(&config).unwrap(), + instance_path.join("runtime.json") + ); + assert_eq!(engine_logs_dir(&config).unwrap(), instance_path.join("logs")); + } + #[test] fn binds_loopback_only_detects_loopback_addresses() { assert!(stamp_with_bind("127.0.0.1").binds_loopback_only());