Skip to main content

eggress_runtime/
snapshot.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use eggress_config::compile::{
5    AdminConfig, GroupFallback, ListenerConfig, RuntimeConfig, UpstreamConfig,
6};
7use eggress_routing::upstream::{UpstreamGroup, UpstreamRuntime};
8use eggress_routing::{RouteActionSpec, Router};
9
10pub struct CompiledRuntimeSnapshot {
11    pub generation: u64,
12    pub upstreams: HashMap<String, Arc<UpstreamRuntime>>,
13    pub router: Arc<Router>,
14    pub health_config: eggress_routing::health::HealthConfig,
15    pub listeners: Vec<ListenerConfig>,
16    pub admin: Option<AdminConfig>,
17    pub reverse_servers: Vec<eggress_config::compile::CompiledReverseServerConfig>,
18    pub reverse_clients: Vec<eggress_config::compile::CompiledReverseClientConfig>,
19}
20
21/// Check whether an existing `UpstreamRuntime` is compatible with a new config,
22/// meaning its chain specification hasn't changed and we can reuse the Arc.
23fn upstream_runtime_compatible(old: &UpstreamRuntime, new: &UpstreamConfig) -> bool {
24    *old.chain == new.chain && old.health_config == new.health
25}
26
27/// Build a `CompiledRuntimeSnapshot` from a `RuntimeConfig`.
28///
29/// Upstream runtimes are created first and shared with groups/router so that
30/// the same `Arc<UpstreamRuntime>` objects are used for health probing and routing.
31pub fn compile_runtime_snapshot(
32    rt: &RuntimeConfig,
33    previous: Option<&CompiledRuntimeSnapshot>,
34) -> Result<CompiledRuntimeSnapshot, Box<dyn std::error::Error + Send + Sync>> {
35    let empty_map = HashMap::new();
36    let previous_upstreams = previous.map(|p| &p.upstreams).unwrap_or(&empty_map);
37
38    let mut upstreams: HashMap<String, Arc<UpstreamRuntime>> = HashMap::new();
39
40    for u in &rt.upstreams {
41        let runtime = if let Some(existing) = previous_upstreams.get(&u.id) {
42            if upstream_runtime_compatible(existing, u) {
43                existing.clone()
44            } else {
45                build_one_upstream_runtime(u)
46            }
47        } else {
48            build_one_upstream_runtime(u)
49        };
50        upstreams.insert(u.id.clone(), runtime);
51    }
52
53    let group_ids: std::collections::HashSet<_> = rt.groups.iter().map(|g| g.id.clone()).collect();
54
55    let mut groups = Vec::new();
56    for g in &rt.groups {
57        let mut members = Vec::new();
58        for m in &g.members {
59            let member = upstreams
60                .get(m)
61                .ok_or_else(|| format!("group '{}' references unknown upstream '{}'", g.id, m))?;
62            members.push(member.clone());
63        }
64        if members.is_empty() {
65            return Err(format!("group '{}' has no valid members", g.id).into());
66        }
67
68        let fallback = match g.fallback {
69            GroupFallback::Reject => eggress_routing::upstream::GroupFallback::Reject,
70            GroupFallback::Direct => eggress_routing::upstream::GroupFallback::Direct,
71            GroupFallback::UseUnhealthy => eggress_routing::upstream::GroupFallback::UseUnhealthy,
72        };
73
74        groups.push((
75            g.id.clone(),
76            UpstreamGroup::new(g.id.clone(), g.scheduler, Arc::from(members), fallback),
77        ));
78    }
79
80    let mut rules = Vec::new();
81    for r in &rt.rules {
82        let action = match &r.action {
83            RouteActionSpec::Direct => RouteActionSpec::Direct,
84            RouteActionSpec::UpstreamGroup(gid) => {
85                if !group_ids.contains(gid) {
86                    return Err(
87                        format!("rule '{}' references unknown group '{}'", r.id, gid).into(),
88                    );
89                }
90                RouteActionSpec::UpstreamGroup(gid.clone())
91            }
92            RouteActionSpec::Reject(reason) => RouteActionSpec::Reject(reason.clone()),
93        };
94        rules.push(eggress_routing::CompiledRule {
95            id: r.id.clone(),
96            matcher: r.matcher.clone(),
97            action,
98        });
99    }
100
101    let router = Router::with_groups(rules, rt.default_action.clone(), groups);
102    let gen = previous.map(|p| p.generation + 1).unwrap_or(0);
103
104    Ok(CompiledRuntimeSnapshot {
105        generation: gen,
106        upstreams,
107        router: Arc::new(router),
108        health_config: eggress_routing::health::HealthConfig::default(),
109        listeners: rt.listeners.clone(),
110        admin: rt.admin.clone(),
111        reverse_servers: rt.reverse_servers.clone(),
112        reverse_clients: rt.reverse_clients.clone(),
113    })
114}
115
116fn build_one_upstream_runtime(u: &UpstreamConfig) -> Arc<UpstreamRuntime> {
117    let id = eggress_core::UpstreamId::new(u.id.clone());
118    let mut runtime =
119        UpstreamRuntime::new(id, u.chain.clone()).with_health_config(u.health.clone());
120
121    if let Some(first_hop) = u.chain.hops.first() {
122        let addr: Result<std::net::SocketAddr, _> =
123            format!("{}:{}", first_hop.endpoint.host, first_hop.endpoint.port).parse();
124        if let Ok(addr) = addr {
125            runtime = runtime.with_health_probe(eggress_routing::health::HealthProbe::TcpConnect {
126                target: addr,
127                timeout: u.health.timeout,
128            });
129        }
130    }
131
132    Arc::new(runtime)
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use eggress_config::compile::{
139        GroupFallback, ProcessConfig, RuntimeConfig, TimeoutConfig, UpstreamConfig,
140    };
141    use eggress_routing::scheduler::SchedulerKind;
142    use eggress_routing::UpstreamGroupId;
143    use eggress_uri::ProxyChainSpec;
144    use std::time::Duration;
145
146    fn default_health() -> eggress_routing::health::HealthConfig {
147        eggress_routing::health::HealthConfig::default()
148    }
149
150    fn empty_config() -> RuntimeConfig {
151        RuntimeConfig {
152            process: ProcessConfig::default(),
153            timeouts: TimeoutConfig::default(),
154            listeners: vec![],
155            upstreams: vec![],
156            groups: vec![],
157            rules: vec![],
158            default_action: RouteActionSpec::Direct,
159            admin: None,
160            reverse_servers: vec![],
161            reverse_clients: vec![],
162        }
163    }
164
165    #[test]
166    fn snapshot_empty_config() {
167        let snap = compile_runtime_snapshot(&empty_config(), None).unwrap();
168        assert_eq!(snap.generation, 0);
169        assert!(snap.upstreams.is_empty());
170        assert!(snap.router.rules().is_empty());
171    }
172
173    #[test]
174    fn snapshot_single_upstream() {
175        let mut cfg = empty_config();
176        cfg.upstreams = vec![UpstreamConfig {
177            id: "proxy1".to_string(),
178            chain: ProxyChainSpec { hops: vec![] },
179            health: default_health(),
180            h2: None,
181        }];
182        let snap = compile_runtime_snapshot(&cfg, None).unwrap();
183        assert_eq!(snap.upstreams.len(), 1);
184        assert!(snap.upstreams.contains_key("proxy1"));
185    }
186
187    #[test]
188    fn snapshot_group_uses_shared_upstream_arc() {
189        let mut cfg = empty_config();
190        cfg.upstreams = vec![UpstreamConfig {
191            id: "proxy1".to_string(),
192            chain: ProxyChainSpec { hops: vec![] },
193            health: default_health(),
194            h2: None,
195        }];
196        cfg.groups = vec![eggress_config::compile::UpstreamGroupConfig {
197            id: UpstreamGroupId(Arc::from("main")),
198            scheduler: SchedulerKind::RoundRobin,
199            members: vec!["proxy1".to_string()],
200            fallback: GroupFallback::Reject,
201        }];
202        let snap = compile_runtime_snapshot(&cfg, None).unwrap();
203        let upstream_arc = snap.upstreams.get("proxy1").unwrap();
204        let group = snap
205            .router
206            .groups()
207            .get(&UpstreamGroupId(Arc::from("main")))
208            .unwrap();
209        let group_member = &group.members[0];
210        assert!(Arc::ptr_eq(upstream_arc, group_member));
211    }
212
213    #[test]
214    fn unchanged_upstream_retains_arc_identity_after_reload() {
215        let mut cfg = empty_config();
216        cfg.upstreams = vec![UpstreamConfig {
217            id: "proxy1".to_string(),
218            chain: ProxyChainSpec { hops: vec![] },
219            health: default_health(),
220            h2: None,
221        }];
222        let snap1 = compile_runtime_snapshot(&cfg, None).unwrap();
223        let original_arc = snap1.upstreams.get("proxy1").unwrap().clone();
224
225        let snap2 = compile_runtime_snapshot(&cfg, Some(&snap1)).unwrap();
226        let reused_arc = snap2.upstreams.get("proxy1").unwrap().clone();
227
228        assert!(Arc::ptr_eq(&original_arc, &reused_arc));
229    }
230
231    #[test]
232    fn changed_upstream_gets_fresh_arc() {
233        let mut cfg1 = empty_config();
234        cfg1.upstreams = vec![UpstreamConfig {
235            id: "proxy1".to_string(),
236            chain: ProxyChainSpec { hops: vec![] },
237            health: default_health(),
238            h2: None,
239        }];
240        let snap1 = compile_runtime_snapshot(&cfg1, None).unwrap();
241        let original_arc = snap1.upstreams.get("proxy1").unwrap().clone();
242
243        let mut cfg2 = empty_config();
244        cfg2.upstreams = vec![UpstreamConfig {
245            id: "proxy1".to_string(),
246            chain: ProxyChainSpec {
247                hops: vec![eggress_uri::ProxyHopSpec {
248                    protocols: vec![eggress_uri::ProtocolSpec::Socks5],
249                    endpoint: eggress_uri::EndpointSpec {
250                        host: "newhost".to_string(),
251                        port: 1080,
252                    },
253                    credentials: None,
254                    rule: None,
255                    local_bind: None,
256                    tls: false,
257                    server_name: None,
258                    insecure: false,
259                    plugins: Vec::new(),
260                    auth_prefix: None,
261                }],
262            },
263            health: default_health(),
264            h2: None,
265        }];
266        let snap2 = compile_runtime_snapshot(&cfg2, Some(&snap1)).unwrap();
267        let new_arc = snap2.upstreams.get("proxy1").unwrap().clone();
268
269        assert!(!Arc::ptr_eq(&original_arc, &new_arc));
270    }
271
272    #[test]
273    fn no_duplicate_upstream_runtime_objects_for_one_id() {
274        let mut cfg = empty_config();
275        cfg.upstreams = vec![UpstreamConfig {
276            id: "proxy1".to_string(),
277            chain: ProxyChainSpec { hops: vec![] },
278            health: default_health(),
279            h2: None,
280        }];
281        cfg.groups = vec![eggress_config::compile::UpstreamGroupConfig {
282            id: UpstreamGroupId(Arc::from("main")),
283            scheduler: SchedulerKind::RoundRobin,
284            members: vec!["proxy1".to_string()],
285            fallback: GroupFallback::Reject,
286        }];
287        let snap = compile_runtime_snapshot(&cfg, None).unwrap();
288        let upstream_arc = snap.upstreams.get("proxy1").unwrap();
289        let group = snap
290            .router
291            .groups()
292            .get(&UpstreamGroupId(Arc::from("main")))
293            .unwrap();
294        let group_member = &group.members[0];
295
296        assert!(Arc::ptr_eq(upstream_arc, group_member));
297    }
298
299    #[test]
300    fn generation_increments_on_reload() {
301        let cfg = empty_config();
302        let snap1 = compile_runtime_snapshot(&cfg, None).unwrap();
303        assert_eq!(snap1.generation, 0);
304        let snap2 = compile_runtime_snapshot(&cfg, Some(&snap1)).unwrap();
305        assert_eq!(snap2.generation, 1);
306        let snap3 = compile_runtime_snapshot(&cfg, Some(&snap2)).unwrap();
307        assert_eq!(snap3.generation, 2);
308    }
309
310    #[test]
311    fn group_references_unknown_upstream() {
312        let mut cfg = empty_config();
313        cfg.groups = vec![eggress_config::compile::UpstreamGroupConfig {
314            id: UpstreamGroupId(Arc::from("main")),
315            scheduler: SchedulerKind::RoundRobin,
316            members: vec!["nonexistent".to_string()],
317            fallback: GroupFallback::Reject,
318        }];
319        let result = compile_runtime_snapshot(&cfg, None);
320        assert!(result.is_err());
321        let err_msg = result.err().unwrap().to_string();
322        assert!(err_msg.contains("nonexistent"));
323    }
324
325    #[test]
326    fn rule_references_unknown_group() {
327        let mut cfg = empty_config();
328        cfg.rules = vec![eggress_routing::CompiledRule {
329            id: eggress_routing::RuleId(Arc::from("r1")),
330            matcher: eggress_routing::MatchExpr::Any,
331            action: RouteActionSpec::UpstreamGroup(UpstreamGroupId(Arc::from("missing"))),
332        }];
333        let result = compile_runtime_snapshot(&cfg, None);
334        assert!(result.is_err());
335        let err_msg = result.err().unwrap().to_string();
336        assert!(err_msg.contains("missing"));
337    }
338
339    #[test]
340    fn multiple_upstreams_all_shared() {
341        let mut cfg = empty_config();
342        cfg.upstreams = vec![
343            UpstreamConfig {
344                id: "p1".to_string(),
345                chain: ProxyChainSpec { hops: vec![] },
346                health: default_health(),
347                h2: None,
348            },
349            UpstreamConfig {
350                id: "p2".to_string(),
351                chain: ProxyChainSpec { hops: vec![] },
352                health: default_health(),
353                h2: None,
354            },
355        ];
356        cfg.groups = vec![eggress_config::compile::UpstreamGroupConfig {
357            id: UpstreamGroupId(Arc::from("grp")),
358            scheduler: SchedulerKind::RoundRobin,
359            members: vec!["p1".to_string(), "p2".to_string()],
360            fallback: GroupFallback::Reject,
361        }];
362        let snap = compile_runtime_snapshot(&cfg, None).unwrap();
363        let group = snap
364            .router
365            .groups()
366            .get(&UpstreamGroupId(Arc::from("grp")))
367            .unwrap();
368        assert!(Arc::ptr_eq(
369            snap.upstreams.get("p1").unwrap(),
370            &group.members[0]
371        ));
372        assert!(Arc::ptr_eq(
373            snap.upstreams.get("p2").unwrap(),
374            &group.members[1]
375        ));
376    }
377
378    #[test]
379    fn changed_health_config_gets_fresh_arc() {
380        let mut cfg1 = empty_config();
381        cfg1.upstreams = vec![UpstreamConfig {
382            id: "proxy1".to_string(),
383            chain: ProxyChainSpec { hops: vec![] },
384            health: eggress_routing::health::HealthConfig {
385                failures_to_unhealthy: 3,
386                ..default_health()
387            },
388            h2: None,
389        }];
390        let snap1 = compile_runtime_snapshot(&cfg1, None).unwrap();
391        let original_arc = snap1.upstreams.get("proxy1").unwrap().clone();
392
393        let mut cfg2 = empty_config();
394        cfg2.upstreams = vec![UpstreamConfig {
395            id: "proxy1".to_string(),
396            chain: ProxyChainSpec { hops: vec![] },
397            health: eggress_routing::health::HealthConfig {
398                failures_to_unhealthy: 1,
399                ..default_health()
400            },
401            h2: None,
402        }];
403        let snap2 = compile_runtime_snapshot(&cfg2, Some(&snap1)).unwrap();
404        let new_arc = snap2.upstreams.get("proxy1").unwrap().clone();
405
406        assert!(
407            !Arc::ptr_eq(&original_arc, &new_arc),
408            "changing health config should produce a fresh upstream runtime ARC"
409        );
410    }
411
412    #[test]
413    fn unchanged_health_config_retains_arc_identity() {
414        let mut cfg1 = empty_config();
415        cfg1.upstreams = vec![UpstreamConfig {
416            id: "proxy1".to_string(),
417            chain: ProxyChainSpec { hops: vec![] },
418            health: eggress_routing::health::HealthConfig {
419                failures_to_unhealthy: 2,
420                successes_to_healthy: 1,
421                interval: Duration::from_secs(10),
422                timeout: Duration::from_secs(2),
423                initial_state: eggress_routing::health::HealthState::Unknown,
424            },
425            h2: None,
426        }];
427        let snap1 = compile_runtime_snapshot(&cfg1, None).unwrap();
428        let original_arc = snap1.upstreams.get("proxy1").unwrap().clone();
429
430        let mut cfg2 = empty_config();
431        cfg2.upstreams = vec![UpstreamConfig {
432            id: "proxy1".to_string(),
433            chain: ProxyChainSpec { hops: vec![] },
434            health: eggress_routing::health::HealthConfig {
435                failures_to_unhealthy: 2,
436                successes_to_healthy: 1,
437                interval: Duration::from_secs(10),
438                timeout: Duration::from_secs(2),
439                initial_state: eggress_routing::health::HealthState::Unknown,
440            },
441            h2: None,
442        }];
443        let snap2 = compile_runtime_snapshot(&cfg2, Some(&snap1)).unwrap();
444        let reused_arc = snap2.upstreams.get("proxy1").unwrap().clone();
445
446        assert!(
447            Arc::ptr_eq(&original_arc, &reused_arc),
448            "identical health config should retain the upstream runtime ARC"
449        );
450    }
451
452    #[test]
453    fn partial_upstream_change_preserves_others() {
454        let mut cfg1 = empty_config();
455        cfg1.upstreams = vec![
456            UpstreamConfig {
457                id: "p1".to_string(),
458                chain: ProxyChainSpec { hops: vec![] },
459                health: default_health(),
460                h2: None,
461            },
462            UpstreamConfig {
463                id: "p2".to_string(),
464                chain: ProxyChainSpec { hops: vec![] },
465                health: default_health(),
466                h2: None,
467            },
468        ];
469        let snap1 = compile_runtime_snapshot(&cfg1, None).unwrap();
470        let p1_original = snap1.upstreams.get("p1").unwrap().clone();
471        let p2_original = snap1.upstreams.get("p2").unwrap().clone();
472
473        let mut cfg2 = empty_config();
474        cfg2.upstreams = vec![
475            UpstreamConfig {
476                id: "p1".to_string(),
477                chain: ProxyChainSpec { hops: vec![] },
478                health: default_health(),
479                h2: None,
480            },
481            UpstreamConfig {
482                id: "p2".to_string(),
483                chain: ProxyChainSpec {
484                    hops: vec![eggress_uri::ProxyHopSpec {
485                        protocols: vec![eggress_uri::ProtocolSpec::Http],
486                        endpoint: eggress_uri::EndpointSpec {
487                            host: "changed".to_string(),
488                            port: 8080,
489                        },
490                        credentials: None,
491                        rule: None,
492                        local_bind: None,
493                        tls: false,
494                        server_name: None,
495                        insecure: false,
496                        plugins: Vec::new(),
497                        auth_prefix: None,
498                    }],
499                },
500                health: default_health(),
501                h2: None,
502            },
503        ];
504        let snap2 = compile_runtime_snapshot(&cfg2, Some(&snap1)).unwrap();
505
506        assert!(Arc::ptr_eq(
507            &p1_original,
508            snap2.upstreams.get("p1").unwrap()
509        ));
510        assert!(!Arc::ptr_eq(
511            &p2_original,
512            snap2.upstreams.get("p2").unwrap()
513        ));
514    }
515}