1use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6
7use base64::{engine::general_purpose::STANDARD, Engine as _};
8use futures::future::join_all;
9use pingora_timeout::timeout;
10use tokio::sync::{RwLock, Semaphore};
11use tracing::{debug, error, info, trace, warn};
12use zentinel_agent_protocol::{
13 v2::MetricsCollector, AgentResponse, EventType, GuardrailInspectEvent, RequestBodyChunkEvent,
14 RequestHeadersEvent, ResponseBodyChunkEvent, ResponseHeadersEvent, WebSocketFrameEvent,
15};
16use zentinel_common::{
17 errors::{ZentinelError, ZentinelResult},
18 types::CircuitBreakerConfig,
19 CircuitBreaker,
20};
21use zentinel_config::{AgentConfig, FailureMode};
22
23use super::agent_v2::AgentV2;
24use super::context::AgentCallContext;
25use super::decision::AgentDecision;
26use super::metrics::AgentMetrics;
27
28pub struct AgentManager {
33 agents: Arc<RwLock<HashMap<String, Arc<AgentV2>>>>,
35 circuit_breakers: Arc<RwLock<HashMap<String, Arc<CircuitBreaker>>>>,
37 metrics: Arc<AgentMetrics>,
39 agent_semaphores: Arc<RwLock<HashMap<String, Arc<Semaphore>>>>,
41}
42
43impl AgentManager {
44 pub async fn new(agents: Vec<AgentConfig>) -> ZentinelResult<Self> {
50 info!(agent_count = agents.len(), "Creating agent manager");
51
52 let mut agent_map = HashMap::new();
53 let breakers = HashMap::new();
54 let mut semaphores = HashMap::new();
55
56 for config in agents {
57 debug!(
58 agent_id = %config.id,
59 transport = ?config.transport,
60 timeout_ms = config.timeout_ms,
61 failure_mode = ?config.failure_mode,
62 max_concurrent_calls = config.max_concurrent_calls,
63 "Configuring agent"
64 );
65
66 let semaphore = Arc::new(Semaphore::new(config.max_concurrent_calls));
68
69 let circuit_breaker = Arc::new(CircuitBreaker::new(
70 config.circuit_breaker.unwrap_or_default(),
71 ));
72
73 trace!(
74 agent_id = %config.id,
75 max_concurrent_calls = config.max_concurrent_calls,
76 pool_config = ?config.pool,
77 "Creating agent instance with internal pool"
78 );
79
80 let agent = Arc::new(AgentV2::new(config.clone(), circuit_breaker));
81
82 agent_map.insert(config.id.clone(), agent);
83 semaphores.insert(config.id.clone(), semaphore);
84
85 debug!(
86 agent_id = %config.id,
87 "Agent configured successfully"
88 );
89 }
90
91 info!(
92 configured_agents = agent_map.len(),
93 "Agent manager created successfully with per-agent queue isolation"
94 );
95
96 Ok(Self {
97 agents: Arc::new(RwLock::new(agent_map)),
98 circuit_breakers: Arc::new(RwLock::new(breakers)),
99 metrics: Arc::new(AgentMetrics::default()),
100 agent_semaphores: Arc::new(RwLock::new(semaphores)),
101 })
102 }
103
104 pub async fn any_agent_handles_event(
106 &self,
107 route_agents: &[String],
108 event_type: EventType,
109 ) -> bool {
110 let agents = self.agents.read().await;
111 route_agents
112 .iter()
113 .filter_map(|id| agents.get(id))
114 .any(|agent| agent.handles_event(event_type))
115 }
116
117 pub async fn process_request_headers(
124 &self,
125 ctx: &AgentCallContext,
126 mut headers: HashMap<String, Vec<String>>,
127 route_agents: &[(String, FailureMode)],
128 ) -> ZentinelResult<AgentDecision> {
129 let method = headers
130 .remove(":method")
131 .and_then(|mut v| {
132 if v.is_empty() {
133 None
134 } else {
135 Some(v.swap_remove(0))
136 }
137 })
138 .unwrap_or_else(|| "GET".to_string());
139 let uri = headers
140 .remove(":path")
141 .and_then(|mut v| {
142 if v.is_empty() {
143 None
144 } else {
145 Some(v.swap_remove(0))
146 }
147 })
148 .unwrap_or_else(|| "/".to_string());
149 let event = RequestHeadersEvent {
150 metadata: ctx.metadata.clone(),
151 method,
152 uri,
153 headers,
154 };
155
156 self.process_event_parallel(EventType::RequestHeaders, &event, route_agents, ctx)
158 .await
159 }
160
161 pub async fn process_request_body(
163 &self,
164 ctx: &AgentCallContext,
165 data: &[u8],
166 is_last: bool,
167 route_agents: &[String],
168 ) -> ZentinelResult<AgentDecision> {
169 let inspecting_agents = match self
171 .apply_body_limits(ctx, route_agents, data.len(), EventType::RequestBodyChunk)
172 .await
173 {
174 BodyLimitsResult::Block(decision) => return Ok(*decision),
175 BodyLimitsResult::Proceed(agents) => agents,
176 };
177
178 let event = RequestBodyChunkEvent {
179 correlation_id: ctx.correlation_id.to_string(),
180 data: STANDARD.encode(data),
181 is_last,
182 total_size: ctx.request_body.as_ref().map(|b| b.len()),
183 chunk_index: 0, bytes_received: data.len(),
185 };
186
187 self.process_event(EventType::RequestBodyChunk, &event, &inspecting_agents, ctx)
188 .await
189 }
190
191 pub async fn process_request_body_streaming(
196 &self,
197 ctx: &AgentCallContext,
198 data: &[u8],
199 is_last: bool,
200 chunk_index: u32,
201 bytes_received: usize,
202 total_size: Option<usize>,
203 route_agents: &[String],
204 ) -> ZentinelResult<AgentDecision> {
205 trace!(
206 correlation_id = %ctx.correlation_id,
207 chunk_index = chunk_index,
208 chunk_size = data.len(),
209 bytes_received = bytes_received,
210 is_last = is_last,
211 "Processing streaming request body chunk"
212 );
213
214 let inspecting_agents = match self
216 .apply_body_limits(
217 ctx,
218 route_agents,
219 bytes_received,
220 EventType::RequestBodyChunk,
221 )
222 .await
223 {
224 BodyLimitsResult::Block(decision) => return Ok(*decision),
225 BodyLimitsResult::Proceed(agents) => agents,
226 };
227
228 let event = RequestBodyChunkEvent {
229 correlation_id: ctx.correlation_id.to_string(),
230 data: STANDARD.encode(data),
231 is_last,
232 total_size,
233 chunk_index,
234 bytes_received,
235 };
236
237 self.process_event(EventType::RequestBodyChunk, &event, &inspecting_agents, ctx)
238 .await
239 }
240
241 pub async fn process_response_body_streaming(
243 &self,
244 ctx: &AgentCallContext,
245 data: &[u8],
246 is_last: bool,
247 chunk_index: u32,
248 bytes_sent: usize,
249 total_size: Option<usize>,
250 route_agents: &[String],
251 ) -> ZentinelResult<AgentDecision> {
252 trace!(
253 correlation_id = %ctx.correlation_id,
254 chunk_index = chunk_index,
255 chunk_size = data.len(),
256 bytes_sent = bytes_sent,
257 is_last = is_last,
258 "Processing streaming response body chunk"
259 );
260
261 let inspecting_agents = match self
263 .apply_body_limits(ctx, route_agents, bytes_sent, EventType::ResponseBodyChunk)
264 .await
265 {
266 BodyLimitsResult::Block(decision) => return Ok(*decision),
267 BodyLimitsResult::Proceed(agents) => agents,
268 };
269
270 let event = ResponseBodyChunkEvent {
271 correlation_id: ctx.correlation_id.to_string(),
272 data: STANDARD.encode(data),
273 is_last,
274 total_size,
275 chunk_index,
276 bytes_sent,
277 };
278
279 self.process_event(
280 EventType::ResponseBodyChunk,
281 &event,
282 &inspecting_agents,
283 ctx,
284 )
285 .await
286 }
287
288 pub async fn process_response_headers(
290 &self,
291 ctx: &AgentCallContext,
292 status: u16,
293 headers: &HashMap<String, Vec<String>>,
294 route_agents: &[String],
295 ) -> ZentinelResult<AgentDecision> {
296 let event = ResponseHeadersEvent {
297 correlation_id: ctx.correlation_id.to_string(),
298 status,
299 headers: headers.clone(),
300 };
301
302 self.process_event(EventType::ResponseHeaders, &event, route_agents, ctx)
303 .await
304 }
305
306 pub async fn process_websocket_frame(
312 &self,
313 route_id: &str,
314 event: WebSocketFrameEvent,
315 ) -> ZentinelResult<AgentResponse> {
316 trace!(
317 correlation_id = %event.correlation_id,
318 route_id = %route_id,
319 frame_index = event.frame_index,
320 opcode = %event.opcode,
321 "Processing WebSocket frame through agents"
322 );
323
324 let agents = self.agents.read().await;
326 let relevant_agents: Vec<_> = agents
327 .values()
328 .filter(|agent| agent.handles_event(EventType::WebSocketFrame))
329 .collect();
330
331 if relevant_agents.is_empty() {
332 trace!(
333 correlation_id = %event.correlation_id,
334 "No agents handle WebSocket frames, allowing"
335 );
336 return Ok(AgentResponse::websocket_allow());
337 }
338
339 debug!(
340 correlation_id = %event.correlation_id,
341 route_id = %route_id,
342 agent_count = relevant_agents.len(),
343 "Processing WebSocket frame through agents"
344 );
345
346 for agent in relevant_agents {
348 if !agent.circuit_breaker().is_closed() {
350 warn!(
351 agent_id = %agent.id(),
352 correlation_id = %event.correlation_id,
353 failure_mode = ?agent.failure_mode(),
354 "Circuit breaker open, skipping agent for WebSocket frame"
355 );
356
357 if agent.failure_mode() == FailureMode::Closed {
358 debug!(
359 correlation_id = %event.correlation_id,
360 agent_id = %agent.id(),
361 "Closing WebSocket due to circuit breaker (fail-closed mode)"
362 );
363 return Ok(AgentResponse::websocket_close(
364 1011,
365 "Service unavailable".to_string(),
366 ));
367 }
368 continue;
369 }
370
371 let start = Instant::now();
373 let timeout_duration = Duration::from_millis(agent.timeout_ms());
374
375 match timeout(
376 timeout_duration,
377 agent.call_event(EventType::WebSocketFrame, &event),
378 )
379 .await
380 {
381 Ok(Ok(response)) => {
382 let duration = start.elapsed();
383 agent.record_success(duration);
384
385 trace!(
386 correlation_id = %event.correlation_id,
387 agent_id = %agent.id(),
388 duration_ms = duration.as_millis(),
389 "WebSocket frame agent call succeeded"
390 );
391
392 if let Some(ref ws_decision) = response.websocket_decision {
394 if !matches!(
395 ws_decision,
396 zentinel_agent_protocol::WebSocketDecision::Allow
397 ) {
398 debug!(
399 correlation_id = %event.correlation_id,
400 agent_id = %agent.id(),
401 decision = ?ws_decision,
402 "Agent returned non-allow WebSocket decision"
403 );
404 return Ok(response);
405 }
406 }
407 }
408 Ok(Err(e)) => {
409 agent.record_failure();
410 error!(
411 agent_id = %agent.id(),
412 correlation_id = %event.correlation_id,
413 error = %e,
414 duration_ms = start.elapsed().as_millis(),
415 failure_mode = ?agent.failure_mode(),
416 "WebSocket frame agent call failed"
417 );
418
419 if agent.failure_mode() == FailureMode::Closed {
420 return Ok(AgentResponse::websocket_close(
421 1011,
422 "Agent error".to_string(),
423 ));
424 }
425 }
426 Err(_) => {
427 agent.record_timeout();
428 warn!(
429 agent_id = %agent.id(),
430 correlation_id = %event.correlation_id,
431 timeout_ms = agent.timeout_ms(),
432 failure_mode = ?agent.failure_mode(),
433 "WebSocket frame agent call timed out"
434 );
435
436 if agent.failure_mode() == FailureMode::Closed {
437 return Ok(AgentResponse::websocket_close(
438 1011,
439 "Gateway timeout".to_string(),
440 ));
441 }
442 }
443 }
444 }
445
446 Ok(AgentResponse::websocket_allow())
448 }
449
450 async fn process_event<T: serde::Serialize>(
452 &self,
453 event_type: EventType,
454 event: &T,
455 route_agents: &[String],
456 ctx: &AgentCallContext,
457 ) -> ZentinelResult<AgentDecision> {
458 trace!(
459 correlation_id = %ctx.correlation_id,
460 event_type = ?event_type,
461 route_agents = ?route_agents,
462 "Starting agent event processing"
463 );
464
465 let agents = self.agents.read().await;
467 let relevant_agents: Vec<_> = route_agents
468 .iter()
469 .filter_map(|id| agents.get(id))
470 .filter(|agent| agent.handles_event(event_type))
471 .collect();
472
473 if relevant_agents.is_empty() {
474 trace!(
475 correlation_id = %ctx.correlation_id,
476 event_type = ?event_type,
477 "No relevant agents for event, allowing request"
478 );
479 return Ok(AgentDecision::default_allow());
480 }
481
482 debug!(
483 correlation_id = %ctx.correlation_id,
484 event_type = ?event_type,
485 agent_count = relevant_agents.len(),
486 agent_ids = ?relevant_agents.iter().map(|a| a.id()).collect::<Vec<_>>(),
487 "Processing event through agents"
488 );
489
490 let mut combined_decision = AgentDecision::default_allow();
492
493 for (agent_index, agent) in relevant_agents.iter().enumerate() {
494 trace!(
495 correlation_id = %ctx.correlation_id,
496 agent_id = %agent.id(),
497 agent_index = agent_index,
498 event_type = ?event_type,
499 "Processing event through agent"
500 );
501
502 let semaphores = self.agent_semaphores.read().await;
504 let agent_semaphore = semaphores.get(agent.id()).cloned();
505 drop(semaphores); let _permit = match agent_semaphore {
508 Some(semaphore) => {
509 trace!(
510 correlation_id = %ctx.correlation_id,
511 agent_id = %agent.id(),
512 "Acquiring per-agent semaphore permit"
513 );
514 Some(semaphore.acquire_owned().await.map_err(|_| {
515 error!(
516 correlation_id = %ctx.correlation_id,
517 agent_id = %agent.id(),
518 "Failed to acquire agent call semaphore permit"
519 );
520 ZentinelError::Internal {
521 message: "Failed to acquire agent call permit".to_string(),
522 correlation_id: Some(ctx.correlation_id.to_string()),
523 source: None,
524 }
525 })?)
526 }
527 None => {
528 warn!(
530 correlation_id = %ctx.correlation_id,
531 agent_id = %agent.id(),
532 "No semaphore found for agent, proceeding without queue isolation"
533 );
534 None
535 }
536 };
537
538 if !agent.circuit_breaker().is_closed() {
540 warn!(
541 agent_id = %agent.id(),
542 correlation_id = %ctx.correlation_id,
543 failure_mode = ?agent.failure_mode(),
544 "Circuit breaker open, skipping agent"
545 );
546
547 if agent.failure_mode() == FailureMode::Closed {
549 debug!(
550 correlation_id = %ctx.correlation_id,
551 agent_id = %agent.id(),
552 "Blocking request due to circuit breaker (fail-closed mode)"
553 );
554 return Ok(AgentDecision::block(503, "Service unavailable")
555 .with_decided_by(agent.id()));
556 }
557 continue;
558 }
559
560 let start = Instant::now();
562 let timeout_duration = Duration::from_millis(agent.timeout_ms());
563
564 trace!(
565 correlation_id = %ctx.correlation_id,
566 agent_id = %agent.id(),
567 timeout_ms = agent.timeout_ms(),
568 "Calling agent"
569 );
570
571 match timeout(timeout_duration, agent.call_event(event_type, event)).await {
572 Ok(Ok(response)) => {
573 let duration = start.elapsed();
574 agent.record_success(duration);
575
576 trace!(
577 correlation_id = %ctx.correlation_id,
578 agent_id = %agent.id(),
579 duration_ms = duration.as_millis(),
580 decision = ?response,
581 "Agent call succeeded"
582 );
583
584 combined_decision.merge(AgentDecision::from_response(response, agent.id()));
586
587 if !combined_decision.is_allow() {
589 debug!(
590 correlation_id = %ctx.correlation_id,
591 agent_id = %agent.id(),
592 decision = ?combined_decision,
593 "Agent returned blocking decision, stopping agent chain"
594 );
595 break;
596 }
597 }
598 Ok(Err(e)) => {
599 agent.record_failure();
600 error!(
601 agent_id = %agent.id(),
602 correlation_id = %ctx.correlation_id,
603 error = %e,
604 duration_ms = start.elapsed().as_millis(),
605 failure_mode = ?agent.failure_mode(),
606 "Agent call failed"
607 );
608
609 if agent.failure_mode() == FailureMode::Closed {
610 return Err(e);
611 }
612 }
613 Err(_) => {
614 agent.record_timeout();
615 warn!(
616 agent_id = %agent.id(),
617 correlation_id = %ctx.correlation_id,
618 timeout_ms = agent.timeout_ms(),
619 failure_mode = ?agent.failure_mode(),
620 "Agent call timed out"
621 );
622
623 if agent.failure_mode() == FailureMode::Closed {
624 debug!(
625 correlation_id = %ctx.correlation_id,
626 agent_id = %agent.id(),
627 "Blocking request due to timeout (fail-closed mode)"
628 );
629 return Ok(AgentDecision::block(504, "Gateway timeout")
630 .with_decided_by(agent.id()));
631 }
632 }
633 }
634 }
635
636 trace!(
637 correlation_id = %ctx.correlation_id,
638 decision = ?combined_decision,
639 agents_processed = relevant_agents.len(),
640 "Agent event processing completed"
641 );
642
643 Ok(combined_decision)
644 }
645
646 async fn process_event_with_failure_modes<T: serde::Serialize>(
651 &self,
652 event_type: EventType,
653 event: &T,
654 route_agents: &[(String, FailureMode)],
655 ctx: &AgentCallContext,
656 ) -> ZentinelResult<AgentDecision> {
657 trace!(
658 correlation_id = %ctx.correlation_id,
659 event_type = ?event_type,
660 route_agents = ?route_agents.iter().map(|(id, _)| id).collect::<Vec<_>>(),
661 "Starting agent event processing with failure modes"
662 );
663
664 let agents = self.agents.read().await;
666 let relevant_agents: Vec<_> = route_agents
667 .iter()
668 .filter_map(|(id, failure_mode)| agents.get(id).map(|agent| (agent, *failure_mode)))
669 .filter(|(agent, _)| agent.handles_event(event_type))
670 .collect();
671
672 if relevant_agents.is_empty() {
673 trace!(
674 correlation_id = %ctx.correlation_id,
675 event_type = ?event_type,
676 "No relevant agents for event, allowing request"
677 );
678 return Ok(AgentDecision::default_allow());
679 }
680
681 debug!(
682 correlation_id = %ctx.correlation_id,
683 event_type = ?event_type,
684 agent_count = relevant_agents.len(),
685 agent_ids = ?relevant_agents.iter().map(|(a, _)| a.id()).collect::<Vec<_>>(),
686 "Processing event through agents"
687 );
688
689 let mut combined_decision = AgentDecision::default_allow();
691
692 for (agent_index, (agent, filter_failure_mode)) in relevant_agents.iter().enumerate() {
693 trace!(
694 correlation_id = %ctx.correlation_id,
695 agent_id = %agent.id(),
696 agent_index = agent_index,
697 event_type = ?event_type,
698 filter_failure_mode = ?filter_failure_mode,
699 "Processing event through agent with filter failure mode"
700 );
701
702 let semaphores = self.agent_semaphores.read().await;
704 let agent_semaphore = semaphores.get(agent.id()).cloned();
705 drop(semaphores); let _permit = if let Some(semaphore) = agent_semaphore {
708 trace!(
709 correlation_id = %ctx.correlation_id,
710 agent_id = %agent.id(),
711 "Acquiring per-agent semaphore permit"
712 );
713 Some(semaphore.acquire_owned().await.map_err(|_| {
714 error!(
715 correlation_id = %ctx.correlation_id,
716 agent_id = %agent.id(),
717 "Failed to acquire agent call semaphore permit"
718 );
719 ZentinelError::Internal {
720 message: "Failed to acquire agent call permit".to_string(),
721 correlation_id: Some(ctx.correlation_id.to_string()),
722 source: None,
723 }
724 })?)
725 } else {
726 warn!(
728 correlation_id = %ctx.correlation_id,
729 agent_id = %agent.id(),
730 "No semaphore found for agent, proceeding without queue isolation"
731 );
732 None
733 };
734
735 if !agent.circuit_breaker().is_closed() {
737 warn!(
738 agent_id = %agent.id(),
739 correlation_id = %ctx.correlation_id,
740 filter_failure_mode = ?filter_failure_mode,
741 "Circuit breaker open, skipping agent"
742 );
743
744 if *filter_failure_mode == FailureMode::Closed {
746 debug!(
747 correlation_id = %ctx.correlation_id,
748 agent_id = %agent.id(),
749 "Blocking request due to circuit breaker (filter fail-closed mode)"
750 );
751 return Ok(AgentDecision::block(503, "Service unavailable")
752 .with_decided_by(agent.id()));
753 }
754 continue;
756 }
757
758 let start = Instant::now();
760 let timeout_duration = Duration::from_millis(agent.timeout_ms());
761
762 trace!(
763 correlation_id = %ctx.correlation_id,
764 agent_id = %agent.id(),
765 timeout_ms = agent.timeout_ms(),
766 "Calling agent"
767 );
768
769 match timeout(timeout_duration, agent.call_event(event_type, event)).await {
770 Ok(Ok(response)) => {
771 let duration = start.elapsed();
772 agent.record_success(duration);
773
774 trace!(
775 correlation_id = %ctx.correlation_id,
776 agent_id = %agent.id(),
777 duration_ms = duration.as_millis(),
778 decision = ?response,
779 "Agent call succeeded"
780 );
781
782 combined_decision.merge(AgentDecision::from_response(response, agent.id()));
784
785 if !combined_decision.is_allow() {
787 debug!(
788 correlation_id = %ctx.correlation_id,
789 agent_id = %agent.id(),
790 decision = ?combined_decision,
791 "Agent returned blocking decision, stopping agent chain"
792 );
793 break;
794 }
795 }
796 Ok(Err(e)) => {
797 agent.record_failure();
798 error!(
799 agent_id = %agent.id(),
800 correlation_id = %ctx.correlation_id,
801 error = %e,
802 duration_ms = start.elapsed().as_millis(),
803 filter_failure_mode = ?filter_failure_mode,
804 "Agent call failed"
805 );
806
807 if *filter_failure_mode == FailureMode::Closed {
809 debug!(
810 correlation_id = %ctx.correlation_id,
811 agent_id = %agent.id(),
812 "Blocking request due to agent failure (filter fail-closed mode)"
813 );
814 return Ok(AgentDecision::block(503, "Agent unavailable")
815 .with_decided_by(agent.id()));
816 }
817 debug!(
819 correlation_id = %ctx.correlation_id,
820 agent_id = %agent.id(),
821 "Continuing despite agent failure (filter fail-open mode)"
822 );
823 }
824 Err(_) => {
825 agent.record_timeout();
826 warn!(
827 agent_id = %agent.id(),
828 correlation_id = %ctx.correlation_id,
829 timeout_ms = agent.timeout_ms(),
830 filter_failure_mode = ?filter_failure_mode,
831 "Agent call timed out"
832 );
833
834 if *filter_failure_mode == FailureMode::Closed {
836 debug!(
837 correlation_id = %ctx.correlation_id,
838 agent_id = %agent.id(),
839 "Blocking request due to timeout (filter fail-closed mode)"
840 );
841 return Ok(AgentDecision::block(504, "Gateway timeout")
842 .with_decided_by(agent.id()));
843 }
844 debug!(
846 correlation_id = %ctx.correlation_id,
847 agent_id = %agent.id(),
848 "Continuing despite timeout (filter fail-open mode)"
849 );
850 }
851 }
852 }
853
854 trace!(
855 correlation_id = %ctx.correlation_id,
856 decision = ?combined_decision,
857 agents_processed = relevant_agents.len(),
858 "Agent event processing with failure modes completed"
859 );
860
861 Ok(combined_decision)
862 }
863
864 async fn process_event_parallel<T: serde::Serialize + Sync>(
879 &self,
880 event_type: EventType,
881 event: &T,
882 route_agents: &[(String, FailureMode)],
883 ctx: &AgentCallContext,
884 ) -> ZentinelResult<AgentDecision> {
885 trace!(
886 correlation_id = %ctx.correlation_id,
887 event_type = ?event_type,
888 route_agents = ?route_agents.iter().map(|(id, _)| id).collect::<Vec<_>>(),
889 "Starting parallel agent event processing"
890 );
891
892 let agents = self.agents.read().await;
894 let semaphores = self.agent_semaphores.read().await;
895
896 let agent_info: Vec<_> = route_agents
898 .iter()
899 .filter_map(|(id, failure_mode)| {
900 let agent = agents.get(id)?;
901 if !agent.handles_event(event_type) {
902 return None;
903 }
904 let semaphore = semaphores.get(id).cloned();
905 Some((Arc::clone(agent), *failure_mode, semaphore))
906 })
907 .collect();
908
909 drop(agents);
911 drop(semaphores);
912
913 if agent_info.is_empty() {
914 trace!(
915 correlation_id = %ctx.correlation_id,
916 event_type = ?event_type,
917 "No relevant agents for event, allowing request"
918 );
919 return Ok(AgentDecision::default_allow());
920 }
921
922 debug!(
923 correlation_id = %ctx.correlation_id,
924 event_type = ?event_type,
925 agent_count = agent_info.len(),
926 agent_ids = ?agent_info.iter().map(|(a, _, _)| a.id()).collect::<Vec<_>>(),
927 "Processing event through agents in parallel"
928 );
929
930 let futures: Vec<_> = agent_info
932 .iter()
933 .map(|(agent, filter_failure_mode, semaphore)| {
934 let agent = Arc::clone(agent);
935 let filter_failure_mode = *filter_failure_mode;
936 let semaphore = semaphore.clone();
937 let correlation_id = ctx.correlation_id.clone();
938
939 async move {
940 let _permit = if let Some(sem) = semaphore {
942 match sem.acquire_owned().await {
943 Ok(permit) => Some(permit),
944 Err(_) => {
945 error!(
946 correlation_id = %correlation_id,
947 agent_id = %agent.id(),
948 "Failed to acquire agent semaphore permit"
949 );
950 return Err((
951 agent.id().to_string(),
952 filter_failure_mode,
953 "Failed to acquire permit".to_string(),
954 ));
955 }
956 }
957 } else {
958 None
959 };
960
961 if !agent.circuit_breaker().is_closed() {
963 warn!(
964 agent_id = %agent.id(),
965 correlation_id = %correlation_id,
966 filter_failure_mode = ?filter_failure_mode,
967 "Circuit breaker open, skipping agent"
968 );
969 return Err((
970 agent.id().to_string(),
971 filter_failure_mode,
972 "Circuit breaker open".to_string(),
973 ));
974 }
975
976 let start = Instant::now();
978 let timeout_duration = Duration::from_millis(agent.timeout_ms());
979
980 match timeout(timeout_duration, agent.call_event(event_type, event)).await {
981 Ok(Ok(response)) => {
982 let duration = start.elapsed();
983 agent.record_success(duration);
984 trace!(
985 correlation_id = %correlation_id,
986 agent_id = %agent.id(),
987 duration_ms = duration.as_millis(),
988 "Parallel agent call succeeded"
989 );
990 Ok((agent.id().to_string(), response))
991 }
992 Ok(Err(e)) => {
993 agent.record_failure();
994 error!(
995 agent_id = %agent.id(),
996 correlation_id = %correlation_id,
997 error = %e,
998 duration_ms = start.elapsed().as_millis(),
999 filter_failure_mode = ?filter_failure_mode,
1000 "Parallel agent call failed"
1001 );
1002 Err((
1003 agent.id().to_string(),
1004 filter_failure_mode,
1005 format!("Agent error: {}", e),
1006 ))
1007 }
1008 Err(_) => {
1009 agent.record_timeout();
1010 warn!(
1011 agent_id = %agent.id(),
1012 correlation_id = %correlation_id,
1013 timeout_ms = agent.timeout_ms(),
1014 filter_failure_mode = ?filter_failure_mode,
1015 "Parallel agent call timed out"
1016 );
1017 Err((
1018 agent.id().to_string(),
1019 filter_failure_mode,
1020 "Timeout".to_string(),
1021 ))
1022 }
1023 }
1024 }
1025 })
1026 .collect();
1027
1028 let results = join_all(futures).await;
1030
1031 let mut combined_decision = AgentDecision::default_allow();
1033 let mut blocking_error: Option<AgentDecision> = None;
1034
1035 for result in results {
1036 match result {
1037 Ok((agent_id, response)) => {
1038 let decision = AgentDecision::from_response(response, &agent_id);
1039
1040 if !decision.is_allow() {
1042 debug!(
1043 correlation_id = %ctx.correlation_id,
1044 agent_id = %agent_id,
1045 decision = ?decision,
1046 "Agent returned blocking decision"
1047 );
1048 return Ok(decision);
1050 }
1051
1052 combined_decision.merge(decision);
1053 }
1054 Err((agent_id, failure_mode, reason)) => {
1055 if failure_mode == FailureMode::Closed && blocking_error.is_none() {
1057 debug!(
1058 correlation_id = %ctx.correlation_id,
1059 agent_id = %agent_id,
1060 reason = %reason,
1061 "Agent failure in fail-closed mode"
1062 );
1063 let status = if reason.contains("Timeout") { 504 } else { 503 };
1066 let message = if reason.contains("Timeout") {
1067 "Gateway timeout"
1068 } else {
1069 "Service unavailable"
1070 };
1071 blocking_error =
1072 Some(AgentDecision::block(status, message).with_decided_by(&agent_id));
1073 } else {
1074 debug!(
1076 correlation_id = %ctx.correlation_id,
1077 agent_id = %agent_id,
1078 reason = %reason,
1079 "Agent failure in fail-open mode, continuing"
1080 );
1081 }
1082 }
1083 }
1084 }
1085
1086 if let Some(error_decision) = blocking_error {
1088 return Ok(error_decision);
1089 }
1090
1091 trace!(
1092 correlation_id = %ctx.correlation_id,
1093 decision = ?combined_decision,
1094 agents_processed = agent_info.len(),
1095 "Parallel agent event processing completed"
1096 );
1097
1098 Ok(combined_decision)
1099 }
1100
1101 pub async fn call_guardrail_agent(
1106 &self,
1107 agent_name: &str,
1108 event: GuardrailInspectEvent,
1109 ) -> ZentinelResult<AgentResponse> {
1110 let agents = self.agents.read().await;
1111 let agent = agents.get(agent_name).ok_or_else(|| ZentinelError::Agent {
1112 agent: agent_name.to_string(),
1113 message: format!("Agent '{}' not found", agent_name),
1114 event: "guardrail_inspect".to_string(),
1115 source: None,
1116 })?;
1117
1118 let agent = Arc::clone(agent);
1119 drop(agents); let semaphores = self.agent_semaphores.read().await;
1123 let semaphore = semaphores.get(agent_name).cloned();
1124 drop(semaphores);
1125
1126 let _permit = if let Some(sem) = semaphore {
1127 Some(
1128 sem.acquire_owned()
1129 .await
1130 .map_err(|_| ZentinelError::Agent {
1131 agent: agent_name.to_string(),
1132 message: "Failed to acquire agent call permit".to_string(),
1133 event: "guardrail_inspect".to_string(),
1134 source: None,
1135 })?,
1136 )
1137 } else {
1138 None
1139 };
1140
1141 if !agent.circuit_breaker().is_closed() {
1143 return Err(ZentinelError::Agent {
1144 agent: agent_name.to_string(),
1145 message: "Circuit breaker open".to_string(),
1146 event: "guardrail_inspect".to_string(),
1147 source: None,
1148 });
1149 }
1150
1151 let start = Instant::now();
1152 let timeout_duration = Duration::from_millis(agent.timeout_ms());
1153
1154 match timeout(timeout_duration, agent.call_guardrail_inspect(&event)).await {
1155 Ok(Ok(response)) => {
1156 agent.record_success(start.elapsed());
1157 Ok(response)
1158 }
1159 Ok(Err(e)) => {
1160 agent.record_failure();
1161 Err(e)
1162 }
1163 Err(_) => {
1164 agent.record_timeout();
1165 Err(ZentinelError::Agent {
1166 agent: agent_name.to_string(),
1167 message: format!(
1168 "Guardrail agent call timed out after {}ms",
1169 timeout_duration.as_millis()
1170 ),
1171 event: "guardrail_inspect".to_string(),
1172 source: None,
1173 })
1174 }
1175 }
1176 }
1177
1178 pub async fn initialize(&self) -> ZentinelResult<()> {
1180 let agents = self.agents.read().await;
1181
1182 info!(agent_count = agents.len(), "Initializing agent connections");
1183
1184 let mut initialized_count = 0;
1185 let mut failed_count = 0;
1186
1187 for (id, agent) in agents.iter() {
1188 debug!(agent_id = %id, "Initializing agent connection");
1189 if let Err(e) = agent.initialize().await {
1190 error!(
1191 agent_id = %id,
1192 error = %e,
1193 "Failed to initialize agent"
1194 );
1195 failed_count += 1;
1196 } else {
1198 trace!(agent_id = %id, "Agent initialized successfully");
1199 initialized_count += 1;
1200 }
1201 }
1202
1203 info!(
1204 initialized = initialized_count,
1205 failed = failed_count,
1206 total = agents.len(),
1207 "Agent initialization complete"
1208 );
1209
1210 Ok(())
1211 }
1212
1213 pub async fn shutdown(&self) {
1215 let agents = self.agents.read().await;
1216
1217 info!(agent_count = agents.len(), "Shutting down agent manager");
1218
1219 for (id, agent) in agents.iter() {
1220 debug!(agent_id = %id, "Shutting down agent");
1221 agent.shutdown().await;
1222 trace!(agent_id = %id, "Agent shutdown complete");
1223 }
1224
1225 info!("Agent manager shutdown complete");
1226 }
1227
1228 pub async fn end_request(&self, correlation_id: &str) {
1234 let agents = self.agents.read().await;
1235 for agent in agents.values() {
1236 agent.clear_correlation_affinity(correlation_id);
1237 }
1238 }
1239
1240 pub fn metrics(&self) -> &AgentMetrics {
1242 &self.metrics
1243 }
1244
1245 pub fn get_agents_for_event(&self, event_type: EventType) -> Vec<String> {
1250 if let Ok(agents) = self.agents.try_read() {
1253 agents
1254 .values()
1255 .filter(|agent| agent.handles_event(event_type))
1256 .map(|agent| agent.id().to_string())
1257 .collect()
1258 } else {
1259 Vec::new()
1260 }
1261 }
1262
1263 pub async fn get_v2_pool_metrics(&self) -> Vec<(String, Arc<MetricsCollector>)> {
1269 let agents = self.agents.read().await;
1270 agents
1271 .iter()
1272 .map(|(id, agent)| (id.clone(), agent.pool_metrics_collector_arc()))
1273 .collect()
1274 }
1275
1276 pub async fn export_v2_pool_metrics(&self) -> String {
1280 let agents = self.agents.read().await;
1281 let mut output = String::new();
1282
1283 for (id, agent) in agents.iter() {
1284 let pool_metrics = agent.export_prometheus();
1285 if !pool_metrics.is_empty() {
1286 output.push_str(&format!("\n# Agent pool metrics: {}\n", id));
1287 output.push_str(&pool_metrics);
1288 }
1289 }
1290
1291 output
1292 }
1293
1294 pub async fn get_v2_metrics_collector(&self, agent_id: &str) -> Option<Arc<MetricsCollector>> {
1298 let agents = self.agents.read().await;
1299 agents
1300 .get(agent_id)
1301 .map(|agent| agent.pool_metrics_collector_arc())
1302 }
1303
1304 async fn apply_body_limits(
1311 &self,
1312 ctx: &AgentCallContext,
1313 route_agents: &[String],
1314 body_size: usize,
1315 event_type: EventType,
1316 ) -> BodyLimitsResult {
1317 let agents = self.agents.read().await;
1318 let limits: Vec<(String, FailureMode, usize)> = route_agents
1319 .iter()
1320 .filter_map(|id| agents.get(id))
1321 .filter(|agent| agent.handles_event(event_type))
1322 .map(|agent| {
1323 let limit = if event_type == EventType::ResponseBodyChunk {
1324 agent.max_response_body_bytes()
1325 } else {
1326 agent.max_request_body_bytes()
1327 };
1328 (agent.id().to_string(), agent.failure_mode(), limit)
1329 })
1330 .collect();
1331
1332 let outcome = evaluate_body_limits(&limits, body_size);
1333
1334 for (agent_id, limit) in &outcome.skipped {
1335 warn!(
1336 correlation_id = %ctx.correlation_id,
1337 agent_id = %agent_id,
1338 body_size = body_size,
1339 limit = limit,
1340 event_type = ?event_type,
1341 "Body exceeds agent inspection limit, skipping agent (fail-open)"
1342 );
1343 if let Some(agent) = agents.get(agent_id) {
1344 agent.metrics().record_body_size_skip();
1345 }
1346 }
1347
1348 if let Some((agent_id, limit)) = outcome.blocked_by {
1349 warn!(
1350 correlation_id = %ctx.correlation_id,
1351 agent_id = %agent_id,
1352 body_size = body_size,
1353 limit = limit,
1354 event_type = ?event_type,
1355 "Body exceeds agent inspection limit, blocking request (fail-closed)"
1356 );
1357 return BodyLimitsResult::Block(Box::new(
1358 AgentDecision::block(413, "Payload too large for security inspection")
1359 .with_decided_by(agent_id),
1360 ));
1361 }
1362
1363 BodyLimitsResult::Proceed(outcome.allowed)
1364 }
1365}
1366
1367enum BodyLimitsResult {
1369 Proceed(Vec<String>),
1371 Block(Box<AgentDecision>),
1373}
1374
1375#[derive(Debug)]
1377struct BodyLimitOutcome {
1378 allowed: Vec<String>,
1380 skipped: Vec<(String, usize)>,
1382 blocked_by: Option<(String, usize)>,
1384}
1385
1386fn evaluate_body_limits(
1388 agents: &[(String, FailureMode, usize)],
1389 body_size: usize,
1390) -> BodyLimitOutcome {
1391 let mut outcome = BodyLimitOutcome {
1392 allowed: Vec::new(),
1393 skipped: Vec::new(),
1394 blocked_by: None,
1395 };
1396
1397 for (id, failure_mode, limit) in agents {
1398 if body_size <= *limit {
1399 outcome.allowed.push(id.clone());
1400 } else if *failure_mode == FailureMode::Closed {
1401 outcome.blocked_by = Some((id.clone(), *limit));
1402 break;
1403 } else {
1404 outcome.skipped.push((id.clone(), *limit));
1405 }
1406 }
1407
1408 outcome
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::*;
1414
1415 fn limits() -> Vec<(String, FailureMode, usize)> {
1416 vec![
1417 ("waf".to_string(), FailureMode::Closed, 1024),
1418 ("audit".to_string(), FailureMode::Open, 512),
1419 ("dlp".to_string(), FailureMode::Open, 4096),
1420 ]
1421 }
1422
1423 #[test]
1424 fn body_within_all_limits_allows_all_agents() {
1425 let outcome = evaluate_body_limits(&limits(), 256);
1426 assert_eq!(outcome.allowed, vec!["waf", "audit", "dlp"]);
1427 assert!(outcome.skipped.is_empty());
1428 assert!(outcome.blocked_by.is_none());
1429 }
1430
1431 #[test]
1432 fn oversized_body_blocks_on_fail_closed_agent() {
1433 let outcome = evaluate_body_limits(&limits(), 2048);
1434 assert_eq!(
1435 outcome.blocked_by,
1436 Some(("waf".to_string(), 1024)),
1437 "fail-closed agent over its limit must block"
1438 );
1439 }
1440
1441 #[test]
1442 fn oversized_body_skips_fail_open_agent_and_keeps_others() {
1443 let outcome = evaluate_body_limits(&limits(), 600);
1444 assert_eq!(outcome.allowed, vec!["waf", "dlp"]);
1445 assert_eq!(outcome.skipped, vec![("audit".to_string(), 512)]);
1446 assert!(outcome.blocked_by.is_none());
1447 }
1448
1449 #[test]
1450 fn no_agents_yields_empty_outcome() {
1451 let outcome = evaluate_body_limits(&[], 1_000_000);
1452 assert!(outcome.allowed.is_empty());
1453 assert!(outcome.skipped.is_empty());
1454 assert!(outcome.blocked_by.is_none());
1455 }
1456}