1use std::collections::{HashMap, HashSet};
9use std::sync::Arc;
10
11use crate::config::PolicyConfig;
12use crate::context::Context;
13use crate::debug::{EdgeKind, StepOutcome, TraceRecorder};
14use crate::plugins::resources::PluginResources;
15use crate::plugins::{self, Plugin};
16
17pub struct CompiledGraph {
22 nodes: HashMap<String, Box<dyn Plugin>>,
23 success_edges: HashMap<String, String>,
25 error_edges: HashMap<String, String>,
27 entry_node_id: String,
29 terminal_node_ids: HashSet<String>,
31 catch_all_handler: Option<String>,
33 policy_name: String,
35 resources: Arc<PluginResources>,
38}
39
40impl CompiledGraph {
41 pub async fn execute(&self, ctx: Context) -> Context {
54 self.run(ctx, None).await.0
55 }
56
57 pub async fn execute_traced(
65 &self,
66 ctx: Context,
67 recorder: TraceRecorder,
68 ) -> (Context, TraceRecorder) {
69 let (ctx, rec) = self.run(ctx, Some(recorder)).await;
70 (
71 ctx,
72 rec.expect("recorder is returned when one was supplied"),
73 )
74 }
75
76 async fn run(
82 &self,
83 mut ctx: Context,
84 mut recorder: Option<TraceRecorder>,
85 ) -> (Context, Option<TraceRecorder>) {
86 let mut current_node_id = self.entry_node_id.clone();
87
88 loop {
89 let node = match self.nodes.get(¤t_node_id) {
90 Some(n) => n,
91 None => {
92 ctx.response.status_code = 500;
93 ctx.response.body = bytes::Bytes::from(format!(
94 "Graph error: node '{}' not found",
95 current_node_id
96 ));
97 if let Some(r) = recorder.as_mut() {
98 r.record_step(
99 ¤t_node_id,
100 "<missing>",
101 StepOutcome::Error {
102 code: "NODE_NOT_FOUND".to_string(),
103 message: format!("node '{}' not found", current_node_id),
104 },
105 std::time::Duration::ZERO,
106 EdgeKind::NodeNotFound,
107 None,
108 &ctx,
109 );
110 }
111 break;
112 }
113 };
114
115 let named_inputs = HashMap::new();
116
117 let node_type = node.plugin_type().to_string();
118 let started = std::time::Instant::now();
119 let result = node.execute(ctx, &named_inputs).await;
120 let elapsed = started.elapsed();
121 if let Some(ref m) = self.resources.metrics {
122 m.node_execution_count
123 .with_label_values(&[&self.policy_name, ¤t_node_id, &node_type])
124 .inc();
125 m.node_execution_duration
126 .with_label_values(&[&self.policy_name, ¤t_node_id])
127 .observe(elapsed.as_secs_f64());
128 }
129
130 match result {
131 Ok(output) => {
132 ctx = output.context;
133
134 let terminal = self.terminal_node_ids.contains(¤t_node_id);
136 let next = if terminal {
137 None
138 } else {
139 self.success_edges.get(¤t_node_id)
140 };
141 if let Some(r) = recorder.as_mut() {
142 let edge = if terminal {
143 EdgeKind::Terminal
144 } else if next.is_some() {
145 EdgeKind::Success
146 } else {
147 EdgeKind::EndOfChain
148 };
149 r.record_step(
150 ¤t_node_id,
151 &node_type,
152 StepOutcome::Success,
153 elapsed,
154 edge,
155 next.map(String::as_str),
156 &ctx,
157 );
158 }
159 match next {
160 Some(next_id) => current_node_id = next_id.clone(),
161 None => break,
163 }
164 }
165 Err(mut err) => {
166 if let Some(ref m) = self.resources.metrics {
167 m.node_errors
168 .with_label_values(&[
169 &self.policy_name,
170 ¤t_node_id,
171 &err.error.code,
172 ])
173 .inc();
174 }
175
176 err.error.node_id = current_node_id.clone();
178 let outcome = StepOutcome::Error {
179 code: err.error.code.clone(),
180 message: err.error.message.clone(),
181 };
182 err.context.errors.push(err.error);
183 ctx = err.context;
184
185 let next_error = self
187 .error_edges
188 .get(¤t_node_id)
189 .map(|id| (id.clone(), EdgeKind::Error))
190 .or_else(|| {
191 self.catch_all_handler
192 .as_ref()
193 .map(|id| (id.clone(), EdgeKind::CatchAll))
194 });
195
196 if next_error.is_none() {
197 ctx.response.status_code = 500;
199 ctx.response.body = bytes::Bytes::from(
200 r#"{"error": "internal_error", "message": "Unhandled error in routing policy"}"#,
201 );
202 ctx.response.headers.insert(
203 "content-type".to_string(),
204 vec!["application/json".to_string()],
205 );
206 }
207
208 if let Some(r) = recorder.as_mut() {
209 let (edge, next_id) = match &next_error {
210 Some((id, kind)) => (*kind, Some(id.as_str())),
211 None => (EdgeKind::Unhandled, None),
212 };
213 r.record_step(
214 ¤t_node_id,
215 &node_type,
216 outcome,
217 elapsed,
218 edge,
219 next_id,
220 &ctx,
221 );
222 }
223
224 match next_error {
225 Some((id, _)) => current_node_id = id,
226 None => break,
227 }
228 }
229 }
230 }
231
232 (ctx, recorder)
233 }
234}
235
236pub fn compile_policy(
257 policy: &PolicyConfig,
258 resources: Arc<PluginResources>,
259) -> Result<CompiledGraph, String> {
260 let mut nodes: HashMap<String, Box<dyn Plugin>> = HashMap::new();
261 let mut success_edges: HashMap<String, String> = HashMap::new();
262 let mut error_edges: HashMap<String, String> = HashMap::new();
263 let mut listener_node_id = None;
264 let mut terminal_node_ids = HashSet::new();
265
266 for node_config in &policy.nodes {
268 tracing::debug!(
269 "Compiling node '{}' type='{}' config={:?}",
270 node_config.id,
271 node_config.node_type,
272 node_config.config
273 );
274 let mut config = node_config.config.clone();
280 for value in config.values_mut() {
281 crate::config::interpolate_env_json(value);
282 }
283 let plugin = plugins::create_plugin(&node_config.node_type, &config, &resources)?;
284 if node_config.node_type == "listener" {
285 listener_node_id = Some(node_config.id.clone());
286 }
287 if node_config.node_type == "client" {
288 terminal_node_ids.insert(node_config.id.clone());
289 }
290 nodes.insert(node_config.id.clone(), plugin);
291 }
292
293 let listener_node_id = listener_node_id.ok_or("Policy must have a listener node")?;
294
295 for edge in &policy.edges {
297 let (from_node, from_port) = parse_edge_endpoint(&edge.from)?;
298 let (to_node, _to_port) = parse_edge_endpoint(&edge.to)?;
299
300 match from_port.as_str() {
301 "success" | "out" => {
302 success_edges.insert(from_node, to_node);
303 }
304 "error" => {
305 error_edges.insert(from_node, to_node);
306 }
307 other => {
308 return Err(format!("Unknown edge port: '{}'", other));
309 }
310 }
311 }
312
313 let entry_node_id = success_edges
315 .get(&listener_node_id)
316 .cloned()
317 .unwrap_or_else(|| listener_node_id.clone());
318
319 Ok(CompiledGraph {
320 nodes,
321 success_edges,
322 error_edges,
323 entry_node_id,
324 terminal_node_ids,
325 catch_all_handler: policy.error_handler.clone(),
326 policy_name: policy.name.clone(),
327 resources,
328 })
329}
330
331fn parse_edge_endpoint(endpoint: &str) -> Result<(String, String), String> {
335 if let Some(dot_pos) = endpoint.rfind('.') {
336 let node_id = endpoint[..dot_pos].to_string();
337 let port = endpoint[dot_pos + 1..].to_string();
338 Ok((node_id, port))
339 } else {
340 Ok((endpoint.to_string(), "out".to_string()))
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use crate::config::{EdgeConfig, NodeConfig, PolicyConfig};
348 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
349 use bytes::Bytes;
350
351 #[test]
352 fn test_parse_edge_endpoint() {
353 let (node, port) = parse_edge_endpoint("listener.out").unwrap();
354 assert_eq!(node, "listener");
355 assert_eq!(port, "out");
356
357 let (node, port) = parse_edge_endpoint("upstream.error").unwrap();
358 assert_eq!(node, "upstream");
359 assert_eq!(port, "error");
360
361 let (node, port) = parse_edge_endpoint("rewrite.success").unwrap();
362 assert_eq!(node, "rewrite");
363 assert_eq!(port, "success");
364 }
365
366 fn test_context(path: &str) -> Context {
367 Context {
368 request: GatewayRequest {
369 method: "GET".to_string(),
370 path: path.to_string(),
371 host: "localhost".to_string(),
372 scheme: "http".to_string(),
373 headers: HashMap::new(),
374 query_params: HashMap::new(),
375 body: Bytes::new(),
376 remote_addr: "127.0.0.1:12345".to_string(),
377 protocol: Protocol::Http1,
378 },
379 response: GatewayResponse {
380 status_code: 0,
381 headers: HashMap::new(),
382 body: Bytes::new(),
383 },
384 message: HashMap::new(),
385 errors: Vec::new(),
386 }
387 }
388
389 #[tokio::test]
390 async fn test_graph_proxy_rewrite_pipeline() {
391 let mut rewrite_config = HashMap::new();
392 rewrite_config.insert(
393 "strip_path_prefix".to_string(),
394 serde_json::Value::String("/api/v1".to_string()),
395 );
396 rewrite_config.insert(
397 "phase".to_string(),
398 serde_json::Value::String("request".to_string()),
399 );
400
401 let policy = PolicyConfig {
402 name: "test".to_string(),
403 error_handler: None,
404 nodes: vec![
405 NodeConfig {
406 id: "listener".to_string(),
407 node_type: "listener".to_string(),
408 config: HashMap::new(),
409 position: None,
410 },
411 NodeConfig {
412 id: "rewrite".to_string(),
413 node_type: "proxy-rewrite".to_string(),
414 config: rewrite_config,
415 position: None,
416 },
417 NodeConfig {
418 id: "client".to_string(),
419 node_type: "client".to_string(),
420 config: HashMap::new(),
421 position: None,
422 },
423 ],
424 edges: vec![
425 EdgeConfig {
426 from: "listener.out".to_string(),
427 to: "rewrite.in".to_string(),
428 },
429 EdgeConfig {
430 from: "rewrite.success".to_string(),
431 to: "client.in".to_string(),
432 },
433 ],
434 };
435
436 let graph = compile_policy(&policy, PluginResources::empty()).unwrap();
437 let ctx = test_context("/api/v1/users");
438 let result = graph.execute(ctx).await;
439
440 assert_eq!(result.request.path, "/users");
441 }
442
443 #[tokio::test]
444 async fn test_graph_records_node_metrics() {
445 let policy = PolicyConfig {
446 name: "metrics-test".to_string(),
447 error_handler: None,
448 nodes: vec![
449 NodeConfig {
450 id: "listener".to_string(),
451 node_type: "listener".to_string(),
452 config: HashMap::new(),
453 position: None,
454 },
455 NodeConfig {
456 id: "client".to_string(),
457 node_type: "client".to_string(),
458 config: HashMap::new(),
459 position: None,
460 },
461 ],
462 edges: vec![EdgeConfig {
463 from: "listener.out".to_string(),
464 to: "client.in".to_string(),
465 }],
466 };
467
468 let metrics = Arc::new(crate::metrics::GatewayMetrics::new());
469 let graph = compile_policy(&policy, PluginResources::new(Some(metrics.clone()))).unwrap();
470 graph.execute(test_context("/test")).await;
471
472 assert_eq!(
473 metrics
474 .node_execution_count
475 .with_label_values(&["metrics-test", "client", "client"])
476 .get(),
477 1
478 );
479 }
480
481 #[tokio::test]
482 async fn test_graph_error_handler_catch_all() {
483 let policy = PolicyConfig {
484 name: "test".to_string(),
485 error_handler: Some("error-handler".to_string()),
486 nodes: vec![
487 NodeConfig {
488 id: "listener".to_string(),
489 node_type: "listener".to_string(),
490 config: HashMap::new(),
491 position: None,
492 },
493 NodeConfig {
494 id: "error-handler".to_string(),
495 node_type: "error-handler".to_string(),
496 config: {
497 let mut c = HashMap::new();
498 c.insert("status_code".to_string(), serde_json::json!(503));
499 c.insert(
500 "body_template".to_string(),
501 serde_json::Value::String(r#"{"error": "{{error.code}}"}"#.to_string()),
502 );
503 c
504 },
505 position: None,
506 },
507 NodeConfig {
508 id: "client".to_string(),
509 node_type: "client".to_string(),
510 config: HashMap::new(),
511 position: None,
512 },
513 ],
514 edges: vec![
515 EdgeConfig {
516 from: "listener.out".to_string(),
517 to: "error-handler.in".to_string(),
518 },
519 EdgeConfig {
520 from: "error-handler.success".to_string(),
521 to: "client.in".to_string(),
522 },
523 ],
524 };
525
526 let graph = compile_policy(&policy, PluginResources::empty()).unwrap();
527 let ctx = test_context("/test");
528 let result = graph.execute(ctx).await;
529
530 assert_eq!(result.response.status_code, 503);
531 }
532
533 use crate::debug::{CaptureOptions, EdgeKind, StepOutcome, TraceRecorder, TraceSource};
536 use std::time::Duration;
537
538 fn recorder(ctx: &Context) -> TraceRecorder {
539 TraceRecorder::new(ctx, CaptureOptions::default(), 100)
540 }
541
542 fn finish(rec: TraceRecorder, ctx: &Context) -> crate::debug::Trace {
543 rec.finish(
544 "t".to_string(),
545 0,
546 TraceSource::Request,
547 Some("r".to_string()),
548 "p".to_string(),
549 ctx,
550 Duration::from_millis(1),
551 )
552 }
553
554 struct AlwaysFails;
557
558 #[async_trait::async_trait]
559 impl Plugin for AlwaysFails {
560 fn plugin_type(&self) -> &str {
561 "always-fails"
562 }
563 async fn execute(
564 &self,
565 ctx: Context,
566 _: &HashMap<String, serde_json::Value>,
567 ) -> crate::plugins::PluginResult {
568 Err(crate::plugins::PluginExecutionError {
569 context: ctx,
570 error: crate::context::GatewayError {
571 node_id: String::new(),
572 code: "BOOM".to_string(),
573 message: "exploded".to_string(),
574 metadata: HashMap::new(),
575 },
576 })
577 }
578 }
579
580 fn failing_graph(
582 error_edges: HashMap<String, String>,
583 catch_all: Option<String>,
584 ) -> CompiledGraph {
585 let mut nodes: HashMap<String, Box<dyn Plugin>> = HashMap::new();
586 nodes.insert("boom".to_string(), Box::new(AlwaysFails));
587 nodes.insert(
588 "client".to_string(),
589 plugins::create_plugin("client", &HashMap::new(), &PluginResources::empty()).unwrap(),
590 );
591 CompiledGraph {
592 nodes,
593 success_edges: HashMap::new(),
594 error_edges,
595 entry_node_id: "boom".to_string(),
596 terminal_node_ids: HashSet::from(["client".to_string()]),
597 catch_all_handler: catch_all,
598 policy_name: "p".to_string(),
599 resources: PluginResources::empty(),
600 }
601 }
602
603 fn rewrite_policy() -> PolicyConfig {
604 let mut cfg = HashMap::new();
605 cfg.insert(
606 "strip_path_prefix".to_string(),
607 serde_json::Value::String("/api/v1".to_string()),
608 );
609 cfg.insert(
610 "phase".to_string(),
611 serde_json::Value::String("request".to_string()),
612 );
613 PolicyConfig {
614 name: "traced".to_string(),
615 error_handler: None,
616 nodes: vec![
617 NodeConfig {
618 id: "listener".to_string(),
619 node_type: "listener".to_string(),
620 config: HashMap::new(),
621 position: None,
622 },
623 NodeConfig {
624 id: "rewrite".to_string(),
625 node_type: "proxy-rewrite".to_string(),
626 config: cfg,
627 position: None,
628 },
629 NodeConfig {
630 id: "client".to_string(),
631 node_type: "client".to_string(),
632 config: HashMap::new(),
633 position: None,
634 },
635 ],
636 edges: vec![
637 EdgeConfig {
638 from: "listener.out".to_string(),
639 to: "rewrite.in".to_string(),
640 },
641 EdgeConfig {
642 from: "rewrite.success".to_string(),
643 to: "client.in".to_string(),
644 },
645 ],
646 }
647 }
648
649 #[tokio::test]
650 async fn test_trace_records_each_node_in_order() {
651 let graph = compile_policy(&rewrite_policy(), PluginResources::empty()).unwrap();
652 let ctx = test_context("/api/v1/users");
653 let rec = recorder(&ctx);
654 let (out, rec) = graph.execute_traced(ctx, rec).await;
655 let trace = finish(rec, &out);
656
657 let ids: Vec<&str> = trace.steps.iter().map(|s| s.node_id.as_str()).collect();
658 assert_eq!(ids, vec!["rewrite", "client"]);
659 assert_eq!(trace.steps[0].node_type, "proxy-rewrite");
660 assert_eq!(trace.steps[0].edge, EdgeKind::Success);
661 assert_eq!(trace.steps[0].next_node_id.as_deref(), Some("client"));
662 assert_eq!(trace.steps[1].edge, EdgeKind::Terminal);
664 assert_eq!(trace.steps[1].next_node_id, None);
665 }
666
667 #[tokio::test]
669 async fn test_trace_captures_what_the_plugin_changed() {
670 let graph = compile_policy(&rewrite_policy(), PluginResources::empty()).unwrap();
671 let ctx = test_context("/api/v1/users");
672 let rec = recorder(&ctx);
673 let (out, rec) = graph.execute_traced(ctx, rec).await;
674 let trace = finish(rec, &out);
675
676 assert_eq!(trace.initial.request.path, "/api/v1/users");
677 assert_eq!(trace.steps[0].after.request.path, "/users");
678
679 let changes = crate::debug::diff::diff(&trace.initial, &trace.steps[0].after);
680 let c = changes
681 .iter()
682 .find(|c| c.path == "request.path")
683 .expect("path change");
684 assert_eq!(c.before.as_deref(), Some("/api/v1/users"));
685 assert_eq!(c.after.as_deref(), Some("/users"));
686 }
687
688 #[tokio::test]
691 async fn test_tracing_does_not_alter_behaviour() {
692 let policy = rewrite_policy();
693 let plain = compile_policy(&policy, PluginResources::empty()).unwrap();
694 let traced = compile_policy(&policy, PluginResources::empty()).unwrap();
695
696 let untraced_out = plain.execute(test_context("/api/v1/users")).await;
697 let ctx = test_context("/api/v1/users");
698 let rec = recorder(&ctx);
699 let (traced_out, _) = traced.execute_traced(ctx, rec).await;
700
701 assert_eq!(untraced_out.request.path, traced_out.request.path);
702 assert_eq!(
703 untraced_out.response.status_code,
704 traced_out.response.status_code
705 );
706 assert_eq!(untraced_out.response.body, traced_out.response.body);
707 assert_eq!(untraced_out.message, traced_out.message);
708 assert_eq!(untraced_out.errors, traced_out.errors);
709 }
710
711 #[tokio::test]
712 async fn test_trace_records_error_edge() {
713 let graph = failing_graph(
714 HashMap::from([("boom".to_string(), "client".to_string())]),
715 None,
716 );
717 let ctx = test_context("/x");
718 let rec = recorder(&ctx);
719 let (out, rec) = graph.execute_traced(ctx, rec).await;
720 let trace = finish(rec, &out);
721
722 assert_eq!(trace.steps[0].node_id, "boom");
723 assert_eq!(
724 trace.steps[0].outcome,
725 StepOutcome::Error {
726 code: "BOOM".to_string(),
727 message: "exploded".to_string()
728 }
729 );
730 assert_eq!(trace.steps[0].edge, EdgeKind::Error);
731 assert_eq!(trace.steps[0].next_node_id.as_deref(), Some("client"));
732 assert_eq!(trace.steps[0].after.errors.len(), 1);
734 assert_eq!(trace.steps[0].after.errors[0].node_id, "boom");
735 }
736
737 #[tokio::test]
738 async fn test_trace_records_catch_all_edge() {
739 let graph = failing_graph(HashMap::new(), Some("client".to_string()));
740 let ctx = test_context("/x");
741 let rec = recorder(&ctx);
742 let (out, rec) = graph.execute_traced(ctx, rec).await;
743 let trace = finish(rec, &out);
744 assert_eq!(trace.steps[0].edge, EdgeKind::CatchAll);
745 }
746
747 #[tokio::test]
750 async fn test_trace_records_unhandled_error() {
751 let graph = failing_graph(HashMap::new(), None);
752 let ctx = test_context("/x");
753 let rec = recorder(&ctx);
754 let (out, rec) = graph.execute_traced(ctx, rec).await;
755 let trace = finish(rec, &out);
756
757 assert_eq!(trace.steps.len(), 1);
758 assert_eq!(trace.steps[0].edge, EdgeKind::Unhandled);
759 assert_eq!(trace.steps[0].next_node_id, None);
760 assert_eq!(out.response.status_code, 500);
762 assert_eq!(trace.steps[0].after.response.status_code, 500);
763 }
764}