use crate::client::GcpHttpClient;
use crate::error::truncate_for_error;
use crate::resource::is_scoped_resource_name;
use adk_core::Result;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
use tokio::time::Instant;
const DEFAULT_POLL_TIMEOUT: Duration = Duration::from_secs(120);
const DEFAULT_INITIAL_DELAY: Duration = Duration::from_millis(100);
const DEFAULT_MAX_DELAY: Duration = Duration::from_secs(2);
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Operation {
pub name: String,
#[serde(default)]
pub done: bool,
#[serde(default)]
pub error: Option<OperationError>,
#[serde(default)]
pub response: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct OperationError {
#[serde(default)]
pub code: i64,
#[serde(default)]
pub message: String,
}
#[derive(Debug, Clone)]
pub struct LroPoller {
poll_timeout: Duration,
initial_delay: Duration,
max_delay: Duration,
}
impl Default for LroPoller {
fn default() -> Self {
Self::new()
}
}
impl LroPoller {
pub fn new() -> Self {
Self {
poll_timeout: DEFAULT_POLL_TIMEOUT,
initial_delay: DEFAULT_INITIAL_DELAY,
max_delay: DEFAULT_MAX_DELAY,
}
}
#[must_use]
pub fn with_poll_timeout(mut self, timeout: Duration) -> Self {
self.poll_timeout = timeout;
self
}
#[must_use]
pub fn with_initial_delay(mut self, delay: Duration) -> Self {
self.initial_delay = delay;
self
}
#[must_use]
pub fn with_max_delay(mut self, delay: Duration) -> Self {
self.max_delay = delay;
self
}
pub async fn wait_for_operation(
&self,
client: &GcpHttpClient,
initial: Value,
operation_kind: &str,
require_response: bool,
project_id: &str,
location: &str,
) -> Result<Option<Value>> {
let errors = client.errors();
let mut operation = parse_operation(client, initial, operation_kind)?;
if !is_scoped_resource_name(&operation.name, project_id, location) {
let name = truncate_for_error(&operation.name);
return Err(errors.invalid_response(format!(
"{} operation name '{name}' does not belong to projects/{project_id}/locations/{location}",
errors.subject(),
)));
}
let operation_name = operation.name.clone();
let deadline = Instant::now() + self.poll_timeout;
let mut delay = self.initial_delay;
loop {
if operation.done {
if let Some(error) = operation.error {
return Err(errors.operation_error(
operation_kind,
&operation_name,
error.code,
&error.message,
));
}
if require_response && operation.response.is_none() {
return Err(errors.invalid_response(format!(
"{} {operation_kind} operation '{operation_name}' completed without a response; retry the request and inspect the operation in Google Cloud",
errors.subject(),
)));
}
return Ok(operation.response);
}
if Instant::now() >= deadline {
return Err(self.deadline_error(client, operation_kind, &operation_name));
}
let remaining = deadline.saturating_duration_since(Instant::now());
let poll = async {
let request = client.request(Method::GET, &operation_name).await?;
client.send_value(request).await
};
let value = tokio::time::timeout(remaining, poll)
.await
.map_err(|_| self.deadline_error(client, operation_kind, &operation_name))??;
let next = parse_operation(client, value, operation_kind)?;
if next.name != operation_name {
return Err(errors.invalid_response(format!(
"{} {operation_kind} poll changed operation identity from '{operation_name}' to '{}'; refusing to follow a different operation",
errors.subject(),
truncate_for_error(&next.name),
)));
}
operation = next;
if !operation.done {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
continue;
}
tokio::time::sleep(delay.min(remaining)).await;
delay = delay.saturating_mul(2).min(self.max_delay);
}
}
}
fn deadline_error(
&self,
client: &GcpHttpClient,
operation_kind: &str,
operation_name: &str,
) -> adk_core::AdkError {
let errors = client.errors();
errors.timeout(format!(
"{} {operation_kind} operation '{operation_name}' did not complete within {} seconds; inspect the operation in Google Cloud before retrying",
errors.subject(),
self.poll_timeout.as_secs_f64(),
))
}
}
fn parse_operation(
client: &GcpHttpClient,
value: Value,
operation_kind: &str,
) -> Result<Operation> {
let errors = client.errors();
let operation: Operation = serde_json::from_value(value).map_err(|error| {
let error = truncate_for_error(&error.to_string());
errors.invalid_response(format!(
"failed to parse {} {operation_kind} operation: {error}",
errors.subject(),
))
})?;
if operation.name.trim().is_empty() {
return Err(errors.invalid_response(format!(
"{} {operation_kind} response did not contain an operation name",
errors.subject(),
)));
}
if operation.error.is_some() && operation.response.is_some() {
return Err(errors.invalid_response(format!(
"{} {operation_kind} operation '{}' contains both error and response results",
errors.subject(),
truncate_for_error(&operation.name),
)));
}
if !operation.done && (operation.error.is_some() || operation.response.is_some()) {
return Err(errors.invalid_response(format!(
"{} {operation_kind} operation '{}' contains a terminal result while done is false",
errors.subject(),
truncate_for_error(&operation.name),
)));
}
Ok(operation)
}