workspace: delete workers through runtime
This commit is contained in:
@@ -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)?;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -42,6 +42,7 @@ pub enum RuntimeEventKind {
|
||||
WorkerInputAccepted,
|
||||
WorkerStopped,
|
||||
WorkerCancelled,
|
||||
WorkerDeleted,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user