use anyhow::Result;
use thiserror::Error;
use crate::{
client::post_graphql,
commands::{Configs, queries},
util::retry::{RetryConfig, retry_with_backoff},
};
#[derive(Debug, Error)]
pub enum WorkflowError {
#[error("workflow failed: {0}")]
Failed(String),
#[error("workflow not found")]
NotFound,
#[error("workflow timed out")]
Timeout,
}
enum TerminalPoll {
Complete,
Failed(String),
}
pub async fn wait_for_workflow(
client: &reqwest::Client,
configs: &Configs,
workflow_id: String,
) -> Result<(), WorkflowError> {
let backboard = configs.get_backboard();
let result = retry_with_backoff(
RetryConfig {
max_attempts: 120, initial_delay_ms: 1000,
max_delay_ms: 2000,
backoff_multiplier: 1.0,
on_retry: None,
},
|| {
let client = client.clone();
let backboard = backboard.clone();
let workflow_id = workflow_id.clone();
async move {
let result = post_graphql::<queries::WorkflowStatus, _>(
&client,
backboard,
queries::workflow_status::Variables { workflow_id },
)
.await?;
use queries::workflow_status::WorkflowStatus;
match result.workflow_status.status {
WorkflowStatus::Complete => Ok(TerminalPoll::Complete),
WorkflowStatus::Error => {
let error_msg = result
.workflow_status
.error
.filter(|e| !e.is_empty())
.unwrap_or_else(|| "Unknown error".to_string());
Ok(TerminalPoll::Failed(error_msg))
}
WorkflowStatus::NotFound => Err(WorkflowError::NotFound.into()),
WorkflowStatus::Running | WorkflowStatus::Other(_) => {
Err(WorkflowError::Timeout.into())
}
}
}
},
)
.await;
match result {
Ok(TerminalPoll::Complete) => Ok(()),
Ok(TerminalPoll::Failed(message)) => Err(WorkflowError::Failed(message)),
Err(e) => Err(e
.downcast::<WorkflowError>()
.unwrap_or_else(|e| WorkflowError::Failed(e.to_string()))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testkit::MockBackboard;
use serde_json::json;
fn status(value: &str, error: Option<&str>) -> serde_json::Value {
json!({ "workflowStatus": { "status": value, "error": error } })
}
#[tokio::test]
async fn complete_on_first_poll_returns_ok() {
let dir = tempfile::tempdir().unwrap();
let server = MockBackboard::spawn();
server.stub("WorkflowStatus", status("Complete", None));
let configs = server.configs(&dir);
let client = reqwest::Client::new();
wait_for_workflow(&client, &configs, "wf-1".to_string())
.await
.unwrap();
assert_eq!(server.hits(), 1);
assert_eq!(
server.variables_for("WorkflowStatus"),
vec![json!({ "workflowId": "wf-1" })]
);
}
#[tokio::test]
async fn failed_workflow_surfaces_immediately_with_its_message() {
let dir = tempfile::tempdir().unwrap();
let server = MockBackboard::spawn();
server.stub(
"WorkflowStatus",
status("Error", Some("volume detach failed")),
);
let configs = server.configs(&dir);
let client = reqwest::Client::new();
let err = wait_for_workflow(&client, &configs, "wf-1".to_string())
.await
.unwrap_err();
assert!(matches!(err, WorkflowError::Failed(ref m) if m == "volume detach failed"));
assert_eq!(server.hits(), 1);
}
#[tokio::test]
async fn failed_workflow_without_detail_reports_unknown_error() {
let dir = tempfile::tempdir().unwrap();
let server = MockBackboard::spawn();
server.stub("WorkflowStatus", status("Error", Some("")));
let configs = server.configs(&dir);
let client = reqwest::Client::new();
let err = wait_for_workflow(&client, &configs, "wf-1".to_string())
.await
.unwrap_err();
assert!(matches!(err, WorkflowError::Failed(ref m) if m == "Unknown error"));
}
#[tokio::test]
async fn running_polls_until_complete() {
let dir = tempfile::tempdir().unwrap();
let server = MockBackboard::spawn();
server.stub("WorkflowStatus", status("Running", None));
server.stub("WorkflowStatus", status("Complete", None));
let configs = server.configs(&dir);
let client = reqwest::Client::new();
wait_for_workflow(&client, &configs, "wf-1".to_string())
.await
.unwrap();
assert_eq!(server.hits(), 2);
}
#[tokio::test]
async fn not_found_tolerates_read_after_write_lag() {
let dir = tempfile::tempdir().unwrap();
let server = MockBackboard::spawn();
server.stub("WorkflowStatus", status("NotFound", None));
server.stub("WorkflowStatus", status("Complete", None));
let configs = server.configs(&dir);
let client = reqwest::Client::new();
wait_for_workflow(&client, &configs, "wf-1".to_string())
.await
.unwrap();
assert_eq!(server.hits(), 2);
}
}