diff --git a/docs/user-guide/en/runtime/blaze.md b/docs/user-guide/en/runtime/blaze.md index fc28a94591..960b4f6dbd 100644 --- a/docs/user-guide/en/runtime/blaze.md +++ b/docs/user-guide/en/runtime/blaze.md @@ -88,6 +88,13 @@ the affected sandbox ID. Blaze leaves the rejected record in place. Repair or restore that record, then restart the service and confirm that `/v1/health` responds. +## Sandbox API + +Blaze exposes sandbox lifecycle and guest operations under `/v1/sandboxes`. +Clients use this namespace to list, create, inspect, and delete sandboxes and +to execute commands, read files, and write files inside them. Sandbox +destruction uses `DELETE /v1/sandboxes/{id}`. + ## Host Integration Boundary Blaze configures the sandbox-local network path. Routing beyond the host and DNS @@ -96,7 +103,7 @@ option in production, configure the required upstream routing or translation and verify guest connectivity for the host environment. To disable the capability, set `enable_network = false` or remove the key, then -destroy existing network-enabled sandboxes through the normal instance API. +destroy existing network-enabled sandboxes through the sandbox API. ## Guest Operations @@ -116,7 +123,6 @@ The sandbox routes are: - `POST /v1/sandboxes/{id}/read` — read one file; and - `POST /v1/sandboxes/{id}/write` — replace one file. -The corresponding `/v1/instances/{id}/...` routes provide the same behavior. Exec requests use the following shape: ```json @@ -153,13 +159,7 @@ Leave `listen.http_addr` disabled in production until Daemon shutdown also does not yet wait for every active HTTP handler or release all runtime owners, so an in-flight request may observe a closed connection. -## Reset and Reusable-Instance Management - -`POST /v1/instances/{id}/reset` does not report success until Blaze can reset -both runtime and storage. A malformed identifier returns HTTP 400, an unknown -instance returns HTTP 404, an instance that is not running returns HTTP 422, -and a running instance returns HTTP 501 without changing its state or owned -resources. +## Reusable-Instance Management The four `/v1/pools` management routes also return HTTP 501. Blaze rejects `storage.pool_size`, `storage.prefork`, and every `[pool]` section except the diff --git a/docs/user-guide/zh/runtime/blaze.md b/docs/user-guide/zh/runtime/blaze.md index cb55515d8b..ceef6eb89f 100644 --- a/docs/user-guide/zh/runtime/blaze.md +++ b/docs/user-guide/zh/runtime/blaze.md @@ -74,6 +74,12 @@ Blaze 会在绑定 Unix listener 或可选 TCP listener 之前完成清单校验 ID。Blaze 会保留被拒绝的记录。修复或恢复该记录后,重新启动 service,并确认 `/v1/health` 可以响应。 +## 沙箱 API + +Blaze 通过 `/v1/sandboxes` 提供沙箱生命周期和客户机操作。客户端使用该 +命名空间列出、创建、查看和删除沙箱,以及在沙箱内执行命令、读取文件和写入 +文件。销毁沙箱使用 `DELETE /v1/sandboxes/{id}`。 + ## 主机集成边界 Blaze 负责配置 sandbox 本地的网络路径。主机以外的路由和 DNS 仍由主机运维方 @@ -81,7 +87,7 @@ Blaze 负责配置 sandbox 本地的网络路径。主机以外的路由和 DNS 环境中验证 guest 连通性。 如需关闭该能力,将 `enable_network` 设置为 `false` 或删除该配置项,再通过 -正常的 instance API 销毁已经启用网络的 sandbox。 +沙箱 API 销毁已经启用网络的 sandbox。 ## Guest 操作 @@ -99,7 +105,7 @@ Sandbox 路由包括: - `POST /v1/sandboxes/{id}/read` — 读取一个文件; - `POST /v1/sandboxes/{id}/write` — 替换一个文件。 -对应的 `/v1/instances/{id}/...` 路由提供相同行为。Exec 请求格式如下: +Exec 请求格式如下: ```json {"cmd":"uname -a","cwd":"/","env":{"LANG":"C"},"timeout":10} @@ -130,12 +136,7 @@ read 响应过大时返回 HTTP 502 和 保持 `listen.http_addr` 关闭。Daemon 停止时也不会等待全部 HTTP handler 或 释放所有 runtime owner,因此正在执行的请求可能看到连接关闭。 -## 重置与可复用实例管理 - -在 Blaze 能够同时重置运行环境和存储之前, -`POST /v1/instances/{id}/reset` 不会返回成功。实例编号格式错误时返回 -HTTP 400,实例不存在时返回 HTTP 404,实例不处于运行状态时返回 HTTP 422, -运行中的实例返回 HTTP 501,且不会改变其状态或已占用资源。 +## 可复用实例管理 四个 `/v1/pools` 管理接口同样返回 HTTP 501。`storage.pool_size` 和 `storage.prefork` 始终会被拒绝;除历史软件包的精确默认值外,任何 `[pool]` diff --git a/src/blaze/README.md b/src/blaze/README.md index 2eafb972be..fc0435cce3 100644 --- a/src/blaze/README.md +++ b/src/blaze/README.md @@ -136,6 +136,8 @@ for configuration, selection, retry, and worker shutdown behavior. ## API Endpoints +Blaze exposes sandbox lifecycle and guest operations through `/v1/sandboxes`. + | Method | Path | Description | |--------|------|-------------| | GET | `/v1/health` | Health check | @@ -146,16 +148,6 @@ for configuration, selection, retry, and worker shutdown behavior. | POST | `/v1/sandboxes/{id}/exec` | Execute a guest command | | POST | `/v1/sandboxes/{id}/read` | Read a guest file | | POST | `/v1/sandboxes/{id}/write` | Replace a guest file | -| GET | `/v1/instances` | Alias for listing sandboxes | -| POST | `/v1/instances` | Alias for creating a sandbox | -| GET | `/v1/instances/{id}` | Alias for sandbox details | -| DELETE | `/v1/instances/{id}` | Alias for destroying a sandbox | -| POST | `/v1/instances/{id}/destroy` | Compatible destroy action | -| POST | `/v1/instances/{id}/exec` | Compatible guest command action | -| POST | `/v1/instances/{id}/read` | Compatible guest file read action | -| POST | `/v1/instances/{id}/write` | Compatible guest file write action | -| POST | `/v1/instances/{id}/checkpoint` | Record checkpoint state | -| POST | `/v1/instances/{id}/reset` | Reserved for running instances; returns `501` until runtime and storage reset are implemented | | GET | `/v1/pools` | Reserved; returns `501` | | GET | `/v1/pools/{backend}/{class}` | Reserved; returns `501` | | POST | `/v1/pools/{backend}/{class}/drain` | Reserved; returns `501` | @@ -168,13 +160,6 @@ for configuration, selection, retry, and worker shutdown behavior. | GET | `/v1/metrics` | Prometheus metrics | | POST | `/v1/admin/reload` | Hot-reload policies | -For reset requests, a malformed instance identifier returns `400`, an unknown -instance returns `404`, and an instance that is not running returns `422`. A -running instance returns `501` without changing its lifecycle state or its -runtime and storage resources. Clients that require a fresh sandbox must -successfully destroy the old sandbox and create a new one; `501` does not mean -that reset completed. - Upgrade compatibility accepts and ignores only this exact daemon section: ```toml @@ -251,10 +236,10 @@ cleanup, and failure boundaries. The operation journal records the operation and start time, not completion of each resource step. An interrupted create is cleaned up rather than resumed, and an existing backend process is not adopted after restart. Failed recovery -does not run in a background retry loop. The checkpoint endpoint retains its -existing metadata transition. Reset remains unavailable until runtime and -storage can be reset together; this recovery flow does not add backend snapshot -or restore operations. +does not run in a background retry loop. Checkpoint capture and restore are not +available in this change. Reset remains unavailable until runtime and storage +can be reset together; this recovery flow does not add backend snapshot, +capture, or restore operations. ### Guest operations diff --git a/src/blaze/README_zh.md b/src/blaze/README_zh.md index e834093778..4c1cc51457 100644 --- a/src/blaze/README_zh.md +++ b/src/blaze/README_zh.md @@ -127,6 +127,8 @@ lock 和唯一的同步许可直至完成;后续同步会被推迟而不会不 ## API 端点 +Blaze 通过 `/v1/sandboxes` 提供沙箱生命周期和客户机操作。 + | 方法 | 路径 | 说明 | |--------|------|-------------| | GET | `/v1/health` | 健康检查 | @@ -137,16 +139,6 @@ lock 和唯一的同步许可直至完成;后续同步会被推迟而不会不 | POST | `/v1/sandboxes/{id}/exec` | 执行 guest 命令 | | POST | `/v1/sandboxes/{id}/read` | 读取 guest 文件 | | POST | `/v1/sandboxes/{id}/write` | 替换 guest 文件 | -| GET | `/v1/instances` | 列出 sandbox 的兼容入口 | -| POST | `/v1/instances` | 创建 sandbox 的兼容入口 | -| GET | `/v1/instances/{id}` | 获取 sandbox 详情的兼容入口 | -| DELETE | `/v1/instances/{id}` | 销毁 sandbox 的兼容入口 | -| POST | `/v1/instances/{id}/destroy` | 保留的销毁 action | -| POST | `/v1/instances/{id}/exec` | Guest 命令兼容入口 | -| POST | `/v1/instances/{id}/read` | Guest 文件读取兼容入口 | -| POST | `/v1/instances/{id}/write` | Guest 文件写入兼容入口 | -| POST | `/v1/instances/{id}/checkpoint` | 记录 checkpoint 状态 | -| POST | `/v1/instances/{id}/reset` | 运行中实例的预留接口;运行时和存储重置实现前返回 `501` | | GET | `/v1/pools` | 预留接口;返回 `501` | | GET | `/v1/pools/{backend}/{class}` | 预留接口;返回 `501` | | POST | `/v1/pools/{backend}/{class}/drain` | 预留接口;返回 `501` | @@ -159,11 +151,6 @@ lock 和唯一的同步许可直至完成;后续同步会被推迟而不会不 | GET | `/v1/metrics` | Prometheus 指标 | | POST | `/v1/admin/reload` | 热加载策略 | -重置请求中的实例编号格式错误时返回 `400`,实例不存在时返回 `404`,实例 -不处于运行状态时返回 `422`。运行中的实例返回 `501`,且不会改变其生命周期 -状态,也不会改变其运行环境和存储资源。需要全新沙箱的客户端必须先成功销毁 -旧沙箱,再创建新沙箱;`501` 不表示重置已经完成。 - 升级兼容仅接受并忽略以下内容完全一致的 daemon `[pool]` 配置段: ```toml @@ -224,9 +211,9 @@ daemon 才会逐个处理未结束的 sandbox。后续逐项恢复期间,如 操作记录只保存操作类型和开始时间,不记录每个资源步骤是否已经完成。中断的 创建会被清理而不是从原位置继续,重启后也不会接管先前的后端进程。恢复失败 -后目前没有后台循环自动重试。检查点接口保持原有的元数据状态变化。重置接口 -在运行环境和存储能够一起重置前不可用;这里的恢复流程没有增加后端快照或 -恢复操作。 +后目前没有后台循环自动重试。本次变更不提供检查点捕获或恢复。重置接口在 +运行环境和存储能够一起重置前不可用;这里的恢复流程没有增加后端快照、捕获 +或恢复操作。 ### Guest 操作 diff --git a/src/blaze/crates/blazed/src/api.rs b/src/blaze/crates/blazed/src/api.rs index a45e76b989..a928add4db 100644 --- a/src/blaze/crates/blazed/src/api.rs +++ b/src/blaze/crates/blazed/src/api.rs @@ -13,8 +13,7 @@ use std::sync::Arc; use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use blaze_core::backend::{BackendKind, BackendStatus, select_backend}; -use blaze_core::kernel::HookKind; -use blaze_core::lifecycle::{SandboxInstance, SandboxState, StartPath}; +use blaze_core::lifecycle::{SandboxInstance, StartPath}; use blaze_core::policy::{ImageMetadata, RuntimeDecision, WorkloadClass}; use http_body_util::{BodyExt, Full}; use hyper::body::{Body, Bytes, Incoming}; @@ -54,6 +53,7 @@ where let method = req.method().clone(); let path = req.uri().path().to_string(); let query = req.uri().query().unwrap_or("").to_string(); + let limit = guest_body_route(&method, &path).then_some(MAX_GUEST_HTTP_BODY_BYTES); let response = match collect_body(req, limit).await { @@ -79,12 +79,7 @@ fn guest_body_route(method: &Method, path: &str) -> bool { .collect::>(); matches!( parts.as_slice(), - [ - "v1", - "instances" | "sandboxes", - _, - "exec" | "read" | "write" - ] + ["v1", "sandboxes", _, "exec" | "read" | "write"] ) } @@ -138,27 +133,13 @@ async fn dispatch( match (m, parts.as_slice()) { ("GET", ["v1", "health"]) => health(state), - ("GET", ["v1", "instances"]) | ("GET", ["v1", "sandboxes"]) => list_instances(state), - ("POST", ["v1", "instances"]) | ("POST", ["v1", "sandboxes"]) => { - create_instance(state, &body).await - } - ("GET", ["v1", "instances", id]) | ("GET", ["v1", "sandboxes", id]) => { - get_instance(state, id) - } - ("POST", ["v1", "sandboxes", id, "exec"]) | ("POST", ["v1", "instances", id, "exec"]) => { - exec_instance(state, id, &body).await - } - ("POST", ["v1", "sandboxes", id, "read"]) | ("POST", ["v1", "instances", id, "read"]) => { - read_instance_file(state, id, &body).await - } - ("POST", ["v1", "sandboxes", id, "write"]) | ("POST", ["v1", "instances", id, "write"]) => { - write_instance_file(state, id, &body).await - } - ("POST", ["v1", "instances", id, "checkpoint"]) => checkpoint(state, id).await, - ("POST", ["v1", "instances", id, "reset"]) => reset_instance(state, id).await, - ("DELETE", ["v1", "instances", id]) - | ("DELETE", ["v1", "sandboxes", id]) - | ("POST", ["v1", "instances", id, "destroy"]) => destroy_instance(state, id).await, + ("GET", ["v1", "sandboxes"]) => list_sandboxes(state), + ("POST", ["v1", "sandboxes"]) => create_sandbox(state, &body).await, + ("GET", ["v1", "sandboxes", id]) => get_sandbox(state, id), + ("POST", ["v1", "sandboxes", id, "exec"]) => exec_sandbox(state, id, &body).await, + ("POST", ["v1", "sandboxes", id, "read"]) => read_sandbox_file(state, id, &body).await, + ("POST", ["v1", "sandboxes", id, "write"]) => write_sandbox_file(state, id, &body).await, + ("DELETE", ["v1", "sandboxes", id]) => destroy_sandbox(state, id).await, ("GET", ["v1", "pools"]) | ("GET", ["v1", "pools", _, _]) | ("POST", ["v1", "pools", _, _, "drain"]) @@ -217,7 +198,7 @@ fn admin_reload(state: &Arc) -> Result>> { } // --------------------------------------------------------------------------- -// Instances +// Sandboxes // --------------------------------------------------------------------------- #[derive(Debug, Deserialize)] @@ -238,15 +219,15 @@ struct CreateInstanceResp { selected_backend: BackendKind, } -fn list_instances(state: &Arc) -> Result>> { +fn list_sandboxes(state: &Arc) -> Result>> { json_ok(&state.manager.list()?) } -fn get_instance(state: &Arc, id: &str) -> Result>> { +fn get_sandbox(state: &Arc, id: &str) -> Result>> { json_ok(&state.manager.get(parse_uuid(id)?)?) } -async fn create_instance(state: &Arc, body: &[u8]) -> Result>> { +async fn create_sandbox(state: &Arc, body: &[u8]) -> Result>> { let req: CreateInstanceReq = serde_json::from_slice(body) .map_err(|e| BlazeDaemonError::BadRequest(format!("invalid create body: {e}")))?; @@ -334,56 +315,7 @@ async fn create_instance(state: &Arc, body: &[u8]) -> Result, id: &str) -> Result>> { - let uuid = parse_uuid(id)?; - let operation_lock = state.operation_lock(uuid); - let _operation = operation_lock.lock().await; - let mut map = state - .instances - .lock() - .map_err(|_| BlazeDaemonError::Internal("instances lock poisoned".into()))?; - let inst = map - .get_mut(&uuid) - .ok_or_else(|| BlazeDaemonError::NotFound(format!("instance {uuid}")))?; - - if inst.state == SandboxState::Running { - inst.transition(SandboxState::Paused)?; - } - inst.transition(SandboxState::Checkpointed)?; - state.state_store.persist(inst)?; - - let checkpoint_id = format!("ckpt-{}-{}", inst.id, chrono::Utc::now().timestamp()); - json_ok(&json!({ - "checkpoint_id": checkpoint_id, - "instance_id": inst.id, - })) -} - -async fn reset_instance(state: &Arc, id: &str) -> Result>> { - let uuid = parse_uuid(id)?; - let operation_lock = state.operation_lock(uuid); - let _operation = operation_lock.lock().await; - let map = state - .instances - .lock() - .map_err(|_| BlazeDaemonError::Internal("instances lock poisoned".into()))?; - let inst = map - .get(&uuid) - .ok_or_else(|| BlazeDaemonError::NotFound(format!("instance {uuid}")))?; - if inst.state != SandboxState::Running { - return Err(blaze_core::BlazeError::InvalidStateTransition { - from: inst.state.to_string(), - to: SandboxState::Reset.to_string(), - } - .into()); - } - - Err(BlazeDaemonError::UnsupportedOperation(format!( - "instance {uuid} cannot be reset until its runtime and storage can be reset together" - ))) -} - -async fn destroy_instance(state: &Arc, id: &str) -> Result>> { +async fn destroy_sandbox(state: &Arc, id: &str) -> Result>> { let uuid = parse_uuid(id)?; state.manager.destroy(uuid).await?; json_ok(&json!({ @@ -403,7 +335,7 @@ struct ExecRequest { timeout: Option, } -async fn exec_instance( +async fn exec_sandbox( state: &Arc, id: &str, body: &[u8], @@ -445,7 +377,7 @@ struct FileRequest { data_b64: Option, } -async fn read_instance_file( +async fn read_sandbox_file( state: &Arc, id: &str, body: &[u8], @@ -459,7 +391,7 @@ async fn read_instance_file( json_ok(&json!({"data_b64": BASE64.encode(data)})) } -async fn write_instance_file( +async fn write_sandbox_file( state: &Arc, id: &str, body: &[u8], @@ -621,11 +553,6 @@ fn error_response(err: &BlazeDaemonError) -> Response> { }) } -// Keep the unused-import lint quiet when `HookKind` is gated behind -// future-only hook registration paths. -#[allow(dead_code)] -fn _hookkind_marker(_k: HookKind) {} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -638,7 +565,7 @@ mod tests { use blaze_core::backend::BackendKind; use blaze_core::config::DaemonConfig; use blaze_core::kernel::HookRegistry; - use blaze_core::lifecycle::{BackendOwnership, OperationKind}; + use blaze_core::lifecycle::{BackendOwnership, OperationKind, SandboxState}; use blaze_core::policy::{ BackendConfigs, FallbackOnMissingHook, PolicyEngine, PolicyFile, PolicyHooks, PolicyMatch, PolicySelect, WorkloadClass, @@ -764,7 +691,7 @@ mod tests { } async fn created_json(state: &Arc, request: &[u8]) -> serde_json::Value { - let response = create_instance(state, request).await.expect("create"); + let response = create_sandbox(state, request).await.expect("create"); serde_json::from_slice( &response .into_body() @@ -1197,7 +1124,7 @@ mod tests { } #[tokio::test] - async fn sandbox_collection_and_item_routes_match_instance_routes() { + async fn sandbox_routes_cover_lifecycle_and_guest_operations() { let temp = tempfile::tempdir().expect("temp"); let config = test_config(&temp); let storage: Arc = Arc::new(FileStorageProvider::with_images( @@ -1207,7 +1134,7 @@ mod tests { let state = build_test_state( config, test_policy(BackendKind::Mock), - spawners(BackendKind::Mock, Arc::new(MockSpawner)), + spawners(BackendKind::Mock, Arc::new(GuestMockSpawner)), BackendKind::Mock, storage, ); @@ -1216,29 +1143,67 @@ mod tests { dispatched_json(&state, Method::POST, "/v1/sandboxes", test_request()).await; assert_eq!(status, StatusCode::CREATED); assert_eq!(created["instance"]["state"], "running"); - let id = created["instance"]["id"].as_str().expect("instance id"); - - let (_, sandboxes) = + assert!(created["decision"].is_object()); + assert_eq!(created["start_path"], "cold"); + assert_eq!(created["selected_backend"], "mock"); + let id = created["instance"]["id"] + .as_str() + .expect("sandbox id") + .to_string(); + let item = format!("/v1/sandboxes/{id}"); + + let (status, sandboxes) = dispatched_json(&state, Method::GET, "/v1/sandboxes", Vec::new()).await; - let (_, instances) = - dispatched_json(&state, Method::GET, "/v1/instances", Vec::new()).await; - assert_eq!(sandboxes, instances); + assert_eq!(status, StatusCode::OK); + assert!( + sandboxes + .as_array() + .expect("sandbox list") + .iter() + .any(|candidate| candidate["id"] == id) + ); + + let (status, fetched) = dispatched_json(&state, Method::GET, &item, Vec::new()).await; + assert_eq!(status, StatusCode::OK); + assert_eq!(fetched["id"], id); - let (_, sandbox) = dispatched_json( + let (status, executed) = dispatched_json( &state, - Method::GET, - &format!("/v1/sandboxes/{id}"), - Vec::new(), + Method::POST, + &format!("{item}/exec"), + serde_json::to_vec(&json!({"cmd": "printf sandbox", "timeout": 5})) + .expect("exec request"), ) .await; - let (_, instance) = dispatched_json( + assert_eq!(status, StatusCode::OK); + assert_eq!(executed["exit_code"], 0); + + let encoded = BASE64.encode(b"sandbox"); + let (status, written) = dispatched_json( &state, - Method::GET, - &format!("/v1/instances/{id}"), - Vec::new(), + Method::POST, + &format!("{item}/write"), + serde_json::to_vec(&json!({"path": "/tmp/sandbox", "data_b64": encoded})) + .expect("write request"), ) .await; - assert_eq!(sandbox, instance); + assert_eq!(status, StatusCode::OK); + assert_eq!(written["bytes"], 7); + + let (status, read) = dispatched_json( + &state, + Method::POST, + &format!("{item}/read"), + serde_json::to_vec(&json!({"path": "/tmp/sandbox"})).expect("read request"), + ) + .await; + assert_eq!(status, StatusCode::OK); + assert_eq!(read["data_b64"], encoded); + + let (status, destroyed) = dispatched_json(&state, Method::DELETE, &item, Vec::new()).await; + assert_eq!(status, StatusCode::OK); + assert_eq!(destroyed["destroyed"], true); + assert_eq!(destroyed["instance_id"], id); } #[tokio::test] @@ -1307,7 +1272,7 @@ mod tests { } #[tokio::test] - async fn destroy_route_forms_share_managed_cleanup() { + async fn unregistered_sandbox_actions_return_not_found() { let temp = tempfile::tempdir().expect("temp"); let config = test_config(&temp); let storage: Arc = Arc::new(FileStorageProvider::with_images( @@ -1322,38 +1287,54 @@ mod tests { storage, ); - let mut ids = Vec::new(); - for _ in 0..3 { - let created = created_json(&state, &test_request()).await; - ids.push( - Uuid::parse_str(created["instance"]["id"].as_str().expect("instance id")) - .expect("uuid"), - ); - } + let created = created_json(&state, &test_request()).await; + let id = created["instance"]["id"] + .as_str() + .expect("sandbox id") + .to_string(); + let uuid = Uuid::parse_str(&id).expect("uuid"); + let routes = [ - (Method::DELETE, format!("/v1/sandboxes/{}", ids[0]), ids[0]), - (Method::DELETE, format!("/v1/instances/{}", ids[1]), ids[1]), - ( - Method::POST, - format!("/v1/instances/{}/destroy", ids[2]), - ids[2], - ), + (Method::POST, format!("/v1/sandboxes/{id}/reset")), + (Method::POST, format!("/v1/sandboxes/{id}/checkpoint")), + (Method::POST, format!("/v1/sandboxes/{id}/destroy")), ]; - for (method, path, id) in routes { - let (status, response) = dispatched_json(&state, method, &path, Vec::new()).await; - assert_eq!(status, StatusCode::OK); - assert_eq!(response["destroyed"], true); - assert_eq!(response["instance_id"], id.to_string()); + for (method, path) in routes { + let (status, body) = handled_json(&state, method, &path, Vec::new()).await; + assert_eq!(status, StatusCode::NOT_FOUND, "{path}"); + assert_eq!(body["status"], 404, "{path}"); + assert!( + body["error"] + .as_str() + .expect("error message") + .contains(&path), + "{path}" + ); assert_eq!( - state.manager.get(id).expect("destroyed state").state, - SandboxState::Destroyed + state.manager.get(uuid).expect("unchanged state").state, + SandboxState::Running, + "{path}" ); - assert!(matches!( - state.state_store.run_dir(id), - Err(BlazeDaemonError::NotFound(_)) - )); } + + let (status, destroyed) = dispatched_json( + &state, + Method::DELETE, + &format!("/v1/sandboxes/{id}"), + Vec::new(), + ) + .await; + assert_eq!(status, StatusCode::OK); + assert_eq!(destroyed["instance_id"], id); + assert_eq!( + state.manager.get(uuid).expect("destroyed state").state, + SandboxState::Destroyed + ); + assert!(matches!( + state.state_store.run_dir(uuid), + Err(BlazeDaemonError::NotFound(_)) + )); } /// When multiple backend binaries exist on disk but the daemon probed @@ -1429,7 +1410,7 @@ mod tests { })) .unwrap(); - let resp = create_instance(&state, &req_body).await.unwrap(); + let resp = create_sandbox(&state, &req_body).await.unwrap(); let body = resp.into_body().collect().await.unwrap().to_bytes(); let resp_json: serde_json::Value = serde_json::from_slice(&body).unwrap(); @@ -1447,128 +1428,6 @@ mod tests { let _ = std::fs::remove_dir_all(&tmp); } - #[tokio::test] - async fn reset_never_reports_success_without_runtime_and_storage_reset() { - let temp = tempfile::tempdir().expect("temp"); - let config = test_config(&temp); - let storage: Arc = Arc::new(FileStorageProvider::with_images( - config.storage.images_dir.clone(), - config.storage.instances_dir.clone(), - )); - let state = build_test_state( - config, - test_policy(BackendKind::Mock), - spawners(BackendKind::Mock, Arc::new(MockSpawner)), - BackendKind::Mock, - storage, - ); - - let (status, body) = handled_json( - &state, - Method::POST, - "/v1/instances/not-a-uuid/reset", - Vec::new(), - ) - .await; - assert_eq!(status, StatusCode::BAD_REQUEST); - assert_eq!(body["status"], 400); - - let missing_id = Uuid::nil(); - let (status, body) = handled_json( - &state, - Method::POST, - &format!("/v1/instances/{missing_id}/reset"), - Vec::new(), - ) - .await; - assert_eq!(status, StatusCode::NOT_FOUND); - assert_eq!(body["status"], 404); - - let created = created_json(&state, &test_request()).await; - let id = created["instance"]["id"].as_str().expect("id"); - let uuid = Uuid::parse_str(id).expect("uuid"); - - let (status, body) = handled_json( - &state, - Method::POST, - &format!("/v1/instances/{id}/reset"), - Vec::new(), - ) - .await; - - assert_eq!(status, StatusCode::NOT_IMPLEMENTED); - assert_eq!(body["status"], 501); - assert!( - body["error"] - .as_str() - .expect("error") - .contains("runtime and storage") - ); - assert_eq!( - state.instances.lock().expect("instances")[&uuid].state, - SandboxState::Running - ); - let persisted = state - .state_store - .load(uuid) - .expect("persisted running state"); - assert_eq!(persisted.state, SandboxState::Running); - assert!(state.manager.backend_owner(uuid).is_some()); - } - - #[tokio::test] - async fn reset_preserves_invalid_state_error_and_persisted_state() { - let temp = tempfile::tempdir().expect("temp"); - let config = test_config(&temp); - let storage: Arc = Arc::new(FileStorageProvider::with_images( - config.storage.images_dir.clone(), - config.storage.instances_dir.clone(), - )); - let state = build_test_state( - config, - test_policy(BackendKind::Mock), - spawners(BackendKind::Mock, Arc::new(MockSpawner)), - BackendKind::Mock, - storage, - ); - let created = created_json(&state, &test_request()).await; - let id = created["instance"]["id"].as_str().expect("id"); - let uuid = Uuid::parse_str(id).expect("uuid"); - { - let mut instances = state.instances.lock().expect("instances"); - let instance = instances.get_mut(&uuid).expect("instance"); - instance.transition(SandboxState::Paused).expect("pause"); - state.state_store.persist(instance).expect("persist pause"); - } - - let (status, body) = handled_json( - &state, - Method::POST, - &format!("/v1/instances/{id}/reset"), - Vec::new(), - ) - .await; - - assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY); - assert_eq!(body["status"], 422); - assert!( - body["error"] - .as_str() - .expect("error") - .contains("paused -> reset") - ); - assert_eq!( - state.instances.lock().expect("instances")[&uuid].state, - SandboxState::Paused - ); - let persisted = state - .state_store - .load(uuid) - .expect("persisted paused state"); - assert_eq!(persisted.state, SandboxState::Paused); - assert!(state.manager.backend_owner(uuid).is_some()); - } - #[tokio::test] async fn sandbox_guest_routes_use_owned_runtime() { let temp = tempfile::tempdir().expect("temp"); @@ -1606,7 +1465,7 @@ mod tests { let (status, written) = dispatched_json( &state, Method::POST, - &format!("/v1/instances/{id}/write"), + &format!("/v1/sandboxes/{id}/write"), serde_json::to_vec(&json!({ "path": "/tmp/value", "data_b64": encoded, @@ -1944,7 +1803,7 @@ mod tests { ); let create_state = state.clone(); let create = - tokio::spawn(async move { create_instance(&create_state, &test_request()).await }); + tokio::spawn(async move { create_sandbox(&create_state, &test_request()).await }); tokio::time::timeout(std::time::Duration::from_secs(2), reached.notified()) .await .expect("create reached the pre-spawn boundary"); @@ -2162,7 +2021,7 @@ mod tests { storage, ); - let error = create_instance(&state, &test_request()) + let error = create_sandbox(&state, &test_request()) .await .expect_err("partial spawn must require recovery"); assert!(matches!(error, BlazeDaemonError::RecoveryRequired(_))); @@ -2184,7 +2043,7 @@ mod tests { assert!(state.manager.backend_owner(instance.id).is_some()); assert!(state.state_store.run_dir(instance.id).is_ok()); - destroy_instance(&state, &instance.id.to_string()) + destroy_sandbox(&state, &instance.id.to_string()) .await .expect("retry destroy"); assert!(!instances_dir.join(instance.id.to_string()).exists()); @@ -2251,7 +2110,7 @@ mod tests { storage, ); - destroy_instance(&state, &instance.id.to_string()) + destroy_sandbox(&state, &instance.id.to_string()) .await .expect("destroy recovered instance"); assert_eq!(persisted_cleanups.load(Ordering::Acquire), 1); @@ -2307,7 +2166,7 @@ mod tests { restarted_storage, ); - destroy_instance(&restarted, &id) + destroy_sandbox(&restarted, &id) .await .expect("destroy recovered mock instance"); assert_eq!(mock_cleanups.load(Ordering::Acquire), 1); @@ -2353,7 +2212,7 @@ mod tests { storage, ); - destroy_instance(&restarted, &id.to_string()) + destroy_sandbox(&restarted, &id.to_string()) .await .expect("destroy state without slot"); assert_eq!(cleanup_count.load(Ordering::Acquire), 0); @@ -2372,7 +2231,7 @@ mod tests { let state = guest_mock_state(&temp); let hook = crate::failpoint::TestFailpoint::new(&["create-guest-ready"]); - hook.run(create_instance(&state, &request)) + hook.run(create_sandbox(&state, &request)) .await .expect_err("guest readiness failure"); @@ -2404,7 +2263,7 @@ mod tests { let spawn_state = mock_state(&spawn_temp); let spawn_hook = crate::failpoint::TestFailpoint::new(&["create-spawn"]); spawn_hook - .run(create_instance(&spawn_state, &request)) + .run(create_sandbox(&spawn_state, &request)) .await .expect_err("spawn failure"); let spawn_instance = spawn_state @@ -2421,7 +2280,7 @@ mod tests { let commit_state = mock_state(&commit_temp); let commit_hook = crate::failpoint::TestFailpoint::new(&["create-state-commit"]); commit_hook - .run(create_instance(&commit_state, &request)) + .run(create_sandbox(&commit_state, &request)) .await .expect_err("state commit failure"); let commit_instance = commit_state @@ -2446,7 +2305,7 @@ mod tests { let id = created["instance"]["id"].as_str().expect("id").to_string(); let kill_hook = crate::failpoint::TestFailpoint::new(&["destroy-kill"]); kill_hook - .run(destroy_instance(&destroy_state, &id)) + .run(destroy_sandbox(&destroy_state, &id)) .await .expect_err("kill boundary"); let uuid = Uuid::parse_str(&id).expect("uuid"); @@ -2460,7 +2319,7 @@ mod tests { Some(OperationKind::Destroy) ); assert!(destroy_state.manager.backend_owner(uuid).is_some()); - destroy_instance(&destroy_state, &id) + destroy_sandbox(&destroy_state, &id) .await .expect("destroy retry"); @@ -2470,7 +2329,7 @@ mod tests { let id = created["instance"]["id"].as_str().expect("id").to_string(); let release_hook = crate::failpoint::TestFailpoint::new(&["storage-release"]); release_hook - .run(destroy_instance(&release_state, &id)) + .run(destroy_sandbox(&release_state, &id)) .await .expect_err("release boundary"); let uuid = Uuid::parse_str(&id).expect("uuid"); @@ -2485,7 +2344,7 @@ mod tests { .map(|operation| operation.kind), Some(OperationKind::Destroy) ); - destroy_instance(&release_state, &id) + destroy_sandbox(&release_state, &id) .await .expect("release retry"); } @@ -2499,7 +2358,7 @@ mod tests { let hook = crate::failpoint::TestFailpoint::new(failpoints); let error = hook - .run(create_instance(&state, &test_request())) + .run(create_sandbox(&state, &test_request())) .await .expect_err("rollback terminal commit failure"); @@ -2535,7 +2394,7 @@ mod tests { .exists() ); - destroy_instance(&state, &instance.id.to_string()) + destroy_sandbox(&state, &instance.id.to_string()) .await .expect("destroy retry"); @@ -2556,7 +2415,7 @@ mod tests { let state = mock_state(&temp); let hook = crate::failpoint::TestFailpoint::new(&["state-before-first-publication"]); - hook.run(create_instance(&state, &test_request())) + hook.run(create_sandbox(&state, &test_request())) .await .expect_err("pre-publication failure"); @@ -2586,7 +2445,7 @@ mod tests { let state = mock_state(&temp); let hook = crate::failpoint::TestFailpoint::new(&["state-first-publication-root-sync"]); - hook.run(create_instance(&state, &test_request())) + hook.run(create_sandbox(&state, &test_request())) .await .expect_err("initial state publication sync failure"); @@ -2630,7 +2489,7 @@ mod tests { let hook = crate::failpoint::TestFailpoint::new(&["state-post-publication-identity"]); let error = hook - .run(create_instance(&state, &test_request())) + .run(create_sandbox(&state, &test_request())) .await .expect_err("unconfirmed publication"); @@ -2667,7 +2526,7 @@ mod tests { .exists() ); - destroy_instance(&state, &instance.id.to_string()) + destroy_sandbox(&state, &instance.id.to_string()) .await .expect("destroy revalidates the publication"); assert_eq!( @@ -2701,7 +2560,7 @@ mod tests { let state = mock_state(&temp); let hook = crate::failpoint::TestFailpoint::new(&["state-post-publication-identity"]); - hook.run(create_instance(&state, &test_request())) + hook.run(create_sandbox(&state, &test_request())) .await .expect_err("unconfirmed publication"); let instance = state @@ -2717,7 +2576,7 @@ mod tests { std::fs::rename(&configured, &retained).expect("move retained state directory"); std::fs::create_dir(&configured).expect("replacement state directory"); - let error = destroy_instance(&state, &instance.id.to_string()) + let error = destroy_sandbox(&state, &instance.id.to_string()) .await .expect_err("replacement must keep recovery fail-closed"); @@ -2783,7 +2642,7 @@ mod tests { let hook = crate::failpoint::TestFailpoint::new(&["destroy-intent-state-commit"]); let error = hook - .run(destroy_instance(&state, &id)) + .run(destroy_sandbox(&state, &id)) .await .expect_err("intent failure"); @@ -2804,7 +2663,7 @@ mod tests { assert!(temp.path().join("instances").join(&id).is_dir()); assert!(state.state_store.run_dir(uuid).is_ok()); - destroy_instance(&state, &id).await.expect("destroy retry"); + destroy_sandbox(&state, &id).await.expect("destroy retry"); assert_eq!(kill_count.load(Ordering::Acquire), 1); assert_eq!(release_count.load(Ordering::Acquire), 1); assert_eq!( @@ -2834,7 +2693,7 @@ mod tests { let hook = crate::failpoint::TestFailpoint::new(&["destroy-stop-state-commit"]); let error = hook - .run(destroy_instance(&state, &id)) + .run(destroy_sandbox(&state, &id)) .await .expect_err("stop commit failure"); @@ -2862,7 +2721,7 @@ mod tests { assert!(temp.path().join("instances").join(&id).is_dir()); assert!(state.state_store.run_dir(uuid).is_ok()); - destroy_instance(&state, &id).await.expect("destroy retry"); + destroy_sandbox(&state, &id).await.expect("destroy retry"); assert_eq!(kill_count.load(Ordering::Acquire), 1); assert_eq!(release_count.load(Ordering::Acquire), 1); let persisted = state @@ -2888,7 +2747,7 @@ mod tests { let hook = crate::failpoint::TestFailpoint::new(&["destroy-final-state-commit"]); let error = hook - .run(destroy_instance(&state, &id)) + .run(destroy_sandbox(&state, &id)) .await .expect_err("final commit failure"); @@ -2916,7 +2775,7 @@ mod tests { ); assert!(state.state_store.run_dir(uuid).is_ok()); - destroy_instance(&state, &id).await.expect("destroy retry"); + destroy_sandbox(&state, &id).await.expect("destroy retry"); assert_eq!(kill_count.load(Ordering::Acquire), 1); assert_eq!(release_count.load(Ordering::Acquire), 2); let destroyed = state.instances.lock().expect("instances")[&uuid].clone(); @@ -2944,7 +2803,7 @@ mod tests { "storage-acquire-rollback", ]); let error = acquire_hook - .run(create_instance(&state, &test_request())) + .run(create_sandbox(&state, &test_request())) .await .expect_err("residual slot must require recovery"); assert!(matches!(error, BlazeDaemonError::RecoveryRequired(_))); @@ -2969,7 +2828,7 @@ mod tests { .join(instance.id.to_string()) .is_dir() ); - destroy_instance(&state, &instance.id.to_string()) + destroy_sandbox(&state, &instance.id.to_string()) .await .expect("destroy residual slot"); } @@ -2996,7 +2855,7 @@ mod tests { let create_hook = pause_hook.clone(); let create = tokio::spawn(async move { create_hook - .run(create_instance(&create_state, &test_request())) + .run(create_sandbox(&create_state, &test_request())) .await }); pause_hook.wait_until_paused().await; @@ -3061,7 +2920,7 @@ mod tests { .contains_key(&id) ); - destroy_instance(&restarted, &id.to_string()) + destroy_sandbox(&restarted, &id.to_string()) .await .expect("destroy acquired slot after restart"); assert_eq!(cleanup_count.load(Ordering::Acquire), 0); diff --git a/src/blaze/crates/blazed/src/sandbox/manager.rs b/src/blaze/crates/blazed/src/sandbox/manager.rs index 21c6b093d5..1f9e32e959 100644 --- a/src/blaze/crates/blazed/src/sandbox/manager.rs +++ b/src/blaze/crates/blazed/src/sandbox/manager.rs @@ -101,9 +101,9 @@ pub struct SandboxManagerInit { pub template_catalog: TemplateCatalog, } -/// Shared resources exposed to API paths that do not change runtime -/// ownership, such as checkpoint and reset. +/// Shared resources returned to the daemon wiring and test harness. pub struct SandboxManagerResources { + #[cfg(test)] pub instances: Arc>>, pub metrics: Arc, } @@ -130,6 +130,7 @@ impl SandboxManager { let backend_instances = Arc::new(Mutex::new(HashMap::new())); let metrics = Arc::new(Metrics::new()); let resources = SandboxManagerResources { + #[cfg(test)] instances: instances.clone(), metrics: metrics.clone(), }; diff --git a/src/blaze/crates/blazed/src/state.rs b/src/blaze/crates/blazed/src/state.rs index 2c5247cdd1..2a03a8d406 100644 --- a/src/blaze/crates/blazed/src/state.rs +++ b/src/blaze/crates/blazed/src/state.rs @@ -5,15 +5,19 @@ //! lock just long enough to read or mutate the piece they need — locks //! are never held across `.await` boundaries. -use std::collections::HashMap; use std::sync::{Arc, Mutex}; +#[cfg(test)] +use std::collections::HashMap; + use blaze_core::backend::BackendKind; use blaze_core::config::DaemonConfig; use blaze_core::kernel::HookRegistry; +#[cfg(test)] use blaze_core::lifecycle::SandboxInstance; use blaze_core::policy::PolicyEngine; use blaze_core::storage::StorageProvider; +#[cfg(test)] use uuid::Uuid; use crate::error::Result; @@ -31,6 +35,7 @@ pub struct ServerState { pub config: Mutex, pub policy: Mutex, pub hook: Mutex, + #[cfg(test)] pub instances: Arc>>, pub manager: Arc, /// The backend kind that `build_spawner` actually probed and selected. @@ -38,6 +43,7 @@ pub struct ServerState { /// backend rather than reporting all configured binaries. pub active_backend: BackendKind, pub storage: Arc, + #[cfg(test)] pub state_store: StateStore, pub metrics: Arc, } @@ -128,19 +134,16 @@ impl ServerState { config: Mutex::new(config), policy: Mutex::new(policy), hook: Mutex::new(hook), + #[cfg(test)] instances: resources.instances, manager: Arc::new(manager), active_backend, storage, + #[cfg(test)] state_store, metrics: resources.metrics, }) } - - /// Return the async operation lock that serializes one sandbox mutation. - pub fn operation_lock(&self, id: Uuid) -> Arc> { - self.manager.operation_lock(id) - } } #[cfg(test)] diff --git a/src/blaze/docs/design/lifecycle-state-consistency.md b/src/blaze/docs/design/lifecycle-state-consistency.md index fa7244c2a8..33381feafa 100644 --- a/src/blaze/docs/design/lifecycle-state-consistency.md +++ b/src/blaze/docs/design/lifecycle-state-consistency.md @@ -4,15 +4,16 @@ Blaze has two related lifecycle boundaries. Before serving requests, it must reconstruct a complete persisted sandbox inventory without exposing a partial -result. While serving requests, it must reject reset and reusable-instance -operations that cannot preserve runtime and storage ownership. Retired `Reset`, -`Warm`, and `start_path = "warm"` values remain decodable so startup can clean -non-terminal records that contain them. +result. While serving requests, it exposes lifecycle and guest operations only +through the sandbox namespace and rejects reserved reusable-capacity operations +before they can change ownership. Retired `Reset`, `Warm`, and +`start_path = "warm"` values remain decodable so startup can clean non-terminal +records that contain them. This document defines both boundaries. The inventory-publication protocol does -not change the HTTP API, configuration keys, or persisted JSON format. The reset -and reusable-instance section defines the public compatibility protocol that -follows from the reachable lifecycle states. +not change the HTTP API, configuration keys, or persisted JSON format. The +management API section defines the sandbox namespace and the reserved +reusable-capacity boundary. ## Terms and owned objects @@ -97,15 +98,12 @@ validated inventory into a partial one. Blaze attempts to persist the recovery state; if that write also fails, reconciliation reports the additional error and the durable record may still contain its previous state. -## Reset and reusable-instance compatibility boundary +## Management API and reusable-state boundary -`POST /v1/instances/{id}/reset` has no successful path until Blaze can reset -runtime and storage as one operation. A malformed identifier returns `400 Bad -Request`, an unknown sandbox returns `404 Not Found`, and an existing sandbox -that is not `Running` returns `422 Unprocessable Entity`. A running sandbox -returns `501 Not Implemented`. Every rejection occurs before any in-memory or -persisted lifecycle change and before any change to runtime or storage -ownership. +Lifecycle and guest operations are registered under `/v1/sandboxes`. +Action-style reset, checkpoint, and destroy paths are unregistered and return +`404 Not Found`. Canonical destruction remains +`DELETE /v1/sandboxes/{id}`. Checkpoint capture is defined separately. The following reserved management routes also return `501 Not Implemented` and do not manage reusable capacity: @@ -117,8 +115,9 @@ do not manage reusable capacity: `GET /v1/health` retains its `storage_pool` object for response compatibility; the file provider reports zero ready, capacity, pending, and quarantined slots. -The metrics endpoint no longer publishes reset, pool-hit, or pool-miss counters -because those operations have no supported success path. +The metrics endpoint does not publish a reset counter because no reset route is +registered. Pool-hit and pool-miss counters also remain absent because reusable +capacity has no supported success path. New sandbox creation always records `start_path = "cold"`. Lifecycle transitions cannot enter `Reset` or `Warm`, so no supported path can produce or @@ -157,7 +156,9 @@ Future lifecycle-state changes must preserve these rules: revalidated; - no request handler can observe either startup map before all inventory checks have passed; -- reset and pool-management rejections occur before lifecycle, runtime, or - storage ownership changes; and +- unregistered sandbox action routes return `404` before reading or changing + sandbox state; +- pool-management rejections occur before lifecycle, runtime, or storage + ownership changes; and - lifecycle operations cannot enter or reactivate `Reset` or `Warm`; legacy values are cleanup inputs only. diff --git a/src/blaze/docs/design/lifecycle-state-consistency_zh.md b/src/blaze/docs/design/lifecycle-state-consistency_zh.md index 358a5abd0e..3db5a8dbc9 100644 --- a/src/blaze/docs/design/lifecycle-state-consistency_zh.md +++ b/src/blaze/docs/design/lifecycle-state-consistency_zh.md @@ -3,12 +3,13 @@ [English](lifecycle-state-consistency.md) Blaze 有两个相互关联的生命周期边界。提供请求服务前,它必须完整重建已经持久化 -的 sandbox 清单,且不能暴露部分结果。提供请求服务期间,它必须拒绝无法保持运行 -环境与存储所有权的重置和实例复用操作。已停用的 `Reset`、`Warm` 和 -`start_path = "warm"` 值继续可解析,以便启动恢复清理包含这些值的非终态记录。 +的 sandbox 清单,且不能暴露部分结果。提供请求服务期间,它只通过沙箱命名空间 +提供生命周期和客户机操作,并在预留的复用容量操作改变所有权之前将其拒绝。已停用 +的 `Reset`、`Warm` 和 `start_path = "warm"` 值继续可解析,以便启动恢复清理 +包含这些值的非终态记录。 本设计定义这两个边界。清单发布流程不改变 HTTP API、配置项或持久化 JSON 格式。 -重置与实例复用章节定义由可达生命周期状态决定的公开兼容协议。 +管理 API 章节定义沙箱命名空间以及预留的复用容量边界。 ## 概念与持有对象 @@ -79,14 +80,12 @@ Blaze 会保留被拒绝的 UUID 目录及其 `state.json`,供运维人员检 变成部分清单。Blaze 会尝试持久化恢复状态;如果这次写入也失败,启动恢复会报告 附加错误,持久化记录仍可能保留先前的状态。 -## 重置与可复用实例兼容性边界 +## 管理 API 与可复用状态边界 -在 Blaze 能够把运行环境和存储作为一个整体完成重置之前, -`POST /v1/instances/{id}/reset` 没有成功路径。格式错误的标识符返回 -`400 Bad Request`,不存在的 sandbox 返回 `404 Not Found`,处于非 `Running` -状态的已有 sandbox 返回 `422 Unprocessable Entity`,运行中的 sandbox 返回 -`501 Not Implemented`。所有拒绝都发生在修改内存或持久化生命周期状态之前, -也不会改变运行环境或存储所有权。 +生命周期和客户机操作注册在 `/v1/sandboxes` 下。操作式重置、检查点和销毁 +路径不注册,并返回 `404 Not Found`。规范的销毁入口仍是 +`DELETE /v1/sandboxes/{id}`。 +检查点捕获由独立设计定义。 以下保留的管理路由同样返回 `501 Not Implemented`,并且不会管理复用容量: @@ -96,8 +95,9 @@ Blaze 会保留被拒绝的 UUID 目录及其 `state.json`,供运维人员检 - `PUT /v1/pools/{backend}/{class}/sizing`。 为了保持响应兼容,`GET /v1/health` 会继续返回 `storage_pool` 对象;文件存储 -提供者报告的就绪、容量、待处理和隔离槽位数量均为零。由于重置、资源池命中和 -资源池未命中都没有受支持的成功路径,监控接口不再发布对应的计数指标。 +提供者报告的就绪、容量、待处理和隔离槽位数量均为零。因为没有注册重置路由, +监控接口不发布重置计数。复用容量没有受支持的成功路径,因此资源池命中和未命中 +计数也继续保持缺失。 新建 sandbox 始终记录 `start_path = "cold"`。生命周期状态转换不能进入 `Reset` 或 `Warm`,因此没有受支持的路径可以产生或重新启用可复用 sandbox。Blaze 继续 @@ -127,5 +127,6 @@ lock 的 daemon 进程。advisory lock 不会阻止无关进程直接修改该 - 启动过程必须持有 run-directory map lock,直到完整清单被接受或拒绝; - 必须先完成最终 UUID 枚举,再复验保留对象; - 所有清单检查完成前,request handler 不能观察到任何一个启动 map; -- 重置和资源池管理请求必须在生命周期、运行环境或存储所有权发生变化前被拒绝; +- 未注册的沙箱操作式路由必须在读取或改变 sandbox 状态前返回 `404`; +- 资源池管理请求必须在生命周期、运行环境或存储所有权发生变化前被拒绝; - 生命周期操作不能进入或重新启用 `Reset` 或 `Warm`;旧值只能用于清理。