214 lines
6.4 KiB
Rust
214 lines
6.4 KiB
Rust
//! 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<serde_json::Value>,
|
||
|
|
pub error: Option<String>,
|
||
|
|
pub metadata: HashMap<String, String>,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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::<f32>() * 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"));
|
||
|
|
}
|
||
|
|
}
|