1use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9
10use tracing::{debug, error, info, trace, warn};
11use zentinel_agent_protocol::v2::{
12 AgentCapabilities, AgentPool, AgentPoolConfig as ProtocolPoolConfig, AgentPoolStats,
13 CancelReason, ConfigPusher, ConfigUpdateType, LoadBalanceStrategy as ProtocolLBStrategy,
14 MetricsCollector,
15};
16use zentinel_agent_protocol::{
17 AgentResponse, EventType, GuardrailInspectEvent, RequestBodyChunkEvent, RequestHeadersEvent,
18 ResponseBodyChunkEvent, ResponseHeadersEvent,
19};
20use zentinel_common::{
21 errors::{ZentinelError, ZentinelResult},
22 CircuitBreaker,
23};
24use zentinel_config::{AgentConfig, AgentEvent, FailureMode, LoadBalanceStrategy};
25
26use super::metrics::AgentMetrics;
27
28const NO_TIMESTAMP: u64 = 0;
30
31pub struct AgentV2 {
33 config: AgentConfig,
35 pool: Arc<AgentPool>,
37 circuit_breaker: Arc<CircuitBreaker>,
39 metrics: Arc<AgentMetrics>,
41 base_instant: Instant,
43 last_success_ns: AtomicU64,
45 consecutive_failures: AtomicU32,
47 maintenance_handle: std::sync::Mutex<Option<tokio::task::JoinHandle<()>>>,
50}
51
52impl AgentV2 {
53 pub fn new(config: AgentConfig, circuit_breaker: Arc<CircuitBreaker>) -> Self {
55 trace!(
56 agent_id = %config.id,
57 agent_type = ?config.agent_type,
58 timeout_ms = config.timeout_ms,
59 events = ?config.events,
60 "Creating v2 agent instance"
61 );
62
63 let pool_config = config
65 .pool
66 .as_ref()
67 .map(|p| ProtocolPoolConfig {
68 connections_per_agent: p.connections_per_agent,
69 load_balance_strategy: convert_lb_strategy(p.load_balance_strategy),
70 connect_timeout: Duration::from_millis(p.connect_timeout_ms),
71 request_timeout: Duration::from_millis(config.timeout_ms),
72 reconnect_interval: Duration::from_millis(p.reconnect_interval_ms),
73 max_reconnect_attempts: p.max_reconnect_attempts,
74 drain_timeout: Duration::from_millis(p.drain_timeout_ms),
75 max_concurrent_per_connection: p.max_concurrent_per_connection,
76 health_check_interval: Duration::from_millis(p.health_check_interval_ms),
77 ..Default::default()
78 })
79 .unwrap_or_default();
80
81 let pool = Arc::new(AgentPool::with_config(pool_config));
82
83 Self {
84 config,
85 pool,
86 circuit_breaker,
87 metrics: Arc::new(AgentMetrics::default()),
88 base_instant: Instant::now(),
89 last_success_ns: AtomicU64::new(NO_TIMESTAMP),
90 consecutive_failures: AtomicU32::new(0),
91 maintenance_handle: std::sync::Mutex::new(None),
92 }
93 }
94
95 pub fn id(&self) -> &str {
97 &self.config.id
98 }
99
100 pub fn circuit_breaker(&self) -> &CircuitBreaker {
102 &self.circuit_breaker
103 }
104
105 pub fn failure_mode(&self) -> FailureMode {
107 self.config.failure_mode
108 }
109
110 pub fn timeout_ms(&self) -> u64 {
112 self.config.timeout_ms
113 }
114
115 pub fn max_request_body_bytes(&self) -> usize {
117 self.config
118 .max_request_body_bytes
119 .unwrap_or(super::DEFAULT_AGENT_MAX_BODY_BYTES)
120 }
121
122 pub fn max_response_body_bytes(&self) -> usize {
124 self.config
125 .max_response_body_bytes
126 .unwrap_or(super::DEFAULT_AGENT_MAX_BODY_BYTES)
127 }
128
129 pub fn metrics(&self) -> &AgentMetrics {
131 &self.metrics
132 }
133
134 pub fn handles_event(&self, event_type: EventType) -> bool {
136 self.config.events.iter().any(|e| match (e, event_type) {
137 (AgentEvent::RequestHeaders, EventType::RequestHeaders) => true,
138 (AgentEvent::RequestBody, EventType::RequestBodyChunk) => true,
139 (AgentEvent::ResponseHeaders, EventType::ResponseHeaders) => true,
140 (AgentEvent::ResponseBody, EventType::ResponseBodyChunk) => true,
141 (AgentEvent::Log, EventType::RequestComplete) => true,
142 (AgentEvent::WebSocketFrame, EventType::WebSocketFrame) => true,
143 (AgentEvent::Guardrail, EventType::GuardrailInspect) => true,
144 _ => false,
145 })
146 }
147
148 pub async fn initialize(&self) -> ZentinelResult<()> {
150 let endpoint = self.get_endpoint()?;
151
152 debug!(
153 agent_id = %self.config.id,
154 endpoint = %endpoint,
155 "Initializing v2 agent pool"
156 );
157
158 let start = Instant::now();
159
160 self.pool
162 .add_agent(&self.config.id, &endpoint)
163 .await
164 .map_err(|e| {
165 error!(
166 agent_id = %self.config.id,
167 endpoint = %endpoint,
168 error = %e,
169 "Failed to add agent to v2 pool"
170 );
171 ZentinelError::Agent {
172 agent: self.config.id.clone(),
173 message: format!("Failed to initialize v2 agent: {}", e),
174 event: "initialize".to_string(),
175 source: None,
176 }
177 })?;
178
179 info!(
180 agent_id = %self.config.id,
181 endpoint = %endpoint,
182 connect_time_ms = start.elapsed().as_millis(),
183 "V2 agent pool initialized"
184 );
185
186 {
191 let pool = Arc::clone(&self.pool);
192 let handle = tokio::spawn(async move { pool.run_maintenance().await });
193 let mut guard = self
194 .maintenance_handle
195 .lock()
196 .unwrap_or_else(std::sync::PoisonError::into_inner);
197 if let Some(old) = guard.replace(handle) {
198 old.abort();
199 }
200 }
201
202 if let Some(config_value) = &self.config.config {
204 self.send_configure(config_value.clone()).await?;
205 }
206
207 Ok(())
208 }
209
210 pub fn clear_correlation_affinity(&self, correlation_id: &str) {
212 self.pool.clear_correlation_affinity(correlation_id);
213 }
214
215 fn get_endpoint(&self) -> ZentinelResult<String> {
217 use zentinel_config::AgentTransport;
218 match &self.config.transport {
219 AgentTransport::Grpc { address, .. } => Ok(address.clone()),
220 AgentTransport::UnixSocket { path } => {
221 Ok(format!("unix:{}", path.display()))
223 }
224 AgentTransport::Http { url, .. } => {
225 Err(ZentinelError::Agent {
227 agent: self.config.id.clone(),
228 message: "HTTP transport not supported for v2 protocol".to_string(),
229 event: "initialize".to_string(),
230 source: None,
231 })
232 }
233 }
234 }
235
236 async fn send_configure(&self, _config: serde_json::Value) -> ZentinelResult<()> {
238 use zentinel_agent_protocol::v2::ConfigUpdateType;
239
240 if let Some(push_id) = self
241 .pool
242 .push_config_to_agent(&self.config.id, ConfigUpdateType::RequestReload)
243 {
244 info!(
245 agent_id = %self.config.id,
246 push_id = %push_id,
247 "Configuration push sent to agent"
248 );
249 Ok(())
250 } else {
251 debug!(
252 agent_id = %self.config.id,
253 "Agent does not support config push, config will be sent on next connection"
254 );
255 Ok(())
256 }
257 }
258
259 pub async fn call_request_headers(
261 &self,
262 event: &RequestHeadersEvent,
263 ) -> ZentinelResult<AgentResponse> {
264 let call_num = self.metrics.calls_total.fetch_add(1, Ordering::Relaxed) + 1;
265
266 let correlation_id = &event.metadata.correlation_id;
268
269 trace!(
270 agent_id = %self.config.id,
271 call_num = call_num,
272 correlation_id = %correlation_id,
273 "Sending request headers to v2 agent"
274 );
275
276 self.pool
277 .send_request_headers(&self.config.id, correlation_id, event)
278 .await
279 .map_err(|e| {
280 error!(
281 agent_id = %self.config.id,
282 correlation_id = %correlation_id,
283 error = %e,
284 "V2 agent request headers call failed"
285 );
286 ZentinelError::Agent {
287 agent: self.config.id.clone(),
288 message: e.to_string(),
289 event: "request_headers".to_string(),
290 source: None,
291 }
292 })
293 }
294
295 pub async fn call_request_body_chunk(
300 &self,
301 event: &RequestBodyChunkEvent,
302 ) -> ZentinelResult<AgentResponse> {
303 let correlation_id = &event.correlation_id;
304
305 trace!(
306 agent_id = %self.config.id,
307 correlation_id = %correlation_id,
308 chunk_index = event.chunk_index,
309 is_last = event.is_last,
310 "Sending request body chunk to v2 agent"
311 );
312
313 self.pool
314 .send_request_body_chunk(&self.config.id, correlation_id, event)
315 .await
316 .map_err(|e| {
317 error!(
318 agent_id = %self.config.id,
319 correlation_id = %correlation_id,
320 error = %e,
321 "V2 agent request body chunk call failed"
322 );
323 ZentinelError::Agent {
324 agent: self.config.id.clone(),
325 message: e.to_string(),
326 event: "request_body_chunk".to_string(),
327 source: None,
328 }
329 })
330 }
331
332 pub async fn call_response_headers(
337 &self,
338 event: &ResponseHeadersEvent,
339 ) -> ZentinelResult<AgentResponse> {
340 let correlation_id = &event.correlation_id;
341
342 trace!(
343 agent_id = %self.config.id,
344 correlation_id = %correlation_id,
345 status = event.status,
346 "Sending response headers to v2 agent"
347 );
348
349 self.pool
350 .send_response_headers(&self.config.id, correlation_id, event)
351 .await
352 .map_err(|e| {
353 error!(
354 agent_id = %self.config.id,
355 correlation_id = %correlation_id,
356 error = %e,
357 "V2 agent response headers call failed"
358 );
359 ZentinelError::Agent {
360 agent: self.config.id.clone(),
361 message: e.to_string(),
362 event: "response_headers".to_string(),
363 source: None,
364 }
365 })
366 }
367
368 pub async fn call_response_body_chunk(
373 &self,
374 event: &ResponseBodyChunkEvent,
375 ) -> ZentinelResult<AgentResponse> {
376 let correlation_id = &event.correlation_id;
377
378 trace!(
379 agent_id = %self.config.id,
380 correlation_id = %correlation_id,
381 chunk_index = event.chunk_index,
382 is_last = event.is_last,
383 "Sending response body chunk to v2 agent"
384 );
385
386 self.pool
387 .send_response_body_chunk(&self.config.id, correlation_id, event)
388 .await
389 .map_err(|e| {
390 error!(
391 agent_id = %self.config.id,
392 correlation_id = %correlation_id,
393 error = %e,
394 "V2 agent response body chunk call failed"
395 );
396 ZentinelError::Agent {
397 agent: self.config.id.clone(),
398 message: e.to_string(),
399 event: "response_body_chunk".to_string(),
400 source: None,
401 }
402 })
403 }
404
405 pub async fn call_guardrail_inspect(
407 &self,
408 event: &GuardrailInspectEvent,
409 ) -> ZentinelResult<AgentResponse> {
410 let call_num = self.metrics.calls_total.fetch_add(1, Ordering::Relaxed) + 1;
411
412 let correlation_id = &event.correlation_id;
413
414 trace!(
415 agent_id = %self.config.id,
416 call_num = call_num,
417 correlation_id = %correlation_id,
418 inspection_type = ?event.inspection_type,
419 "Sending guardrail inspect to v2 agent"
420 );
421
422 self.pool
423 .send_guardrail_inspect(&self.config.id, correlation_id, event)
424 .await
425 .map_err(|e| {
426 error!(
427 agent_id = %self.config.id,
428 correlation_id = %correlation_id,
429 error = %e,
430 "V2 agent guardrail inspect call failed"
431 );
432 ZentinelError::Agent {
433 agent: self.config.id.clone(),
434 message: e.to_string(),
435 event: "guardrail_inspect".to_string(),
436 source: None,
437 }
438 })
439 }
440
441 pub async fn call_event<T: serde::Serialize>(
446 &self,
447 event_type: EventType,
448 event: &T,
449 ) -> ZentinelResult<AgentResponse> {
450 let json = serde_json::to_value(event).map_err(|e| ZentinelError::Agent {
451 agent: self.config.id.clone(),
452 message: format!("Failed to serialize event: {}", e),
453 event: format!("{:?}", event_type),
454 source: None,
455 })?;
456
457 match event_type {
458 EventType::RequestHeaders => {
459 let typed: RequestHeadersEvent =
460 serde_json::from_value(json).map_err(|e| ZentinelError::Agent {
461 agent: self.config.id.clone(),
462 message: format!("Failed to deserialize RequestHeadersEvent: {}", e),
463 event: format!("{:?}", event_type),
464 source: None,
465 })?;
466 self.call_request_headers(&typed).await
467 }
468 EventType::RequestBodyChunk => {
469 let typed: RequestBodyChunkEvent =
470 serde_json::from_value(json).map_err(|e| ZentinelError::Agent {
471 agent: self.config.id.clone(),
472 message: format!("Failed to deserialize RequestBodyChunkEvent: {}", e),
473 event: format!("{:?}", event_type),
474 source: None,
475 })?;
476 self.call_request_body_chunk(&typed).await
477 }
478 EventType::ResponseHeaders => {
479 let typed: ResponseHeadersEvent =
480 serde_json::from_value(json).map_err(|e| ZentinelError::Agent {
481 agent: self.config.id.clone(),
482 message: format!("Failed to deserialize ResponseHeadersEvent: {}", e),
483 event: format!("{:?}", event_type),
484 source: None,
485 })?;
486 self.call_response_headers(&typed).await
487 }
488 EventType::ResponseBodyChunk => {
489 let typed: ResponseBodyChunkEvent =
490 serde_json::from_value(json).map_err(|e| ZentinelError::Agent {
491 agent: self.config.id.clone(),
492 message: format!("Failed to deserialize ResponseBodyChunkEvent: {}", e),
493 event: format!("{:?}", event_type),
494 source: None,
495 })?;
496 self.call_response_body_chunk(&typed).await
497 }
498 EventType::GuardrailInspect => {
499 let typed: GuardrailInspectEvent =
500 serde_json::from_value(json).map_err(|e| ZentinelError::Agent {
501 agent: self.config.id.clone(),
502 message: format!("Failed to deserialize GuardrailInspectEvent: {}", e),
503 event: format!("{:?}", event_type),
504 source: None,
505 })?;
506 self.call_guardrail_inspect(&typed).await
507 }
508 _ => Err(ZentinelError::Agent {
509 agent: self.config.id.clone(),
510 message: format!("Unsupported event type {:?}", event_type),
511 event: format!("{:?}", event_type),
512 source: None,
513 }),
514 }
515 }
516
517 pub async fn cancel_request(
519 &self,
520 correlation_id: &str,
521 reason: CancelReason,
522 ) -> ZentinelResult<()> {
523 trace!(
524 agent_id = %self.config.id,
525 correlation_id = %correlation_id,
526 reason = ?reason,
527 "Cancelling request on v2 agent"
528 );
529
530 self.pool
531 .cancel_request(&self.config.id, correlation_id, reason)
532 .await
533 .map_err(|e| {
534 warn!(
535 agent_id = %self.config.id,
536 correlation_id = %correlation_id,
537 error = %e,
538 "Failed to cancel request on v2 agent"
539 );
540 ZentinelError::Agent {
541 agent: self.config.id.clone(),
542 message: format!("Cancel failed: {}", e),
543 event: "cancel".to_string(),
544 source: None,
545 }
546 })
547 }
548
549 pub async fn capabilities(&self) -> Option<AgentCapabilities> {
551 self.pool.agent_capabilities(&self.config.id).await
552 }
553
554 pub async fn is_healthy(&self) -> bool {
556 self.pool.is_agent_healthy(&self.config.id)
557 }
558
559 pub fn record_success(&self, duration: Duration) {
561 let success_count = self.metrics.calls_success.fetch_add(1, Ordering::Relaxed) + 1;
562 self.metrics
563 .duration_total_us
564 .fetch_add(duration.as_micros() as u64, Ordering::Relaxed);
565 self.consecutive_failures.store(0, Ordering::Relaxed);
566 self.last_success_ns.store(
567 self.base_instant.elapsed().as_nanos() as u64,
568 Ordering::Relaxed,
569 );
570
571 trace!(
572 agent_id = %self.config.id,
573 duration_ms = duration.as_millis(),
574 total_successes = success_count,
575 "Recorded v2 agent call success"
576 );
577
578 self.circuit_breaker.record_success();
579 }
580
581 #[inline]
583 pub fn time_since_last_success(&self) -> Option<Duration> {
584 let last_ns = self.last_success_ns.load(Ordering::Relaxed);
585 if last_ns == NO_TIMESTAMP {
586 return None;
587 }
588 let current_ns = self.base_instant.elapsed().as_nanos() as u64;
589 Some(Duration::from_nanos(current_ns.saturating_sub(last_ns)))
590 }
591
592 pub fn record_failure(&self) {
594 let fail_count = self.metrics.calls_failed.fetch_add(1, Ordering::Relaxed) + 1;
595 let consecutive = self.consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
596
597 debug!(
598 agent_id = %self.config.id,
599 total_failures = fail_count,
600 consecutive_failures = consecutive,
601 "Recorded v2 agent call failure"
602 );
603
604 self.circuit_breaker.record_failure();
605 }
606
607 pub fn record_timeout(&self) {
609 let timeout_count = self.metrics.calls_timeout.fetch_add(1, Ordering::Relaxed) + 1;
610 let consecutive = self.consecutive_failures.fetch_add(1, Ordering::Relaxed) + 1;
611
612 debug!(
613 agent_id = %self.config.id,
614 total_timeouts = timeout_count,
615 consecutive_failures = consecutive,
616 timeout_ms = self.config.timeout_ms,
617 "Recorded v2 agent call timeout"
618 );
619
620 self.circuit_breaker.record_failure();
621 }
622
623 pub async fn pool_stats(&self) -> Option<AgentPoolStats> {
625 self.pool.agent_stats(&self.config.id).await
626 }
627
628 pub fn pool_metrics_collector(&self) -> &MetricsCollector {
633 self.pool.metrics_collector()
634 }
635
636 pub fn pool_metrics_collector_arc(&self) -> Arc<MetricsCollector> {
640 self.pool.metrics_collector_arc()
641 }
642
643 pub fn export_prometheus(&self) -> String {
648 self.pool.export_prometheus()
649 }
650
651 pub fn config_pusher(&self) -> &ConfigPusher {
656 self.pool.config_pusher()
657 }
658
659 pub fn push_config(&self, update_type: ConfigUpdateType) -> Option<String> {
663 self.pool.push_config_to_agent(&self.config.id, update_type)
664 }
665
666 pub async fn send_configuration(&self, config: serde_json::Value) -> ZentinelResult<()> {
670 if let Some(push_id) = self.push_config(ConfigUpdateType::RequestReload) {
674 debug!(
675 agent_id = %self.config.id,
676 push_id = %push_id,
677 "Configuration push initiated"
678 );
679 Ok(())
680 } else {
681 warn!(
682 agent_id = %self.config.id,
683 "Agent does not support config push"
684 );
685 Err(ZentinelError::Agent {
686 agent: self.config.id.clone(),
687 message: "Agent does not support config push".to_string(),
688 event: "send_configuration".to_string(),
689 source: None,
690 })
691 }
692 }
693
694 pub async fn shutdown(&self) {
698 debug!(
699 agent_id = %self.config.id,
700 "Shutting down v2 agent"
701 );
702
703 if let Some(handle) = self
705 .maintenance_handle
706 .lock()
707 .unwrap_or_else(std::sync::PoisonError::into_inner)
708 .take()
709 {
710 handle.abort();
711 }
712
713 if let Err(e) = self.pool.remove_agent(&self.config.id).await {
715 warn!(
716 agent_id = %self.config.id,
717 error = %e,
718 "Error removing agent from pool during shutdown"
719 );
720 }
721
722 let stats = (
723 self.metrics.calls_total.load(Ordering::Relaxed),
724 self.metrics.calls_success.load(Ordering::Relaxed),
725 self.metrics.calls_failed.load(Ordering::Relaxed),
726 self.metrics.calls_timeout.load(Ordering::Relaxed),
727 );
728
729 info!(
730 agent_id = %self.config.id,
731 total_calls = stats.0,
732 successes = stats.1,
733 failures = stats.2,
734 timeouts = stats.3,
735 "V2 agent shutdown complete"
736 );
737 }
738}
739
740fn convert_lb_strategy(strategy: LoadBalanceStrategy) -> ProtocolLBStrategy {
742 match strategy {
743 LoadBalanceStrategy::RoundRobin => ProtocolLBStrategy::RoundRobin,
744 LoadBalanceStrategy::LeastConnections => ProtocolLBStrategy::LeastConnections,
745 LoadBalanceStrategy::HealthBased => ProtocolLBStrategy::HealthBased,
746 LoadBalanceStrategy::Random => ProtocolLBStrategy::Random,
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753
754 #[test]
755 fn test_convert_lb_strategy() {
756 assert_eq!(
757 convert_lb_strategy(LoadBalanceStrategy::RoundRobin),
758 ProtocolLBStrategy::RoundRobin
759 );
760 assert_eq!(
761 convert_lb_strategy(LoadBalanceStrategy::LeastConnections),
762 ProtocolLBStrategy::LeastConnections
763 );
764 assert_eq!(
765 convert_lb_strategy(LoadBalanceStrategy::HealthBased),
766 ProtocolLBStrategy::HealthBased
767 );
768 assert_eq!(
769 convert_lb_strategy(LoadBalanceStrategy::Random),
770 ProtocolLBStrategy::Random
771 );
772 }
773}