//! Webhook Event Handler use serde::{Deserialize, Serialize}; use std::collections::HashMap; use rand; /// Webhook event type #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub enum WebhookEventType { RequestComplete, RequestFailed, SynthesisComplete, EntityLinkingComplete, InferenceComplete, ReasoningComplete, SummarizationComplete, } /// Webhook event #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebhookEvent { pub event_type: WebhookEventType, pub agent_id: String, pub timestamp: String, pub payload: WebhookPayload, } /// Webhook payload #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WebhookPayload { pub request_id: String, pub status: String, pub result: Option, pub error: Option, pub metadata: HashMap, } /// Webhook manager with exponential backoff + jitter pub struct WebhookManager { url: String, retry_count: u32, timeout_secs: u32, } impl WebhookManager { pub fn new(url: String) -> Self { WebhookManager { url, retry_count: 3, timeout_secs: 30, } } /// Create with custom retry count pub fn with_retry_count(mut self, count: u32) -> Self { self.retry_count = count; self } /// Send webhook event with exponential backoff + jitter pub async fn send(&self, event: &WebhookEvent) -> Result<(), String> { let client = reqwest::Client::new(); let mut retries = 0; loop { match client .post(&self.url) .json(event) .timeout(std::time::Duration::from_secs(self.timeout_secs as u64)) .send() .await { Ok(resp) if resp.status().is_success() => return Ok(()), Ok(resp) => { if retries < self.retry_count { let backoff = self.calculate_backoff(retries); retries += 1; tokio::time::sleep(backoff).await; } else { return Err(format!("Webhook failed after {} retries: {}", self.retry_count, resp.status())); } } Err(e) => { if retries < self.retry_count { let backoff = self.calculate_backoff(retries); retries += 1; tokio::time::sleep(backoff).await; } else { return Err(format!("Webhook error after {} retries: {}", self.retry_count, e)); } } } } } /// Calculate exponential backoff with jitter (prevents thundering herd) fn calculate_backoff(&self, retry_count: u32) -> std::time::Duration { let base_ms = 100_u64 * 2_u64.pow(retry_count); // Add ±10% jitter let jitter = (base_ms as f32 * 0.1 * (rand::random::() * 2.0 - 1.0)) as u64; let total_ms = base_ms.saturating_add_signed(jitter as i64); std::time::Duration::from_millis(total_ms) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_webhook_event_type() { let et = WebhookEventType::RequestComplete; assert_eq!(et, WebhookEventType::RequestComplete); } #[test] fn test_webhook_event_structure() { let event = WebhookEvent { event_type: WebhookEventType::RequestComplete, agent_id: "agent1".to_string(), timestamp: "2025-01-30T10:00:00Z".to_string(), payload: WebhookPayload { request_id: "req1".to_string(), status: "success".to_string(), result: None, error: None, metadata: HashMap::new(), }, }; assert_eq!(event.agent_id, "agent1"); } #[test] fn test_webhook_payload_structure() { let payload = WebhookPayload { request_id: "req1".to_string(), status: "success".to_string(), result: Some(serde_json::json!({"data": "test"})), error: None, metadata: HashMap::new(), }; assert_eq!(payload.request_id, "req1"); } #[test] fn test_webhook_manager_creation() { let manager = WebhookManager::new("http://localhost:8080/webhook".to_string()); assert_eq!(manager.url, "http://localhost:8080/webhook"); } #[test] fn test_webhook_manager_defaults() { let manager = WebhookManager::new("http://test".to_string()); assert_eq!(manager.retry_count, 3); assert_eq!(manager.timeout_secs, 30); } #[test] fn test_webhook_manager_custom_retry() { let manager = WebhookManager::new("http://test".to_string()) .with_retry_count(5); assert_eq!(manager.retry_count, 5); } #[test] fn test_backoff_calculation() { let manager = WebhookManager::new("http://test".to_string()); let backoff0 = manager.calculate_backoff(0); let backoff1 = manager.calculate_backoff(1); assert!(backoff1 > backoff0); // Exponential increase } #[test] fn test_webhook_event_types() { let types = vec![ WebhookEventType::RequestComplete, WebhookEventType::RequestFailed, WebhookEventType::SynthesisComplete, ]; assert_eq!(types.len(), 3); } #[test] fn test_webhook_payload_with_result() { let payload = WebhookPayload { request_id: "r1".to_string(), status: "ok".to_string(), result: Some(serde_json::json!({"answer": "42"})), error: None, metadata: HashMap::new(), }; assert!(payload.result.is_some()); } #[test] fn test_webhook_payload_with_error() { let payload = WebhookPayload { request_id: "r1".to_string(), status: "error".to_string(), result: None, error: Some("Failed".to_string()), metadata: HashMap::new(), }; assert!(payload.error.is_some()); } #[test] fn test_webhook_retry_message() { let manager = WebhookManager::new("http://test".to_string()); let msg = format!("Webhook failed after {} retries", manager.retry_count); assert!(msg.contains("retries")); } }