fix: report actual worker execution unavailability
This commit is contained in:
@@ -15,7 +15,7 @@ pub enum RuntimeError {
|
|||||||
#[error("worker {worker_id} was not found")]
|
#[error("worker {worker_id} was not found")]
|
||||||
WorkerNotFound { worker_id: WorkerId },
|
WorkerNotFound { worker_id: WorkerId },
|
||||||
|
|
||||||
#[error("worker {worker_id} has no execution backend: {message}")]
|
#[error("worker {worker_id} execution is unavailable: {message}")]
|
||||||
WorkerExecutionUnavailable {
|
WorkerExecutionUnavailable {
|
||||||
worker_id: WorkerId,
|
worker_id: WorkerId,
|
||||||
message: String,
|
message: String,
|
||||||
|
|||||||
@@ -1425,9 +1425,9 @@ impl Runtime {
|
|||||||
let previous_workspace_api = {
|
let previous_workspace_api = {
|
||||||
let state = self.lock()?;
|
let state = self.lock()?;
|
||||||
let worker = state.worker(worker_ref)?;
|
let worker = state.worker(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(format!(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"worker {} has pending restore reconciliation",
|
"worker {} has pending {operation} reconciliation",
|
||||||
worker_ref.worker_id
|
worker_ref.worker_id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -1453,9 +1453,9 @@ impl Runtime {
|
|||||||
{
|
{
|
||||||
let mut state = self.lock()?;
|
let mut state = self.lock()?;
|
||||||
let worker = state.worker_mut(worker_ref)?;
|
let worker = state.worker_mut(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(format!(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"worker {} has pending restore reconciliation",
|
"worker {} has pending {operation} reconciliation",
|
||||||
worker_ref.worker_id
|
worker_ref.worker_id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -2052,10 +2052,10 @@ impl Runtime {
|
|||||||
fn ensure_worker_execution(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
|
fn ensure_worker_execution(&self, worker_ref: &WorkerRef) -> Result<(), RuntimeError> {
|
||||||
let state = self.lock()?;
|
let state = self.lock()?;
|
||||||
let worker = state.worker(worker_ref)?;
|
let worker = state.worker(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::WorkerExecutionUnavailable {
|
return Err(RuntimeError::WorkerExecutionUnavailable {
|
||||||
worker_id: worker_ref.worker_id,
|
worker_id: worker_ref.worker_id,
|
||||||
message: "worker restore reconciliation is pending".to_string(),
|
message: format!("worker {operation} reconciliation is pending"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if worker.execution_handle.is_some() {
|
if worker.execution_handle.is_some() {
|
||||||
@@ -2734,9 +2734,9 @@ impl Runtime {
|
|||||||
let state = self.lock()?;
|
let state = self.lock()?;
|
||||||
state.ensure_running()?;
|
state.ensure_running()?;
|
||||||
let worker = state.worker(worker_ref)?;
|
let worker = state.worker(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(format!(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"worker {} has pending restore reconciliation",
|
"worker {} has pending {operation} reconciliation",
|
||||||
worker_ref.worker_id
|
worker_ref.worker_id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -2793,9 +2793,9 @@ impl Runtime {
|
|||||||
state.ensure_running()?;
|
state.ensure_running()?;
|
||||||
state.ensure_worker_ref(worker_ref)?;
|
state.ensure_worker_ref(worker_ref)?;
|
||||||
let worker = state.worker(worker_ref)?;
|
let worker = state.worker(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(format!(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"worker {} has pending restore reconciliation and cannot be deleted",
|
"worker {} has pending {operation} reconciliation and cannot be deleted",
|
||||||
worker_ref.worker_id
|
worker_ref.worker_id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -2834,9 +2834,9 @@ impl Runtime {
|
|||||||
state.ensure_running()?;
|
state.ensure_running()?;
|
||||||
state.ensure_worker_ref(worker_ref)?;
|
state.ensure_worker_ref(worker_ref)?;
|
||||||
let worker = state.worker(worker_ref)?;
|
let worker = state.worker(worker_ref)?;
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(format!(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"worker {} still has pending restore reconciliation",
|
"worker {} still has pending {operation} reconciliation",
|
||||||
worker_ref.worker_id
|
worker_ref.worker_id
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -3619,10 +3619,10 @@ impl Runtime {
|
|||||||
"Worker retention requires a stopped Worker".to_string(),
|
"Worker retention requires a stopped Worker".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if worker.has_pending_lifecycle_operation() {
|
if let Some(operation) = worker.pending_lifecycle_operation_name() {
|
||||||
return Err(RuntimeError::InvalidRequest(
|
return Err(RuntimeError::InvalidRequest(format!(
|
||||||
"Worker retention cannot remove pending restore reconciliation".to_string(),
|
"Worker retention cannot remove pending {operation} reconciliation"
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
let result = provider.execute(request)?;
|
let result = provider.execute(request)?;
|
||||||
state.workers.remove(&request.worker_id);
|
state.workers.remove(&request.worker_id);
|
||||||
@@ -4801,8 +4801,18 @@ impl WorkerRecord {
|
|||||||
self.worker_state = Some(incoming.clone());
|
self.worker_state = Some(incoming.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pending_lifecycle_operation_name(&self) -> Option<&'static str> {
|
||||||
|
if self.pending_stop.is_some() {
|
||||||
|
Some("stop")
|
||||||
|
} else if self.pending_restore.is_some() {
|
||||||
|
Some("restore")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn has_pending_lifecycle_operation(&self) -> bool {
|
fn has_pending_lifecycle_operation(&self) -> bool {
|
||||||
self.pending_restore.is_some() || self.pending_stop.is_some()
|
self.pending_lifecycle_operation_name().is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn belongs_to_workspace(&self, workspace_id: &str) -> bool {
|
fn belongs_to_workspace(&self, workspace_id: &str) -> bool {
|
||||||
@@ -7418,6 +7428,30 @@ mod tests {
|
|||||||
assert!(runtime.worker_operations.lock().unwrap().is_empty());
|
assert!(runtime.worker_operations.lock().unwrap().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn assert_pending_lifecycle_errors(runtime: &Runtime, worker_ref: &WorkerRef, operation: &str) {
|
||||||
|
let input_error = runtime
|
||||||
|
.send_input(worker_ref, WorkerInput::user("blocked while pending"))
|
||||||
|
.unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
input_error.to_string(),
|
||||||
|
format!(
|
||||||
|
"worker {} execution is unavailable: worker {operation} reconciliation is pending",
|
||||||
|
worker_ref.worker_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
for error in [
|
||||||
|
runtime.cancel_worker(worker_ref, None).unwrap_err(),
|
||||||
|
runtime.delete_worker(worker_ref).unwrap_err(),
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.to_string()
|
||||||
|
.contains(&format!("pending {operation} reconciliation")),
|
||||||
|
"{error}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn failed_stop_cleanup_retains_execution_for_retry() {
|
fn failed_stop_cleanup_retains_execution_for_retry() {
|
||||||
let (runtime, backend) = runtime_and_backend();
|
let (runtime, backend) = runtime_and_backend();
|
||||||
@@ -7454,6 +7488,7 @@ mod tests {
|
|||||||
backend.stop_operation_ids.lock().unwrap().as_slice(),
|
backend.stop_operation_ids.lock().unwrap().as_slice(),
|
||||||
&[operation_id]
|
&[operation_id]
|
||||||
);
|
);
|
||||||
|
assert_pending_lifecycle_errors(&runtime, &created.worker_ref, "stop");
|
||||||
|
|
||||||
let stopped = runtime.stop_worker(&created.worker_ref, None).unwrap();
|
let stopped = runtime.stop_worker(&created.worker_ref, None).unwrap();
|
||||||
assert_eq!(stopped.status, WorkerStatus::Stopped);
|
assert_eq!(stopped.status, WorkerStatus::Stopped);
|
||||||
@@ -9133,6 +9168,7 @@ mod tests {
|
|||||||
.pending_restore
|
.pending_restore
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.operation_id;
|
.operation_id;
|
||||||
|
assert_pending_lifecycle_errors(&runtime, &created.worker_ref, "restore");
|
||||||
let delete_error = runtime.delete_worker(&created.worker_ref).unwrap_err();
|
let delete_error = runtime.delete_worker(&created.worker_ref).unwrap_err();
|
||||||
assert!(
|
assert!(
|
||||||
delete_error
|
delete_error
|
||||||
|
|||||||
@@ -5522,6 +5522,10 @@ fn sanitize_embedded_execution_message(
|
|||||||
) -> String {
|
) -> String {
|
||||||
let summary =
|
let summary =
|
||||||
format!("Embedded Worker execution backend rejected {operation:?} with {outcome:?}");
|
format!("Embedded Worker execution backend rejected {operation:?} with {outcome:?}");
|
||||||
|
sanitize_embedded_execution_detail(&summary, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sanitize_embedded_execution_detail(summary: &str, message: &str) -> String {
|
||||||
let mut redact_next = false;
|
let mut redact_next = false;
|
||||||
let detail = message
|
let detail = message
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
@@ -5558,7 +5562,7 @@ fn sanitize_embedded_execution_message(
|
|||||||
.join(" ");
|
.join(" ");
|
||||||
let detail = detail.trim();
|
let detail = detail.trim();
|
||||||
if detail.is_empty() {
|
if detail.is_empty() {
|
||||||
return summary;
|
return summary.to_string();
|
||||||
}
|
}
|
||||||
let truncated = detail.chars().count() > 512;
|
let truncated = detail.chars().count() > 512;
|
||||||
let mut detail = detail.chars().take(512).collect::<String>();
|
let mut detail = detail.chars().take(512).collect::<String>();
|
||||||
@@ -5580,11 +5584,18 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
|
|||||||
HostDiagnosticSeverity::Warning,
|
HostDiagnosticSeverity::Warning,
|
||||||
"Embedded Runtime worker was not found".to_string(),
|
"Embedded Runtime worker was not found".to_string(),
|
||||||
),
|
),
|
||||||
EmbeddedRuntimeError::WorkerExecutionUnavailable { .. }
|
EmbeddedRuntimeError::WorkerExecutionUnavailable { message, .. } => diagnostic(
|
||||||
| EmbeddedRuntimeError::ExecutionBackendUnavailable { .. } => diagnostic(
|
|
||||||
"embedded_worker_execution_unavailable",
|
"embedded_worker_execution_unavailable",
|
||||||
HostDiagnosticSeverity::Warning,
|
HostDiagnosticSeverity::Warning,
|
||||||
"Embedded Worker has no execution backend attached".to_string(),
|
sanitize_embedded_execution_detail("Embedded Worker execution is unavailable", message),
|
||||||
|
),
|
||||||
|
EmbeddedRuntimeError::ExecutionBackendUnavailable { message } => diagnostic(
|
||||||
|
"embedded_worker_execution_unavailable",
|
||||||
|
HostDiagnosticSeverity::Warning,
|
||||||
|
sanitize_embedded_execution_detail(
|
||||||
|
"Embedded Runtime execution backend is unavailable",
|
||||||
|
message,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
EmbeddedRuntimeError::WorkerExecutionRejected {
|
EmbeddedRuntimeError::WorkerExecutionRejected {
|
||||||
operation,
|
operation,
|
||||||
@@ -6055,6 +6066,58 @@ mod tests {
|
|||||||
assert_eq!(result.diagnostics.len(), 1);
|
assert_eq!(result.diagnostics.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_execution_unavailable_preserves_the_actual_reason() {
|
||||||
|
for reason in [
|
||||||
|
"worker stop reconciliation is pending",
|
||||||
|
"worker restore reconciliation is pending",
|
||||||
|
"stopped worker requires an explicit restore",
|
||||||
|
"worker has no live execution handle",
|
||||||
|
"runtime has no execution backend",
|
||||||
|
] {
|
||||||
|
let diagnostic =
|
||||||
|
embedded_runtime_diagnostic(&EmbeddedRuntimeError::WorkerExecutionUnavailable {
|
||||||
|
worker_id: EmbeddedWorkerId::now_v7(),
|
||||||
|
message: reason.to_string(),
|
||||||
|
});
|
||||||
|
assert_eq!(diagnostic.code, "embedded_worker_execution_unavailable");
|
||||||
|
assert_eq!(diagnostic.severity, HostDiagnosticSeverity::Warning);
|
||||||
|
assert_eq!(
|
||||||
|
diagnostic.message,
|
||||||
|
format!("Embedded Worker execution is unavailable: {reason}")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let diagnostic =
|
||||||
|
embedded_runtime_diagnostic(&EmbeddedRuntimeError::ExecutionBackendUnavailable {
|
||||||
|
message: "Runtime has no execution backend".to_string(),
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
diagnostic.message,
|
||||||
|
"Embedded Runtime execution backend is unavailable: Runtime has no execution backend"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn embedded_execution_unavailable_detail_remains_bounded_and_redacted() {
|
||||||
|
let diagnostic = embedded_runtime_diagnostic(
|
||||||
|
&EmbeddedRuntimeError::WorkerExecutionUnavailable {
|
||||||
|
worker_id: EmbeddedWorkerId::now_v7(),
|
||||||
|
message: format!(
|
||||||
|
"worker stop reconciliation is pending /private/runtime Bearer sensitive-value {}",
|
||||||
|
"x".repeat(600)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
diagnostic
|
||||||
|
.message
|
||||||
|
.contains("stop reconciliation is pending")
|
||||||
|
);
|
||||||
|
assert!(!diagnostic.message.contains("/private/runtime"));
|
||||||
|
assert!(!diagnostic.message.contains("sensitive-value"));
|
||||||
|
assert!(diagnostic.message.chars().count() < 600);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn embedded_delete_persistence_failure_diagnostic_is_bounded_and_path_free() {
|
fn embedded_delete_persistence_failure_diagnostic_is_bounded_and_path_free() {
|
||||||
let diagnostic =
|
let diagnostic =
|
||||||
|
|||||||
Reference in New Issue
Block a user