From 6b91d83fe265a352ade04ee483081eda47f99994 Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 13 Jul 2026 19:06:26 +0900 Subject: [PATCH] runtime: normalize filesystem options --- .yoi/tickets/00001KXDEVT13/artifacts/.gitkeep | 0 .yoi/tickets/00001KXDEVT13/item.md | 33 +++ .yoi/tickets/00001KXDEVT13/thread.md | 82 ++++++ crates/worker-runtime/src/main.rs | 240 ++++++++++-------- 4 files changed, 249 insertions(+), 106 deletions(-) create mode 100644 .yoi/tickets/00001KXDEVT13/artifacts/.gitkeep create mode 100644 .yoi/tickets/00001KXDEVT13/item.md create mode 100644 .yoi/tickets/00001KXDEVT13/thread.md diff --git a/.yoi/tickets/00001KXDEVT13/artifacts/.gitkeep b/.yoi/tickets/00001KXDEVT13/artifacts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/.yoi/tickets/00001KXDEVT13/item.md b/.yoi/tickets/00001KXDEVT13/item.md new file mode 100644 index 00000000..26da0976 --- /dev/null +++ b/.yoi/tickets/00001KXDEVT13/item.md @@ -0,0 +1,33 @@ +--- +title: 'Normalize worker runtime filesystem options' +state: 'done' +created_at: '2026-07-13T10:02:48Z' +updated_at: '2026-07-13T10:04:09Z' +assignee: null +queued_by: 'yoi ticket' +queued_at: '2026-07-13T10:04:08Z' +--- + +## 背景 + +`worker-runtime-rest-server` の filesystem option は、`fs-store` feature によって使える永続化境界として見えるべきである。現在の `--worker-store-dir` / `--worker-metadata-dir` / `--worker-runtime-base-dir` は内部実装の分割を直接露出しており、`--store memory` も「store backend」ではなく「永続 store を使わない ephemeral mode」を表している。 + +`fs-store` feature 付き build では filesystem-backed store が default で動き、明示的に永続化を切る場合だけ `--no-store` とする。 + +## 要件 + +- `--fs-root` は filesystem-backed storage 全体の top-level root とする。 +- `--fs-worker-dir` を追加し、Worker session / metadata / controller runtime / workdirless Worker root をまとめて導出する。 +- `--fs-runtime-dir` を追加し、Runtime catalog / Worker list / execution mapping / events の保存先にする。 +- `--workdir-target` を追加し、Workdir materialization target を指定できるようにする。 +- `--store ` は future backend switching 用に残し、`memory` は受け付けない。 +- 旧 `--store memory` は `--no-store` に置き換える。 +- 旧 `--worker-store-dir` / `--worker-metadata-dir` / `--worker-runtime-base-dir` は削除する。 + +## 受け入れ条件 + +- `--help` に新しい option 名が表示され、旧 worker path option は表示されない。 +- `--store memory` は reject され、`--no-store` は Runtime catalog persistence を無効化する。 +- `--fs-root ` だけで `/runtime`、`/worker`、`/workdirs` が default 派生される。 +- Worker Runtime server は新 option で起動できる。 +- worker-runtime / workspace-server の関連テストと Nix build が通る。 diff --git a/.yoi/tickets/00001KXDEVT13/thread.md b/.yoi/tickets/00001KXDEVT13/thread.md new file mode 100644 index 00000000..b4bcb80e --- /dev/null +++ b/.yoi/tickets/00001KXDEVT13/thread.md @@ -0,0 +1,82 @@ + + +## 作成 + +LocalTicketBackend によって作成されました。 + +--- + + + +## Intake summary + +Marked ready by `yoi ticket state`. + +--- + + + +## State changed + +Marked ready by `yoi ticket state`. + + +--- + + + +## State changed + +Ticket を `yoi ticket` が queued にしました。 + + +--- + + + +## State changed + +State changed to `inprogress`. + + +--- + + + +## Implementation report + +Implemented filesystem option cleanup for `worker-runtime-rest-server`. + +- Kept `--store ` as the only accepted store selector value. +- Added `--no-store` to represent the old ephemeral memory Runtime catalog mode. +- Changed `--fs-root` to mean the top-level filesystem-backed storage root. +- Added `--fs-worker-dir` for Worker storage, defaulting to `/worker`. +- Added `--fs-runtime-dir` for Runtime catalog storage, defaulting to `/runtime`. +- Added `--workdir-target` for Workdir materialization, defaulting to `/workdirs`. +- Removed the old `--worker-store-dir`, `--worker-metadata-dir`, and `--worker-runtime-base-dir` CLI options. +- Worker session store, Worker metadata store, Worker controller runtime base, and workdirless Worker root are now derived from `--fs-worker-dir`. + +Validation: +- `cargo fmt --check` +- `git diff --check` +- `cargo check -q` +- `cargo check -q -p worker-runtime --features fs-store,ws-server` +- `cargo test -q -p worker-runtime --features fs-store,ws-server` +- `cargo test -q -p yoi-workspace-server` +- `cargo run -q -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --help` +- `cargo run -q -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --store memory` rejects the old memory store selector +- `cargo run -q -p worker-runtime --features ws-server,fs-store --bin worker-runtime-rest-server -- --worker-store-dir /tmp/x` rejects the removed worker path option +- Started `worker-runtime-rest-server` with `--fs-root /tmp/yoi-runtime-fs-options-test` and verified `/v1/runtime` responds with `backend: fs_store` while using the derived runtime directory. +- `nix build .#yoi --no-link` + + +--- + + + +## State changed + +State changed to `done`. + + +--- diff --git a/crates/worker-runtime/src/main.rs b/crates/worker-runtime/src/main.rs index 0ee87007..36c9b533 100644 --- a/crates/worker-runtime/src/main.rs +++ b/crates/worker-runtime/src/main.rs @@ -66,17 +66,11 @@ fn run() -> Result<(), ProcessError> { } fn build_runtime(config: &ProcessConfig) -> Result { - let runtime_root = working_directory_runtime_root(config); - let mut factory = ProfileRuntimeWorkerFactory::new(runtime_root.join("worker-root")); - 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); - } + let fs_paths = config.resolved_fs_paths(); + let mut factory = ProfileRuntimeWorkerFactory::new(fs_paths.worker_dir.join("worker-root")) + .with_store_dir(fs_paths.worker_dir.join("sessions")) + .with_worker_metadata_dir(fs_paths.worker_dir.join("metadata")) + .with_runtime_base_dir(fs_paths.worker_dir.join("runtime")); if let Some(endpoint) = config.backend_resource_endpoint.clone() { factory = factory.with_resource_client(Arc::new( worker_runtime::resource::HttpBackendResourceClient::new( @@ -88,12 +82,11 @@ fn build_runtime(config: &ProcessConfig) -> Result { if let Some(profile) = config.profile.clone() { factory = factory.with_profile(profile); } - let working_directory_root = working_directory_runtime_root(config); let backend = Arc::new( WorkerRuntimeExecutionBackend::new(factory) .map_err(ProcessError::WorkerAdapter)? .with_working_directory_materializer(LocalGitWorktreeMaterializer::new( - working_directory_root, + fs_paths.workdir_target.clone(), )), ); @@ -115,22 +108,6 @@ fn build_runtime(config: &ProcessConfig) -> Result { } } -fn working_directory_runtime_root(config: &ProcessConfig) -> PathBuf { - match &config.http.store { - RuntimeHttpStoreSelection::Fs { root } => root.clone(), - _ => config - .worker_runtime_base_dir - .clone() - .unwrap_or_else(default_working_directory_runtime_root), - } -} - -fn default_working_directory_runtime_root() -> PathBuf { - manifest::paths::data_dir() - .map(|data_dir| data_dir.join("worker-runtime")) - .unwrap_or_else(|| env::temp_dir().join("yoi-worker-runtime")) -} - fn runtime_options_from_http(config: &RuntimeHttpServerConfig) -> RuntimeOptions { RuntimeOptions { display_name: config.display_name.clone(), @@ -138,10 +115,8 @@ fn runtime_options_from_http(config: &RuntimeHttpServerConfig) -> RuntimeOptions } } -fn default_fs_root() -> Option { - manifest::paths::data_dir() - .map(|data_dir| data_dir.join("worker-runtime-rest")) - .or_else(|| Some(env::temp_dir().join("yoi-worker-runtime-rest"))) +fn default_fs_root() -> PathBuf { + manifest::paths::data_dir().unwrap_or_else(|| env::temp_dir().join("yoi")) } fn parse_args(args: I) -> Result, ProcessError> @@ -150,9 +125,6 @@ where S: Into, { let mut config = ProcessConfig::default()?; - let mut store = StoreArg::Fs { - root: default_fs_root(), - }; let mut args = args.into_iter().map(Into::into).collect::>(); while let Some(arg) = args.pop_front() { @@ -168,16 +140,16 @@ where "--display-name" => { config.http.display_name = Some(take_value(&flag, inline_value, &mut args)?); } - "--worker-store-dir" => { - config.worker_store_dir = + "--fs-worker-dir" => { + config.fs_worker_dir = Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?)); } - "--worker-metadata-dir" => { - config.worker_metadata_dir = + "--fs-runtime-dir" => { + config.fs_runtime_dir = Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?)); } - "--worker-runtime-base-dir" => { - config.worker_runtime_base_dir = + "--workdir-target" => { + config.workdir_target = Some(PathBuf::from(take_value(&flag, inline_value, &mut args)?)); } "--backend-resource-endpoint" => { @@ -192,23 +164,19 @@ where } "--store" => { let value = take_value(&flag, inline_value, &mut args)?; - store = match value.as_str() { - "memory" => StoreArg::Memory, - "fs" | "fs-store" => StoreArg::Fs { - root: default_fs_root(), - }, - _ => { - return Err(ProcessError::usage(format!( - "unsupported --store `{value}`; expected `memory` or `fs`" - ))); - } - }; + if !matches!(value.as_str(), "fs" | "fs-store") { + return Err(ProcessError::usage(format!( + "unsupported --store `{value}`; only `fs` is accepted (use --no-store for ephemeral mode)" + ))); + } + } + "--no-store" => { + ensure_no_inline_value(&flag, inline_value.as_deref())?; + config.no_store = true; } "--fs-root" => { let value = take_value(&flag, inline_value, &mut args)?; - store = StoreArg::Fs { - root: Some(PathBuf::from(value)), - }; + config.fs_root = Some(PathBuf::from(value)); } "--local-token" => { let value = take_value(&flag, inline_value, &mut args)?; @@ -243,7 +211,7 @@ where } } - apply_store_selection(&mut config.http, store)?; + apply_store_selection(&mut config); Ok(Some(config)) } @@ -272,58 +240,84 @@ fn take_value( .ok_or_else(|| ProcessError::usage(format!("{flag} requires a value"))) } +fn ensure_no_inline_value(flag: &str, inline_value: Option<&str>) -> Result<(), ProcessError> { + if inline_value.is_some() { + return Err(ProcessError::usage(format!( + "{flag} does not accept a value" + ))); + } + Ok(()) +} + fn parse_usize_flag(flag: &str, value: String) -> Result { value .parse::() .map_err(|error| ProcessError::usage(format!("invalid {flag} value `{value}`: {error}"))) } -fn apply_store_selection( - config: &mut RuntimeHttpServerConfig, - store: StoreArg, -) -> Result<(), ProcessError> { - match store { - StoreArg::Memory => { - config.store = RuntimeHttpStoreSelection::Memory; - Ok(()) - } - StoreArg::Fs { root } => { - let root = root.unwrap_or_else(|| env::temp_dir().join("yoi-worker-runtime-rest")); - config.store = RuntimeHttpStoreSelection::Fs { root }; - Ok(()) - } +fn apply_store_selection(config: &mut ProcessConfig) { + if config.no_store { + config.http.store = RuntimeHttpStoreSelection::Memory; + } else { + let runtime_dir = config.resolved_fs_paths().runtime_dir; + config.http.store = RuntimeHttpStoreSelection::Fs { root: runtime_dir }; } } #[derive(Clone, Debug, PartialEq, Eq)] struct ProcessConfig { http: RuntimeHttpServerConfig, - worker_store_dir: Option, - worker_metadata_dir: Option, - worker_runtime_base_dir: Option, + fs_root: Option, + fs_worker_dir: Option, + fs_runtime_dir: Option, + workdir_target: Option, + no_store: bool, backend_resource_endpoint: Option, backend_resource_token: Option, profile: Option, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct FsPathConfig { + root: PathBuf, + worker_dir: PathBuf, + runtime_dir: PathBuf, + workdir_target: PathBuf, +} + impl ProcessConfig { fn default() -> Result { Ok(Self { http: RuntimeHttpServerConfig::default(), - worker_store_dir: None, - worker_metadata_dir: None, - worker_runtime_base_dir: None, + fs_root: None, + fs_worker_dir: None, + fs_runtime_dir: None, + workdir_target: None, + no_store: false, backend_resource_endpoint: None, backend_resource_token: None, profile: None, }) } -} -#[derive(Clone, Debug, PartialEq, Eq)] -enum StoreArg { - Memory, - Fs { root: Option }, + fn resolved_fs_paths(&self) -> FsPathConfig { + let root = self.fs_root.clone().unwrap_or_else(default_fs_root); + FsPathConfig { + worker_dir: self + .fs_worker_dir + .clone() + .unwrap_or_else(|| root.join("worker")), + runtime_dir: self + .fs_runtime_dir + .clone() + .unwrap_or_else(|| root.join("runtime")), + workdir_target: self + .workdir_target + .clone() + .unwrap_or_else(|| root.join("workdirs")), + root, + } + } } #[derive(Debug)] @@ -377,24 +371,27 @@ impl From for ProcessError { } fn usage() -> &'static str { - "Usage: worker-runtime-rest-server [OPTIONS]\n\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 Bind socket address (default: 127.0.0.1:38800)\n\ - --display-name Runtime display name\n\ - --profile Force spawned Workers to use a Profile selector\n\ - --worker-store-dir Worker session store directory\n\ - --worker-metadata-dir Worker metadata directory\n\ - --worker-runtime-base-dir Worker controller runtime directory\n\ - --backend-resource-endpoint Internal Backend resource fetch endpoint for resource handles\n\ - --backend-resource-token Optional bearer token for the Backend resource fetch endpoint\n\ - --store Runtime catalog store selection (default: fs)\n\ - --fs-root Runtime catalog filesystem store root (default: user data dir)\n\ - --local-token Minimal local bearer token placeholder\n\ - --local-token-env Read local bearer token placeholder from env\n\ - --max-event-batch-items Override event batch limit\n\ - -h, --help Show this help" + r#"Usage: worker-runtime-rest-server [OPTIONS] + +Starts a worker-backed Runtime REST command API for a trusted backend/proxy. +Browsers must not connect to this Runtime process directly. + +Options: + --bind Bind socket address (default: 127.0.0.1:38800) + --display-name Runtime display name + --profile Force spawned Workers to use a Profile selector + --fs-root Filesystem-backed storage root (default: user data dir) + --fs-worker-dir Worker storage directory (default: /worker) + --fs-runtime-dir Runtime catalog directory (default: /runtime) + --workdir-target Workdir materialization target (default: /workdirs) + --backend-resource-endpoint Internal Backend resource fetch endpoint for resource handles + --backend-resource-token Optional bearer token for the Backend resource fetch endpoint + --store Runtime catalog store kind (default: fs) + --no-store Disable Runtime catalog persistence for ephemeral runs + --local-token Minimal local bearer token placeholder + --local-token-env Read local bearer token placeholder from env + --max-event-batch-items Override event batch limit + -h, --help Show this help"# } #[cfg(test)] @@ -417,13 +414,21 @@ mod tests { } #[test] - fn rejects_removed_workspace_and_cwd_options() { + fn rejects_removed_workspace_cwd_and_worker_path_options() { assert!(parse_args(["--workspace", "/tmp/workspace"]).is_err()); assert!(parse_args(["--cwd", "/tmp/workspace"]).is_err()); + assert!(parse_args(["--worker-store-dir", "/tmp/sessions"]).is_err()); + assert!(parse_args(["--worker-metadata-dir", "/tmp/metadata"]).is_err()); + assert!(parse_args(["--worker-runtime-base-dir", "/tmp/runtime"]).is_err()); } #[test] - fn parses_memory_runtime_process_config() { + fn rejects_store_memory_in_favor_of_no_store() { + assert!(parse_args(["--store", "memory"]).is_err()); + } + + #[test] + fn parses_runtime_process_config() { let config = parse_args([ "--bind", "127.0.0.1:0", @@ -463,12 +468,35 @@ mod tests { #[test] fn parses_fs_store_runtime_process_config() { - let config = parse_args(["--store", "fs", "--fs-root", "/tmp/runtime-store"]) - .unwrap() - .unwrap(); + let config = parse_args([ + "--store", + "fs", + "--fs-root", + "/tmp/yoi", + "--fs-worker-dir", + "/tmp/yoi-worker", + "--fs-runtime-dir", + "/tmp/yoi-runtime", + "--workdir-target", + "/tmp/yoi-workdirs", + ]) + .unwrap() + .unwrap(); assert!(matches!( config.http.store, - RuntimeHttpStoreSelection::Fs { ref root } if root == &PathBuf::from("/tmp/runtime-store") + RuntimeHttpStoreSelection::Fs { ref root } if root == &PathBuf::from("/tmp/yoi-runtime") + )); + let paths = config.resolved_fs_paths(); + assert_eq!(paths.worker_dir, PathBuf::from("/tmp/yoi-worker")); + assert_eq!(paths.workdir_target, PathBuf::from("/tmp/yoi-workdirs")); + } + + #[test] + fn no_store_disables_runtime_catalog_persistence() { + let config = parse_args(["--no-store"]).unwrap().unwrap(); + assert!(matches!( + config.http.store, + RuntimeHttpStoreSelection::Memory )); } }