use std::time::{Duration, Instant}; use anyhow::{anyhow, Result}; use disasmer_core::TaskInstanceId; use serde_json::{json, Value}; use crate::virtual_model::AdapterState; use super::{ client_user_request, coordinator_request, DebugEpochRecord, DebugEpochStatusRecord, TaskRestartRecord, }; pub(super) fn coordinator_debug_epoch_request( state: &AdapterState, payload: Value, ) -> Result { let coordinator = crate::view_state::normalize_coordinator_endpoint(&state.coordinator_endpoint); coordinator_request(&coordinator, payload) } pub(super) fn parse_debug_epoch_response(response: Value) -> Result { let epoch = response .get("epoch") .and_then(Value::as_u64) .ok_or_else(|| anyhow!("coordinator debug epoch response did not include an epoch"))?; let command = response .get("command") .and_then(Value::as_str) .ok_or_else(|| anyhow!("coordinator debug epoch response did not include a command"))? .to_owned(); let affected_tasks = response .get("affected_tasks") .and_then(Value::as_array) .map(Vec::len) .unwrap_or(0); Ok(DebugEpochRecord { epoch, command, affected_tasks, }) } pub(super) fn wait_for_debug_epoch_state( state: &AdapterState, epoch: u64, frozen: bool, ) -> Result { let deadline = Instant::now() + Duration::from_secs(60); loop { let response = coordinator_debug_epoch_request( state, client_user_request( state, json!({ "type": "inspect_debug_epoch", "tenant": state.tenant, "project": state.project_id, "actor_user": state.actor_user, "process": state.process.as_str(), "epoch": epoch, }), ), )?; let status = parse_debug_epoch_status(response)?; if status.failed { return Err(anyhow!( "debug epoch {epoch} participant failed: {}", status.failure_messages.join("; ") )); } if (frozen && status.fully_frozen) || (!frozen && status.fully_resumed) { return Ok(status); } if Instant::now() >= deadline { return Err(anyhow!( "debug epoch {epoch} did not receive {}/{} signed participant acknowledgements for {} within 60 seconds", status.acknowledgements.len(), status.expected_tasks, if frozen { "frozen state" } else { "resumed state" } )); } std::thread::sleep(Duration::from_millis(100)); } } fn parse_debug_epoch_status(response: Value) -> Result { let epoch = response .get("epoch") .and_then(Value::as_u64) .ok_or_else(|| anyhow!("coordinator debug epoch status omitted epoch"))?; let command = response .get("command") .and_then(Value::as_str) .ok_or_else(|| anyhow!("coordinator debug epoch status omitted command"))? .to_owned(); let expected_tasks = response .get("expected_tasks") .and_then(Value::as_array) .map(Vec::len) .unwrap_or(0); let acknowledgements = response .get("acknowledgements") .and_then(Value::as_array) .cloned() .unwrap_or_default(); let failure_messages = response .get("failure_messages") .and_then(Value::as_array) .into_iter() .flatten() .filter_map(Value::as_str) .map(str::to_owned) .collect(); Ok(DebugEpochStatusRecord { epoch, command, expected_tasks, acknowledgements, fully_frozen: response .get("fully_frozen") .and_then(Value::as_bool) .unwrap_or(false), fully_resumed: response .get("fully_resumed") .and_then(Value::as_bool) .unwrap_or(false), failed: response .get("failed") .and_then(Value::as_bool) .unwrap_or(false), failure_messages, }) } pub(crate) fn parse_task_restart_response(response: Value) -> Result { Ok(TaskRestartRecord { accepted: response .get("accepted") .and_then(Value::as_bool) .ok_or_else(|| anyhow!("coordinator task restart response did not include accepted"))?, restarted_task_instance: response .get("restarted_task_instance") .and_then(Value::as_str) .map(TaskInstanceId::new), clean_boundary_available: response .get("clean_boundary_available") .and_then(Value::as_bool) .ok_or_else(|| { anyhow!("coordinator task restart response did not include clean_boundary_available") })?, requires_whole_process_restart: response .get("requires_whole_process_restart") .and_then(Value::as_bool) .ok_or_else(|| { anyhow!( "coordinator task restart response did not include requires_whole_process_restart" ) })?, active_task: response .get("active_task") .and_then(Value::as_bool) .ok_or_else(|| anyhow!("coordinator task restart response did not include active_task"))?, completed_event_observed: response .get("completed_event_observed") .and_then(Value::as_bool) .ok_or_else(|| { anyhow!("coordinator task restart response did not include completed_event_observed") })?, message: response .get("message") .and_then(Value::as_str) .ok_or_else(|| anyhow!("coordinator task restart response did not include message"))? .to_owned(), }) }