fix: log failed worker creations to stdout
This commit is contained in:
@@ -47,6 +47,7 @@ use protocol::{Event, Method};
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
use std::io::Write as _;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex, MutexGuard, Weak};
|
use std::sync::{Arc, Mutex, MutexGuard, Weak};
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
@@ -783,6 +784,20 @@ impl Runtime {
|
|||||||
&self,
|
&self,
|
||||||
request: CreateWorkerRequest,
|
request: CreateWorkerRequest,
|
||||||
scope: Option<&RuntimeWorkspaceScope>,
|
scope: Option<&RuntimeWorkspaceScope>,
|
||||||
|
) -> Result<WorkerDetail, RuntimeError> {
|
||||||
|
let worker_id = request.worker_id;
|
||||||
|
let workspace_id = scope.map(|scope| scope.workspace_id.as_str());
|
||||||
|
let result = self.create_worker_with_workspace_inner(request, scope);
|
||||||
|
if let Err(error) = &result {
|
||||||
|
write_runtime_worker_create_failure(worker_id, workspace_id, error);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_worker_with_workspace_inner(
|
||||||
|
&self,
|
||||||
|
request: CreateWorkerRequest,
|
||||||
|
scope: Option<&RuntimeWorkspaceScope>,
|
||||||
) -> Result<WorkerDetail, RuntimeError> {
|
) -> Result<WorkerDetail, RuntimeError> {
|
||||||
let operation_lock = self.worker_operation_lock(request.worker_id)?;
|
let operation_lock = self.worker_operation_lock(request.worker_id)?;
|
||||||
let _operation_guard = operation_lock
|
let _operation_guard = operation_lock
|
||||||
@@ -3362,6 +3377,69 @@ fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), R
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn runtime_worker_create_failure_log_line(
|
||||||
|
worker_id: WorkerId,
|
||||||
|
workspace_id: Option<&str>,
|
||||||
|
error: &RuntimeError,
|
||||||
|
) -> String {
|
||||||
|
let (error_kind, operation, outcome) = match error {
|
||||||
|
RuntimeError::RuntimeStopped => ("runtime_stopped", None, None),
|
||||||
|
RuntimeError::InvalidInitialInputKind { .. } => ("invalid_initial_input_kind", None, None),
|
||||||
|
RuntimeError::WorkerNotFound { .. } => ("worker_not_found", None, None),
|
||||||
|
RuntimeError::WorkerExecutionUnavailable { .. } => {
|
||||||
|
("worker_execution_unavailable", None, None)
|
||||||
|
}
|
||||||
|
RuntimeError::ExecutionBackendUnavailable { .. } => {
|
||||||
|
("execution_backend_unavailable", None, None)
|
||||||
|
}
|
||||||
|
RuntimeError::WorkerExecutionRejected {
|
||||||
|
operation, outcome, ..
|
||||||
|
} => (
|
||||||
|
"worker_execution_rejected",
|
||||||
|
Some(format!("{operation:?}")),
|
||||||
|
Some(format!("{outcome:?}")),
|
||||||
|
),
|
||||||
|
RuntimeError::LimitTooLarge { .. } => ("limit_too_large", None, None),
|
||||||
|
RuntimeError::InvalidRequest(_) => ("invalid_request", None, None),
|
||||||
|
RuntimeError::WorkspaceOwnerMismatch { .. } => ("workspace_owner_mismatch", None, None),
|
||||||
|
RuntimeError::WorkingDirectory(_) => ("working_directory", None, None),
|
||||||
|
RuntimeError::ConfigBundleMissing { .. } => ("config_bundle_missing", None, None),
|
||||||
|
RuntimeError::ConfigBundleDigestMismatch { .. } => {
|
||||||
|
("config_bundle_digest_mismatch", None, None)
|
||||||
|
}
|
||||||
|
RuntimeError::InvalidProfileSelector { .. } => ("invalid_profile_selector", None, None),
|
||||||
|
RuntimeError::UnsupportedConfigDeclaration { .. } => {
|
||||||
|
("unsupported_config_declaration", None, None)
|
||||||
|
}
|
||||||
|
RuntimeError::StoreIo { .. } => ("store_io", None, None),
|
||||||
|
RuntimeError::StoreMissing { .. } => ("store_missing", None, None),
|
||||||
|
RuntimeError::StoreCorrupt { .. } => ("store_corrupt", None, None),
|
||||||
|
RuntimeError::StatePoisoned => ("state_poisoned", None, None),
|
||||||
|
};
|
||||||
|
serde_json::json!({
|
||||||
|
"level": "ERROR",
|
||||||
|
"event": "worker_create_failed",
|
||||||
|
"component": "runtime",
|
||||||
|
"workspace_id": workspace_id,
|
||||||
|
"worker_id": worker_id.to_string(),
|
||||||
|
"error_kind": error_kind,
|
||||||
|
"operation": operation,
|
||||||
|
"outcome": outcome,
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_runtime_worker_create_failure(
|
||||||
|
worker_id: WorkerId,
|
||||||
|
workspace_id: Option<&str>,
|
||||||
|
error: &RuntimeError,
|
||||||
|
) {
|
||||||
|
let line = runtime_worker_create_failure_log_line(worker_id, workspace_id, error);
|
||||||
|
let mut stdout = std::io::stdout().lock();
|
||||||
|
let _ = writeln!(stdout, "{line}");
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_create_workspace_scope(
|
fn validate_create_workspace_scope(
|
||||||
request: &CreateWorkerRequest,
|
request: &CreateWorkerRequest,
|
||||||
workspace_id: Option<&str>,
|
workspace_id: Option<&str>,
|
||||||
@@ -3485,6 +3563,24 @@ mod tests {
|
|||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_create_failure_log_is_structured_for_stdout() {
|
||||||
|
let worker_id = WorkerId::now_v7();
|
||||||
|
let line = runtime_worker_create_failure_log_line(
|
||||||
|
worker_id,
|
||||||
|
Some("workspace-a"),
|
||||||
|
&RuntimeError::InvalidRequest("rejected create".to_string()),
|
||||||
|
);
|
||||||
|
let event: serde_json::Value = serde_json::from_str(&line).unwrap();
|
||||||
|
assert_eq!(event["level"], "ERROR");
|
||||||
|
assert_eq!(event["event"], "worker_create_failed");
|
||||||
|
assert_eq!(event["component"], "runtime");
|
||||||
|
assert_eq!(event["workspace_id"], "workspace-a");
|
||||||
|
assert_eq!(event["worker_id"], worker_id.to_string());
|
||||||
|
assert_eq!(event["error_kind"], "invalid_request");
|
||||||
|
assert!(event.get("message").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
fn test_command() -> protocol::WorkerCommandEnvelope {
|
fn test_command() -> protocol::WorkerCommandEnvelope {
|
||||||
protocol::WorkerCommandEnvelope {
|
protocol::WorkerCommandEnvelope {
|
||||||
command_id: 1,
|
command_id: 1,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
|
use std::io::Write as _;
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::{Arc, Mutex, RwLock, Weak};
|
use std::sync::{Arc, Mutex, RwLock, Weak};
|
||||||
@@ -2406,6 +2407,12 @@ impl WorkspaceApi {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
write_workspace_worker_create_failure(
|
||||||
|
runtime_id,
|
||||||
|
worker_id,
|
||||||
|
"workdir_attachment_reserve",
|
||||||
|
&diagnostics,
|
||||||
|
);
|
||||||
return Err(ApiError::with_diagnostics(error, diagnostics));
|
return Err(ApiError::with_diagnostics(error, diagnostics));
|
||||||
}
|
}
|
||||||
let mut result = match self
|
let mut result = match self
|
||||||
@@ -2419,6 +2426,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"runtime_spawn_transport",
|
||||||
None,
|
None,
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2438,6 +2446,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"runtime_spawn_rejected",
|
||||||
None,
|
None,
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2455,6 +2464,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"runtime_worker_identity",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2481,6 +2491,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"runtime_spawn_rejected",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2503,6 +2514,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"workspace_api_transport",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2520,6 +2532,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"workspace_api_rejected",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
attachment_reservation
|
attachment_reservation
|
||||||
@@ -2550,6 +2563,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"worker_registry_identity",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
Some((workdir_id.as_str(), reservation_id.as_str())),
|
Some((workdir_id.as_str(), reservation_id.as_str())),
|
||||||
@@ -2581,6 +2595,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"worker_registry_finalize",
|
||||||
None,
|
None,
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
Some((workdir_id.as_str(), reservation_id.as_str())),
|
Some((workdir_id.as_str(), reservation_id.as_str())),
|
||||||
@@ -2612,6 +2627,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"workdir_attachment_finalize",
|
||||||
None,
|
None,
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
Some((workdir_id.as_str(), reservation_id.as_str())),
|
Some((workdir_id.as_str(), reservation_id.as_str())),
|
||||||
@@ -2628,6 +2644,7 @@ impl WorkspaceApi {
|
|||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
&reservation_fingerprint,
|
&reservation_fingerprint,
|
||||||
|
"create_reservation_complete",
|
||||||
Some(worker),
|
Some(worker),
|
||||||
&compensation_context,
|
&compensation_context,
|
||||||
None,
|
None,
|
||||||
@@ -14275,6 +14292,37 @@ fn finalize_worker_spawn_stage<T>(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn workspace_worker_create_failure_log_line(
|
||||||
|
runtime_id: &str,
|
||||||
|
worker_id: WorkerId,
|
||||||
|
phase: &str,
|
||||||
|
diagnostics: &[RuntimeDiagnostic],
|
||||||
|
) -> String {
|
||||||
|
serde_json::json!({
|
||||||
|
"level": "ERROR",
|
||||||
|
"event": "worker_create_failed",
|
||||||
|
"component": "workspace_server",
|
||||||
|
"runtime_id": runtime_id,
|
||||||
|
"worker_id": worker_id.to_string(),
|
||||||
|
"phase": phase,
|
||||||
|
"cleanup_succeeded": diagnostics.is_empty(),
|
||||||
|
"cleanup_diagnostics": diagnostics,
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_workspace_worker_create_failure(
|
||||||
|
runtime_id: &str,
|
||||||
|
worker_id: WorkerId,
|
||||||
|
phase: &str,
|
||||||
|
diagnostics: &[RuntimeDiagnostic],
|
||||||
|
) {
|
||||||
|
let line = workspace_worker_create_failure_log_line(runtime_id, worker_id, phase, diagnostics);
|
||||||
|
let mut stdout = std::io::stdout().lock();
|
||||||
|
let _ = writeln!(stdout, "{line}");
|
||||||
|
let _ = stdout.flush();
|
||||||
|
}
|
||||||
|
|
||||||
fn api_error_with_additional_diagnostics(
|
fn api_error_with_additional_diagnostics(
|
||||||
mut error: ApiError,
|
mut error: ApiError,
|
||||||
diagnostics: Vec<RuntimeDiagnostic>,
|
diagnostics: Vec<RuntimeDiagnostic>,
|
||||||
@@ -14288,6 +14336,7 @@ fn compensate_failed_workspace_worker_create(
|
|||||||
runtime_id: &str,
|
runtime_id: &str,
|
||||||
reservation_worker_id: WorkerId,
|
reservation_worker_id: WorkerId,
|
||||||
create_fingerprint: &str,
|
create_fingerprint: &str,
|
||||||
|
failure_phase: &str,
|
||||||
worker: Option<&WorkerSummary>,
|
worker: Option<&WorkerSummary>,
|
||||||
context: &WorkerSpawnCompensationContext<'_>,
|
context: &WorkerSpawnCompensationContext<'_>,
|
||||||
attachment_reservation: Option<(&str, &str)>,
|
attachment_reservation: Option<(&str, &str)>,
|
||||||
@@ -14355,6 +14404,12 @@ fn compensate_failed_workspace_worker_create(
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
write_workspace_worker_create_failure(
|
||||||
|
runtime_id,
|
||||||
|
reservation_worker_id,
|
||||||
|
failure_phase,
|
||||||
|
&diagnostics,
|
||||||
|
);
|
||||||
diagnostics
|
diagnostics
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19557,6 +19612,34 @@ mod tests {
|
|||||||
assert_eq!(sanitized, "failed to open server database");
|
assert_eq!(sanitized, "failed to open server database");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn worker_create_failure_log_is_structured_for_stdout() {
|
||||||
|
let worker_id = WorkerId::now_v7();
|
||||||
|
let diagnostics = vec![RuntimeDiagnostic {
|
||||||
|
code: "worker_cleanup_failed".to_string(),
|
||||||
|
severity: DiagnosticSeverity::Error,
|
||||||
|
message: "cleanup failed".to_string(),
|
||||||
|
}];
|
||||||
|
let line = workspace_worker_create_failure_log_line(
|
||||||
|
"runtime-a",
|
||||||
|
worker_id,
|
||||||
|
"runtime_spawn_transport",
|
||||||
|
&diagnostics,
|
||||||
|
);
|
||||||
|
let event: serde_json::Value = serde_json::from_str(&line).unwrap();
|
||||||
|
assert_eq!(event["level"], "ERROR");
|
||||||
|
assert_eq!(event["event"], "worker_create_failed");
|
||||||
|
assert_eq!(event["component"], "workspace_server");
|
||||||
|
assert_eq!(event["runtime_id"], "runtime-a");
|
||||||
|
assert_eq!(event["worker_id"], worker_id.to_string());
|
||||||
|
assert_eq!(event["phase"], "runtime_spawn_transport");
|
||||||
|
assert_eq!(event["cleanup_succeeded"], false);
|
||||||
|
assert_eq!(
|
||||||
|
event["cleanup_diagnostics"][0]["code"],
|
||||||
|
"worker_cleanup_failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn workdir_runtime_miss_uses_exact_typed_code() {
|
fn workdir_runtime_miss_uses_exact_typed_code() {
|
||||||
let typed_not_found = [RuntimeDiagnostic {
|
let typed_not_found = [RuntimeDiagnostic {
|
||||||
|
|||||||
Reference in New Issue
Block a user