workspace: delete workers through runtime

This commit is contained in:
2026-07-12 09:06:59 +09:00
parent d8c0853d55
commit 3009bb4ad9
12 changed files with 489 additions and 112 deletions
+12
View File
@@ -192,6 +192,18 @@ impl FsRuntimeStore {
Ok(())
}
pub(crate) fn delete_worker_snapshot(&self, worker_id: &WorkerId) -> Result<(), RuntimeError> {
let worker_dir = self.worker_dir(worker_id);
if !worker_dir.exists() {
return Ok(());
}
fs::remove_dir_all(&worker_dir).map_err(|source| RuntimeError::StoreIo {
operation: "delete worker store",
path: worker_dir,
source,
})
}
pub(crate) fn append_event(&self, event: &RuntimeEvent) -> Result<(), RuntimeError> {
if let Some(worker_ref) = &event.worker_ref {
self.ensure_worker_ref(worker_ref)?;
+23 -2
View File
@@ -15,7 +15,7 @@ use crate::config_bundle::{ConfigBundle, ConfigBundleAvailability, ConfigBundleS
use crate::error::RuntimeError;
use crate::identity::{RuntimeId, WorkerId, WorkerRef};
use crate::interaction::{WorkerInput, WorkerInteractionAck};
use crate::management::{RuntimeLimits, RuntimeSummary};
use crate::management::{RuntimeLimits, RuntimeSummary, WorkerDeleteResult};
#[cfg(feature = "ws-server")]
use crate::observation::WorkerObservationCursor;
use axum::body::{Body, Bytes};
@@ -147,7 +147,10 @@ pub fn runtime_http_router(runtime: Runtime, local_token: Option<String>) -> Rou
get(get_working_directory).delete(cleanup_working_directory),
)
.route("/v1/workers", get(list_workers).post(create_worker))
.route("/v1/workers/{worker_id}", get(get_worker))
.route(
"/v1/workers/{worker_id}",
get(get_worker).delete(delete_worker),
)
.route("/v1/workers/{worker_id}/input", post(send_worker_input))
.route("/v1/workers/{worker_id}/stop", post(stop_worker))
.route("/v1/workers/{worker_id}/cancel", post(cancel_worker));
@@ -219,6 +222,12 @@ pub struct RuntimeHttpWorkerResponse {
pub worker: WorkerDetail,
}
/// Worker delete response.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHttpWorkerDeleteResponse {
pub worker: WorkerDeleteResult,
}
/// Worker input acknowledgement response.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeHttpWorkerInputResponse {
@@ -461,6 +470,18 @@ async fn get_worker(
Ok(Json(RuntimeHttpWorkerResponse { worker }))
}
async fn delete_worker(
State(state): State<RuntimeHttpState>,
Path(worker_id): Path<String>,
) -> RestResult<RuntimeHttpWorkerDeleteResponse> {
let worker_ref = worker_ref_for(&state.runtime, worker_id)?;
let worker = state
.runtime
.delete_worker(&worker_ref)
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpWorkerDeleteResponse { worker }))
}
async fn create_worker(
State(state): State<RuntimeHttpState>,
body: Result<Json<CreateWorkerRequest>, JsonRejection>,
+8 -1
View File
@@ -1,4 +1,4 @@
use crate::identity::RuntimeId;
use crate::identity::{RuntimeId, WorkerId};
use serde::{Deserialize, Serialize};
/// Runtime backend kind.
@@ -54,6 +54,13 @@ fn unknown_platform_component() -> String {
"unknown".to_string()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkerDeleteResult {
pub runtime_id: RuntimeId,
pub worker_id: WorkerId,
pub deleted: bool,
}
/// Management-plane summary for a Runtime.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RuntimeSummary {
+1
View File
@@ -42,6 +42,7 @@ pub enum RuntimeEventKind {
WorkerInputAccepted,
WorkerStopped,
WorkerCancelled,
WorkerDeleted,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
+74
View File
@@ -25,6 +25,7 @@ use crate::identity::{RuntimeId, WorkerId, WorkerRef};
use crate::interaction::{WorkerInput, WorkerInputKind, WorkerInteractionAck};
use crate::management::{
RuntimeBackendKind, RuntimeLimits, RuntimeOptions, RuntimeStatus, RuntimeSummary,
WorkerDeleteResult,
};
use crate::observation::{
EventCursor, EventSubscription, EventSubscriptionMode, RuntimeEvent, RuntimeEventBatch,
@@ -648,6 +649,46 @@ impl Runtime {
)
}
/// Delete a non-running Worker from Runtime state and persisted Worker storage.
pub fn delete_worker(
&self,
worker_ref: &WorkerRef,
) -> Result<WorkerDeleteResult, RuntimeError> {
let mut state = self.lock()?;
state.ensure_running()?;
state.ensure_worker_ref(worker_ref)?;
let worker = state.worker(worker_ref)?;
if worker.status.is_active() {
return Err(RuntimeError::InvalidRequest(format!(
"worker {} is running and must be stopped before deletion",
worker_ref.worker_id
)));
}
let removed = state.workers.remove(&worker_ref.worker_id).ok_or_else(|| {
RuntimeError::WorkerNotFound {
runtime_id: state.runtime_id.clone(),
worker_id: worker_ref.worker_id,
}
})?;
#[cfg(feature = "ws-server")]
state
.observation_events
.retain(|event| event.worker_ref != *worker_ref);
let event_id = state.push_event(
Some(worker_ref.clone()),
RuntimeEventKind::WorkerDeleted,
"worker deleted",
);
state.persist_runtime_snapshot()?;
state.delete_worker_snapshot(&worker_ref.worker_id)?;
state.persist_event_by_id(event_id)?;
Ok(WorkerDeleteResult {
runtime_id: removed.worker_ref.runtime_id,
worker_id: removed.worker_id,
deleted: true,
})
}
/// Cursor pointing to the beginning of Runtime events.
pub fn event_cursor_from_start(&self) -> Result<EventCursor, RuntimeError> {
let state = self.lock()?;
@@ -1145,6 +1186,14 @@ impl RuntimeState {
Ok(())
}
#[cfg(feature = "fs-store")]
fn delete_worker_snapshot(&self, worker_id: &WorkerId) -> Result<(), RuntimeError> {
if let Some(store) = self.fs_store() {
store.delete_worker_snapshot(worker_id)?;
}
Ok(())
}
#[cfg(feature = "fs-store")]
fn persist_event_by_id(&self, event_id: u64) -> Result<(), RuntimeError> {
if let Some(store) = self.fs_store() {
@@ -1201,6 +1250,11 @@ impl RuntimeState {
Ok(())
}
#[cfg(not(feature = "fs-store"))]
fn delete_worker_snapshot(&self, _worker_id: &WorkerId) -> Result<(), RuntimeError> {
Ok(())
}
#[cfg(not(feature = "fs-store"))]
fn persist_event_by_id(&self, _event_id: u64) -> Result<(), RuntimeError> {
Ok(())
@@ -1997,6 +2051,26 @@ mod tests {
assert_eq!(summary.cancelled_worker_count, 1);
}
#[test]
fn delete_worker_removes_stopped_worker_from_runtime() {
let runtime = runtime_with_backend();
let worker = runtime.create_worker(task_request("delete me")).unwrap();
assert!(runtime.delete_worker(&worker.worker_ref).is_err());
runtime
.stop_worker(&worker.worker_ref, Some("done".to_string()))
.unwrap();
let result = runtime.delete_worker(&worker.worker_ref).unwrap();
assert!(result.deleted);
assert_eq!(result.worker_id, worker.worker_id);
assert!(matches!(
runtime.worker_detail(&worker.worker_ref),
Err(RuntimeError::WorkerNotFound { .. })
));
let summary = runtime.summary().unwrap();
assert_eq!(summary.worker_count, 0);
}
#[test]
fn stop_then_cancel_preserves_stopped_terminal_state() {
let runtime = runtime_with_backend();