1use cidr_map::CidrSet;
2use data_loader::KeySource;
3use dns_resolver::IpLookupStrategy;
4use mailexchanger::MailExchanger;
5#[cfg(feature = "lua")]
6use mlua::prelude::*;
7use openssl::ssl::SslOptions;
8use ordermap::OrderMap;
9use rfc5321::SmtpClientTimeouts;
10use rustls::crypto::aws_lc_rs::ALL_CIPHER_SUITES;
11use rustls::SupportedCipherSuite;
12use serde::{Deserialize, Deserializer, Serialize};
13use std::collections::BTreeMap;
14use std::fmt::Write;
15use std::time::Duration;
16use throttle::{LimitSpec, ThrottleSpec};
17use utoipa::ToSchema;
18
19#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Copy)]
20pub enum Tls {
21 Opportunistic,
25 OpportunisticInsecure,
29 Required,
31 RequiredInsecure,
35 Disabled,
37}
38
39impl Tls {
40 pub fn allow_insecure(&self) -> bool {
41 match self {
42 Self::OpportunisticInsecure | Self::RequiredInsecure => true,
43 _ => false,
44 }
45 }
46
47 pub fn is_opportunistic(&self) -> bool {
48 match self {
49 Self::OpportunisticInsecure | Self::Opportunistic => true,
50 _ => false,
51 }
52 }
53}
54
55impl Default for Tls {
56 fn default() -> Self {
57 Self::Opportunistic
58 }
59}
60
61pub fn parse_openssl_options(option_list: &str) -> anyhow::Result<SslOptions> {
62 let mut result = SslOptions::empty();
63
64 for option in option_list.split('|') {
65 match SslOptions::from_name(option) {
66 Some(opt) => {
67 result.insert(opt);
68 }
69 None => {
70 let mut allowed: Vec<_> = SslOptions::all()
71 .iter_names()
72 .map(|(name, _)| format!("`{name}`"))
73 .collect();
74 allowed.sort();
75 let allowed = allowed.join(", ");
76 anyhow::bail!(
77 "`{option}` is not a valid SslOption name. \
78 Possible values are {allowed} joined together by the pipe `|` character."
79 );
80 }
81 }
82 }
83
84 Ok(result)
85}
86
87fn deserialize_ssl_options<'de, D>(deserializer: D) -> Result<Option<SslOptions>, D::Error>
88where
89 D: Deserializer<'de>,
90{
91 use serde::de::Error;
92 let maybe_options = Option::<String>::deserialize(deserializer)?;
93
94 match maybe_options {
95 None => Ok(None),
96 Some(option_list) => match parse_openssl_options(&option_list) {
97 Ok(options) => Ok(Some(options)),
98 Err(err) => Err(D::Error::custom(format!("{err:#}"))),
99 },
100 }
101}
102
103fn deserialize_supported_ciphersuite<'de, D>(
104 deserializer: D,
105) -> Result<Vec<SupportedCipherSuite>, D::Error>
106where
107 D: Deserializer<'de>,
108{
109 use serde::de::Error;
110 let suites = Vec::<String>::deserialize(deserializer)?;
111 let mut result = vec![];
112
113 for s in suites {
114 match find_rustls_cipher_suite(&s) {
115 Some(s) => {
116 result.push(s);
117 }
118 None => {
119 return Err(D::Error::custom(format!(
120 "`{s}` is not a valid rustls cipher suite"
121 )));
122 }
123 }
124 }
125
126 Ok(result)
127}
128
129pub fn find_rustls_cipher_suite(name: &str) -> Option<SupportedCipherSuite> {
130 for suite in ALL_CIPHER_SUITES {
131 let sname = format!("{:?}", suite.suite());
132 if sname.eq_ignore_ascii_case(name) {
133 return Some(*suite);
134 }
135 }
136 None
137}
138
139#[derive(Deserialize, Serialize, Debug, Clone, Default, Copy, PartialEq, Eq)]
140#[cfg_attr(feature = "lua", derive(FromLua))]
141pub enum WakeupStrategy {
142 #[default]
143 Aggressive,
144 Relaxed,
145}
146
147#[derive(Deserialize, Serialize, Debug, Clone, Default, Copy, PartialEq, Eq)]
148#[cfg_attr(feature = "lua", derive(FromLua))]
149pub enum MemoryReductionPolicy {
150 #[default]
151 ShrinkDataAndMeta,
152 ShrinkData,
153 NoShrink,
154}
155
156#[derive(Deserialize, Serialize, Debug, Clone, Default, Copy, PartialEq, Eq)]
157#[cfg_attr(feature = "lua", derive(FromLua))]
158pub enum ConfigRefreshStrategy {
159 #[default]
160 Ttl,
161 Epoch,
162}
163
164#[derive(Deserialize, Serialize, Debug, Clone, Default, Copy, PartialEq, Eq)]
165#[cfg_attr(feature = "lua", derive(FromLua))]
166pub enum ReconnectStrategy {
167 TerminateSession,
171 ReconnectSameHost,
174 #[default]
177 ConnectNextHost,
178}
179
180#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
181#[cfg_attr(feature = "lua", derive(FromLua))]
182#[serde(deny_unknown_fields)]
183pub struct EgressPathConfig {
184 #[serde(default = "EgressPathConfig::default_connection_limit")]
185 pub connection_limit: LimitSpec,
186
187 #[serde(default)]
188 pub additional_connection_limits: OrderMap<String, LimitSpec>,
189
190 #[serde(default)]
191 pub enable_tls: Tls,
192
193 #[serde(default = "EgressPathConfig::default_enable_mta_sts")]
194 pub enable_mta_sts: bool,
195
196 #[serde(default = "EgressPathConfig::default_enable_dane")]
197 pub enable_dane: bool,
198
199 #[serde(default = "EgressPathConfig::default_enable_pipelining")]
200 pub enable_pipelining: bool,
201
202 #[serde(default = "EgressPathConfig::default_enable_rset")]
203 pub enable_rset: bool,
204
205 #[serde(default)]
206 pub tls_prefer_openssl: bool,
207
208 #[serde(default)]
209 pub tls_certificate: Option<KeySource>,
210
211 #[serde(default)]
212 pub tls_private_key: Option<KeySource>,
213
214 #[serde(default)]
215 pub openssl_cipher_list: Option<String>,
216 #[serde(default)]
217 pub openssl_cipher_suites: Option<String>,
218 #[serde(
219 default,
220 deserialize_with = "deserialize_ssl_options",
221 skip_serializing )]
223 pub openssl_options: Option<SslOptions>,
224
225 #[serde(
226 default,
227 deserialize_with = "deserialize_supported_ciphersuite",
228 skip_serializing )]
230 pub rustls_cipher_suites: Vec<SupportedCipherSuite>,
231
232 #[serde(flatten)]
233 pub client_timeouts: SmtpClientTimeouts,
234
235 #[serde(default, with = "duration_serde")]
239 pub system_shutdown_timeout: Option<Duration>,
240
241 #[serde(default = "EgressPathConfig::default_max_ready")]
242 pub max_ready: usize,
243
244 #[serde(default = "EgressPathConfig::default_consecutive_connection_failures_before_delay")]
245 pub consecutive_connection_failures_before_delay: usize,
246
247 #[serde(default = "EgressPathConfig::default_smtp_port")]
248 pub smtp_port: u16,
249
250 #[serde(default)]
251 pub smtp_auth_plain_username: Option<String>,
252
253 #[serde(default)]
254 pub smtp_auth_plain_password: Option<KeySource>,
255
256 #[serde(default)]
257 pub allow_smtp_auth_plain_without_tls: bool,
258
259 #[serde(default)]
266 pub allow_smtp_auth_plain_without_valid_certificate: bool,
267
268 #[serde(default)]
269 pub max_message_rate: Option<ThrottleSpec>,
270
271 #[serde(default)]
272 pub additional_message_rate_throttles: OrderMap<String, ThrottleSpec>,
273
274 #[serde(default)]
275 pub source_selection_rate: Option<ThrottleSpec>,
276
277 #[serde(default)]
278 pub additional_source_selection_rates: OrderMap<String, ThrottleSpec>,
279
280 #[serde(default)]
281 pub max_connection_rate: Option<ThrottleSpec>,
282
283 #[serde(default = "EgressPathConfig::default_max_deliveries_per_connection")]
284 pub max_deliveries_per_connection: usize,
285
286 #[serde(default = "EgressPathConfig::default_max_recipients_per_batch")]
287 pub max_recipients_per_batch: usize,
288
289 #[serde(default = "CidrSet::default_prohibited_hosts")]
290 pub prohibited_hosts: CidrSet,
291
292 #[serde(default)]
293 pub skip_hosts: CidrSet,
294
295 #[serde(default)]
296 pub ip_lookup_strategy: IpLookupStrategy,
297
298 #[serde(default)]
299 pub ehlo_domain: Option<String>,
300
301 #[serde(default)]
303 pub aggressive_connection_opening: bool,
304
305 #[serde(
310 default = "EgressPathConfig::default_refresh_interval",
311 with = "duration_serde"
312 )]
313 pub refresh_interval: Duration,
314 #[serde(default)]
315 pub refresh_strategy: ConfigRefreshStrategy,
316
317 #[serde(default)]
318 pub dispatcher_wakeup_strategy: WakeupStrategy,
319 #[serde(default)]
320 pub maintainer_wakeup_strategy: WakeupStrategy,
321
322 #[serde(default)]
326 pub provider_name: Option<String>,
327
328 #[serde(default, with = "duration_serde")]
333 pub remember_broken_tls: Option<Duration>,
334
335 #[serde(default)]
339 pub opportunistic_tls_reconnect_on_failed_handshake: bool,
340
341 #[serde(default)]
343 pub use_lmtp: bool,
344
345 #[serde(default)]
348 pub reconnect_strategy: ReconnectStrategy,
349
350 #[serde(default)]
352 pub readyq_pool_name: Option<String>,
353
354 #[serde(default)]
356 pub low_memory_reduction_policy: MemoryReductionPolicy,
357
358 #[serde(default)]
360 pub no_memory_reduction_policy: MemoryReductionPolicy,
361
362 #[serde(default)]
366 pub try_next_host_on_transport_error: bool,
367
368 #[serde(default)]
372 pub ignore_8bit_checks: bool,
373
374 #[serde(default, with = "duration_serde")]
384 pub dispatcher_progress_watchdog_timeout: Option<Duration>,
385}
386
387#[cfg(feature = "lua")]
388impl LuaUserData for EgressPathConfig {
389 fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
390 config::impl_pairs_and_index(methods);
391 }
392}
393
394impl Default for EgressPathConfig {
395 fn default() -> Self {
396 Self {
397 connection_limit: Self::default_connection_limit(),
398 tls_prefer_openssl: false,
399 enable_tls: Tls::default(),
400 enable_mta_sts: Self::default_enable_mta_sts(),
401 enable_dane: Self::default_enable_dane(),
402 enable_rset: Self::default_enable_rset(),
403 enable_pipelining: Self::default_enable_pipelining(),
404 max_ready: Self::default_max_ready(),
405 consecutive_connection_failures_before_delay:
406 Self::default_consecutive_connection_failures_before_delay(),
407 smtp_port: Self::default_smtp_port(),
408 max_message_rate: None,
409 max_connection_rate: None,
410 max_deliveries_per_connection: Self::default_max_deliveries_per_connection(),
411 max_recipients_per_batch: Self::default_max_recipients_per_batch(),
412 client_timeouts: SmtpClientTimeouts::default(),
413 system_shutdown_timeout: None,
414 prohibited_hosts: CidrSet::default_prohibited_hosts(),
415 skip_hosts: CidrSet::default(),
416 ehlo_domain: None,
417 allow_smtp_auth_plain_without_tls: false,
418 allow_smtp_auth_plain_without_valid_certificate: false,
419 smtp_auth_plain_username: None,
420 smtp_auth_plain_password: None,
421 aggressive_connection_opening: false,
422 rustls_cipher_suites: vec![],
423 tls_certificate: None,
424 tls_private_key: None,
425 openssl_cipher_list: None,
426 openssl_cipher_suites: None,
427 openssl_options: None,
428 refresh_interval: Self::default_refresh_interval(),
429 refresh_strategy: ConfigRefreshStrategy::default(),
430 additional_message_rate_throttles: OrderMap::default(),
431 additional_connection_limits: OrderMap::default(),
432 source_selection_rate: None,
433 additional_source_selection_rates: OrderMap::default(),
434 provider_name: None,
435 remember_broken_tls: None,
436 opportunistic_tls_reconnect_on_failed_handshake: false,
437 use_lmtp: false,
438 reconnect_strategy: ReconnectStrategy::default(),
439 readyq_pool_name: None,
440 low_memory_reduction_policy: MemoryReductionPolicy::default(),
441 no_memory_reduction_policy: MemoryReductionPolicy::default(),
442 maintainer_wakeup_strategy: WakeupStrategy::default(),
443 dispatcher_wakeup_strategy: WakeupStrategy::default(),
444 try_next_host_on_transport_error: false,
445 ignore_8bit_checks: false,
446 ip_lookup_strategy: IpLookupStrategy::default(),
447 dispatcher_progress_watchdog_timeout: None,
448 }
449 }
450}
451
452impl EgressPathConfig {
453 fn default_connection_limit() -> LimitSpec {
454 LimitSpec::new(32)
455 }
456
457 fn default_enable_mta_sts() -> bool {
458 true
459 }
460
461 fn default_enable_pipelining() -> bool {
462 true
463 }
464
465 fn default_enable_rset() -> bool {
466 true
467 }
468
469 fn default_enable_dane() -> bool {
470 false
471 }
472
473 fn default_max_ready() -> usize {
474 1024
475 }
476
477 fn default_consecutive_connection_failures_before_delay() -> usize {
478 100
479 }
480
481 fn default_smtp_port() -> u16 {
482 25
483 }
484
485 fn default_max_deliveries_per_connection() -> usize {
486 1024
487 }
488
489 fn default_max_recipients_per_batch() -> usize {
490 100
491 }
492
493 fn default_refresh_interval() -> Duration {
494 Duration::from_secs(60)
495 }
496
497 pub fn compute_constraints(
507 &self,
508 additional: Option<&EffectiveConstraints>,
509 ) -> EffectiveConstraints {
510 let mut constraints = self.compute_path_constraints();
511 if let Some(extra) = additional {
512 constraints.merge(extra);
513 }
514 constraints
515 }
516
517 fn compute_path_constraints(&self) -> EffectiveConstraints {
518 let max_concurrent_dispatchers = {
519 let mut best = EffectiveCeiling {
520 value: self.connection_limit.limit as f64,
521 source: CeilingSource::Primary,
522 display: self.connection_limit.limit.to_string(),
523 };
524 for (name, spec) in &self.additional_connection_limits {
525 let value = spec.limit as f64;
526 if value < best.value {
527 best = EffectiveCeiling {
528 value,
529 source: CeilingSource::Additional { name: name.clone() },
530 display: spec.limit.to_string(),
531 };
532 }
533 }
534 best
535 };
536
537 let mut msg_candidates: Vec<EffectiveCeiling> = vec![];
540 if let Some(spec) = &self.max_message_rate {
541 msg_candidates.push(throttle_ceiling(spec, CeilingSource::Primary));
542 }
543 for (name, spec) in &self.additional_message_rate_throttles {
544 msg_candidates.push(throttle_ceiling(
545 spec,
546 CeilingSource::Additional { name: name.clone() },
547 ));
548 }
549 if let Some(spec) = &self.max_connection_rate {
550 let k = self.max_deliveries_per_connection as u64;
558 let total = ThrottleSpec {
559 limit: k.saturating_mul(spec.limit),
560 period: spec.period,
561 max_burst: None,
562 force_local: false,
563 };
564 msg_candidates.push(EffectiveCeiling {
565 value: throttle_rate_per_sec(spec) * k as f64,
566 source: CeilingSource::ReconnectCycling,
567 display: format!("{k} × {spec} = {total}"),
568 });
569 }
570 let max_message_rate = msg_candidates
574 .iter()
575 .min_by(|a, b| a.value.total_cmp(&b.value))
576 .cloned();
577
578 let max_message_rate_declared = match (&max_message_rate, &self.max_message_rate) {
585 (Some(ceiling), Some(declared))
586 if !matches!(ceiling.source, CeilingSource::Primary) =>
587 {
588 Some(declared.to_string())
589 }
590 _ => None,
591 };
592
593 let max_connection_rate = self
594 .max_connection_rate
595 .as_ref()
596 .map(|spec| throttle_ceiling(spec, CeilingSource::Primary));
597
598 let max_source_selection_rate = {
599 let mut candidates: Vec<EffectiveCeiling> = vec![];
600 if let Some(spec) = &self.source_selection_rate {
601 candidates.push(throttle_ceiling(spec, CeilingSource::Primary));
602 }
603 for (name, spec) in &self.additional_source_selection_rates {
604 candidates.push(throttle_ceiling(
605 spec,
606 CeilingSource::Additional { name: name.clone() },
607 ));
608 }
609 candidates
610 .into_iter()
611 .min_by(|a, b| a.value.total_cmp(&b.value))
612 };
613
614 EffectiveConstraints {
615 max_concurrent_dispatchers,
616 max_message_rate,
617 max_message_rate_declared,
618 max_connection_rate,
619 max_source_selection_rate,
620 }
621 }
622}
623
624fn throttle_rate_per_sec(spec: &ThrottleSpec) -> f64 {
625 spec.limit as f64 / spec.period as f64
626}
627
628fn throttle_ceiling(spec: &ThrottleSpec, source: CeilingSource) -> EffectiveCeiling {
629 EffectiveCeiling {
630 value: throttle_rate_per_sec(spec),
631 source,
632 display: spec.to_string(),
633 }
634}
635
636#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
641pub struct EffectiveCeiling {
642 pub value: f64,
645 pub source: CeilingSource,
646 pub display: String,
653}
654
655#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
659#[serde(tag = "kind", rename_all = "snake_case")]
660pub enum CeilingSource {
661 Primary,
665 Additional { name: String },
667 ReconnectCycling,
675 Other { name: String },
680}
681
682#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
686pub struct MxResolution {
687 pub site_name: String,
690
691 pub by_preference: BTreeMap<u16, Vec<String>>,
695
696 pub is_mx: bool,
699
700 pub is_domain_literal: bool,
702
703 pub is_secure: bool,
705}
706
707impl From<&MailExchanger> for MxResolution {
708 fn from(mx: &MailExchanger) -> Self {
709 Self {
710 site_name: mx.site_name.clone(),
711 by_preference: mx.by_pref.clone(),
712 is_mx: mx.is_mx,
713 is_domain_literal: mx.is_domain_literal,
714 is_secure: mx.is_secure,
715 }
716 }
717}
718
719impl MxResolution {
720 pub fn render(&self, out: &mut dyn Write) -> std::fmt::Result {
725 let mut flags: Vec<&str> = vec![];
726 if !self.is_mx {
727 flags.push("synthesised");
728 }
729 if self.is_domain_literal {
730 flags.push("domain literal");
731 }
732 if self.is_secure {
733 flags.push("dnssec verified");
734 }
735 write!(out, "mx (site: {}", self.site_name)?;
736 if !flags.is_empty() {
737 write!(out, ", {}", flags.join(", "))?;
738 }
739 writeln!(out, "):")?;
740 for (pref, hosts) in &self.by_preference {
741 for host in hosts {
742 writeln!(out, " {pref:>4} {host}")?;
743 }
744 }
745 Ok(())
746 }
747
748 pub fn to_human_string(&self) -> String {
749 let mut s = String::new();
750 let _ = self.render(&mut s);
751 s
752 }
753}
754
755#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
764pub struct EffectiveConstraints {
765 pub max_concurrent_dispatchers: EffectiveCeiling,
766 pub max_message_rate: Option<EffectiveCeiling>,
770 pub max_message_rate_declared: Option<String>,
775 pub max_connection_rate: Option<EffectiveCeiling>,
776 pub max_source_selection_rate: Option<EffectiveCeiling>,
777}
778
779impl EffectiveConstraints {
780 pub fn render(&self, out: &mut dyn Write) -> std::fmt::Result {
785 let label_axis = |w: &mut dyn Write, label: &str, value: &str| -> std::fmt::Result {
786 writeln!(w, " {label:<24}{value}")
787 };
788 let label_source =
789 |w: &mut dyn Write, src: &CeilingSource, primary: &str| -> std::fmt::Result {
790 let s = match src {
791 CeilingSource::Primary => primary,
792 CeilingSource::Additional { name } => name.as_str(),
793 CeilingSource::ReconnectCycling => {
794 "max_deliveries_per_connection × max_connection_rate"
795 }
796 CeilingSource::Other { name } => name.as_str(),
797 };
798 writeln!(w, " source: {s}")
799 };
800
801 writeln!(out, "ceilings:")?;
802
803 label_axis(
804 out,
805 "concurrent dispatchers:",
806 &self.max_concurrent_dispatchers.display,
807 )?;
808 label_source(
809 out,
810 &self.max_concurrent_dispatchers.source,
811 "connection_limit",
812 )?;
813
814 if let Some(c) = &self.max_message_rate {
815 label_axis(out, "message rate:", &c.display)?;
816 label_source(out, &c.source, "max_message_rate")?;
817 if let Some(declared) = &self.max_message_rate_declared {
818 writeln!(
819 out,
820 " declared: max_message_rate = {declared} ← effectively unreachable"
821 )?;
822 }
823 }
824
825 if let Some(c) = &self.max_connection_rate {
826 label_axis(out, "connection rate:", &c.display)?;
827 label_source(out, &c.source, "max_connection_rate")?;
828 }
829
830 if let Some(c) = &self.max_source_selection_rate {
831 label_axis(out, "source selection rate:", &c.display)?;
832 label_source(out, &c.source, "source_selection_rate")?;
833 }
834
835 Ok(())
836 }
837
838 pub fn to_human_string(&self) -> String {
839 let mut s = String::new();
840 let _ = self.render(&mut s);
843 s
844 }
845
846 pub fn merge(&mut self, other: &EffectiveConstraints) {
853 let path_declared = self
857 .max_message_rate
858 .as_ref()
859 .filter(|c| matches!(c.source, CeilingSource::Primary))
860 .map(|c| c.display.clone())
861 .or_else(|| self.max_message_rate_declared.clone());
862
863 merge_axis(
864 &mut self.max_concurrent_dispatchers,
865 &other.max_concurrent_dispatchers,
866 );
867 merge_optional(&mut self.max_message_rate, &other.max_message_rate);
868 merge_optional(&mut self.max_connection_rate, &other.max_connection_rate);
869 merge_optional(
870 &mut self.max_source_selection_rate,
871 &other.max_source_selection_rate,
872 );
873
874 if let Some(merged) = &self.max_message_rate {
875 if !matches!(merged.source, CeilingSource::Primary) {
876 if let Some(declared) = path_declared {
877 self.max_message_rate_declared = Some(declared);
878 }
879 }
880 }
881 }
882}
883
884fn merge_axis(a: &mut EffectiveCeiling, b: &EffectiveCeiling) {
885 if b.value.total_cmp(&a.value).is_lt() {
886 *a = b.clone();
887 }
888}
889
890fn merge_optional(a: &mut Option<EffectiveCeiling>, b: &Option<EffectiveCeiling>) {
891 match (a.as_mut(), b) {
892 (Some(av), Some(bv)) if bv.value.total_cmp(&av.value).is_lt() => *av = bv.clone(),
893 (None, Some(bv)) => *a = Some(bv.clone()),
894 _ => {}
895 }
896}
897
898#[cfg(test)]
899mod constraints_tests {
900 use super::*;
901
902 fn cfg() -> EgressPathConfig {
903 EgressPathConfig::default()
904 }
905
906 fn value_from_display(display: &str) -> f64 {
914 if let Some((_, total)) = display.split_once(" = ") {
915 let spec = ThrottleSpec::try_from(total).unwrap();
916 return spec.limit as f64 / spec.period as f64;
917 }
918 if let Ok(spec) = ThrottleSpec::try_from(display) {
919 return spec.limit as f64 / spec.period as f64;
920 }
921 display.parse::<u64>().unwrap() as f64
922 }
923
924 fn primary(display: &str) -> EffectiveCeiling {
925 EffectiveCeiling {
926 value: value_from_display(display),
927 source: CeilingSource::Primary,
928 display: display.to_string(),
929 }
930 }
931
932 fn additional(name: &str, display: &str) -> EffectiveCeiling {
933 EffectiveCeiling {
934 value: value_from_display(display),
935 source: CeilingSource::Additional {
936 name: name.to_string(),
937 },
938 display: display.to_string(),
939 }
940 }
941
942 fn other(name: &str, display: &str) -> EffectiveCeiling {
943 EffectiveCeiling {
944 value: value_from_display(display),
945 source: CeilingSource::Other {
946 name: name.to_string(),
947 },
948 display: display.to_string(),
949 }
950 }
951
952 fn reconnect_cycling(display: &str) -> EffectiveCeiling {
953 EffectiveCeiling {
954 value: value_from_display(display),
955 source: CeilingSource::ReconnectCycling,
956 display: display.to_string(),
957 }
958 }
959
960 fn throttle(s: &str) -> ThrottleSpec {
961 ThrottleSpec::try_from(s).unwrap()
962 }
963
964 #[test]
965 fn rust_compact_round_trips() {
966 let original = EgressPathConfig::default();
971 let s = mod_serde::toml_encode_pretty_compact(&original).unwrap();
972 let parsed: EgressPathConfig = toml::from_str(&s).unwrap();
973 assert_eq!(parsed, original);
974 }
975
976 #[test]
977 fn defaults_only_concurrency() {
978 assert_eq!(
979 cfg().compute_constraints(None),
980 EffectiveConstraints {
981 max_concurrent_dispatchers: primary("32"),
982 max_message_rate: None,
983 max_message_rate_declared: None,
984 max_connection_rate: None,
985 max_source_selection_rate: None,
986 }
987 );
988 }
989
990 #[test]
991 fn additional_connection_limit_wins() {
992 let mut p = cfg();
993 p.additional_connection_limits
994 .insert("provider_shared".to_string(), LimitSpec::new(10));
995 assert_eq!(
996 p.compute_constraints(None),
997 EffectiveConstraints {
998 max_concurrent_dispatchers: additional("provider_shared", "10"),
999 max_message_rate: None,
1000 max_message_rate_declared: None,
1001 max_connection_rate: None,
1002 max_source_selection_rate: None,
1003 }
1004 );
1005 }
1006
1007 #[test]
1008 fn primary_connection_limit_wins_when_smaller() {
1009 let mut p = cfg();
1010 p.connection_limit = LimitSpec::new(4);
1011 p.additional_connection_limits
1012 .insert("large_pool".to_string(), LimitSpec::new(100));
1013 assert_eq!(
1014 p.compute_constraints(None),
1015 EffectiveConstraints {
1016 max_concurrent_dispatchers: primary("4"),
1017 max_message_rate: None,
1018 max_message_rate_declared: None,
1019 max_connection_rate: None,
1020 max_source_selection_rate: None,
1021 }
1022 );
1023 }
1024
1025 #[test]
1026 fn primary_message_rate_only() {
1027 let mut p = cfg();
1028 p.max_message_rate = Some(throttle("1000/s"));
1029 assert_eq!(
1030 p.compute_constraints(None),
1031 EffectiveConstraints {
1032 max_concurrent_dispatchers: primary("32"),
1033 max_message_rate: Some(primary("1000/s")),
1034 max_message_rate_declared: None,
1035 max_connection_rate: None,
1036 max_source_selection_rate: None,
1037 }
1038 );
1039 }
1040
1041 #[test]
1042 fn additional_message_rate_wins() {
1043 let mut p = cfg();
1044 p.max_message_rate = Some(throttle("1000/s"));
1045 p.additional_message_rate_throttles
1046 .insert("provider_cap".to_string(), throttle("250/s"));
1047 assert_eq!(
1048 p.compute_constraints(None),
1049 EffectiveConstraints {
1050 max_concurrent_dispatchers: primary("32"),
1051 max_message_rate: Some(additional("provider_cap", "250/s")),
1052 max_message_rate_declared: Some("1000/s".to_string()),
1053 max_connection_rate: None,
1054 max_source_selection_rate: None,
1055 }
1056 );
1057 }
1058
1059 #[test]
1060 fn additional_message_rate_throttles_mixed_periods() {
1061 let mut p = cfg();
1066 p.additional_message_rate_throttles
1067 .insert("per_hour".to_string(), throttle("10/hr"));
1068 p.additional_message_rate_throttles
1069 .insert("per_minute".to_string(), throttle("8/min"));
1070 p.additional_message_rate_throttles
1071 .insert("per_second".to_string(), throttle("5/s"));
1072 assert_eq!(
1073 p.compute_constraints(None),
1074 EffectiveConstraints {
1075 max_concurrent_dispatchers: primary("32"),
1076 max_message_rate: Some(additional("per_hour", "10/h")),
1077 max_message_rate_declared: None,
1078 max_connection_rate: None,
1079 max_source_selection_rate: None,
1080 }
1081 );
1082 }
1083
1084 #[test]
1085 fn hourly_rate_preserves_units() {
1086 let mut p = cfg();
1091 p.max_message_rate = Some(throttle("10000/hr"));
1092 assert_eq!(
1093 p.compute_constraints(None),
1094 EffectiveConstraints {
1095 max_concurrent_dispatchers: primary("32"),
1096 max_message_rate: Some(primary("10000/h")),
1097 max_message_rate_declared: None,
1098 max_connection_rate: None,
1099 max_source_selection_rate: None,
1100 }
1101 );
1102 }
1103
1104 #[test]
1105 fn reconnect_cycling_wins() {
1106 let mut p = cfg();
1109 p.max_message_rate = Some(throttle("1000/s"));
1110 p.max_deliveries_per_connection = 10;
1111 p.max_connection_rate = Some(throttle("10/s"));
1112 assert_eq!(
1113 p.compute_constraints(None),
1114 EffectiveConstraints {
1115 max_concurrent_dispatchers: primary("32"),
1116 max_message_rate: Some(reconnect_cycling("10 × 10/s = 100/s")),
1117 max_message_rate_declared: Some("1000/s".to_string()),
1118 max_connection_rate: Some(primary("10/s")),
1119 max_source_selection_rate: None,
1120 }
1121 );
1122 }
1123
1124 #[test]
1125 fn reconnect_cycling_does_not_bind_with_large_k() {
1126 let mut p = cfg();
1130 p.max_message_rate = Some(throttle("1000/s"));
1131 p.max_connection_rate = Some(throttle("10/s"));
1132 assert_eq!(
1133 p.compute_constraints(None),
1134 EffectiveConstraints {
1135 max_concurrent_dispatchers: primary("32"),
1136 max_message_rate: Some(primary("1000/s")),
1137 max_message_rate_declared: None,
1138 max_connection_rate: Some(primary("10/s")),
1139 max_source_selection_rate: None,
1140 }
1141 );
1142 }
1143
1144 #[test]
1145 fn reconnect_cycling_alone_sets_message_rate() {
1146 let mut p = cfg();
1150 p.max_deliveries_per_connection = 5;
1151 p.max_connection_rate = Some(throttle("2/s"));
1152 assert_eq!(
1153 p.compute_constraints(None),
1154 EffectiveConstraints {
1155 max_concurrent_dispatchers: primary("32"),
1156 max_message_rate: Some(reconnect_cycling("5 × 2/s = 10/s")),
1157 max_message_rate_declared: None,
1158 max_connection_rate: Some(primary("2/s")),
1159 max_source_selection_rate: None,
1160 }
1161 );
1162 }
1163
1164 #[test]
1165 fn merge_external_message_rate_wins() {
1166 let mut p = cfg();
1171 p.max_message_rate = Some(throttle("1000/s"));
1172 let external = EffectiveConstraints {
1173 max_concurrent_dispatchers: EffectiveCeiling {
1174 value: f64::INFINITY,
1175 source: CeilingSource::Other {
1176 name: "queue config".to_string(),
1177 },
1178 display: "∞".to_string(),
1179 },
1180 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1181 max_message_rate_declared: None,
1182 max_connection_rate: None,
1183 max_source_selection_rate: None,
1184 };
1185 assert_eq!(
1186 p.compute_constraints(Some(&external)),
1187 EffectiveConstraints {
1188 max_concurrent_dispatchers: primary("32"),
1189 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1190 max_message_rate_declared: Some("1000/s".to_string()),
1191 max_connection_rate: None,
1192 max_source_selection_rate: None,
1193 }
1194 );
1195 }
1196
1197 #[test]
1198 fn merge_external_does_not_bind() {
1199 let mut p = cfg();
1202 p.max_message_rate = Some(throttle("100/s"));
1203 let external = EffectiveConstraints {
1204 max_concurrent_dispatchers: EffectiveCeiling {
1205 value: f64::INFINITY,
1206 source: CeilingSource::Other {
1207 name: "queue config".to_string(),
1208 },
1209 display: "∞".to_string(),
1210 },
1211 max_message_rate: Some(other("scheduled queue max_message_rate", "1000/s")),
1212 max_message_rate_declared: None,
1213 max_connection_rate: None,
1214 max_source_selection_rate: None,
1215 };
1216 assert_eq!(
1217 p.compute_constraints(Some(&external)),
1218 EffectiveConstraints {
1219 max_concurrent_dispatchers: primary("32"),
1220 max_message_rate: Some(primary("100/s")),
1221 max_message_rate_declared: None,
1222 max_connection_rate: None,
1223 max_source_selection_rate: None,
1224 }
1225 );
1226 }
1227
1228 #[test]
1229 fn render_external_wins_with_annotation() {
1230 let mut p = cfg();
1231 p.max_message_rate = Some(throttle("1000/s"));
1232 let external = EffectiveConstraints {
1233 max_concurrent_dispatchers: EffectiveCeiling {
1234 value: f64::INFINITY,
1235 source: CeilingSource::Other {
1236 name: "queue config".to_string(),
1237 },
1238 display: "∞".to_string(),
1239 },
1240 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1241 max_message_rate_declared: None,
1242 max_connection_rate: None,
1243 max_source_selection_rate: None,
1244 };
1245 let c = p.compute_constraints(Some(&external));
1246 k9::snapshot!(
1247 c.to_human_string(),
1248 "
1249ceilings:
1250 concurrent dispatchers: 32
1251 source: connection_limit
1252 message rate: 100/s
1253 source: scheduled queue max_message_rate
1254 declared: max_message_rate = 1000/s ← effectively unreachable
1255
1256"
1257 );
1258 }
1259
1260 #[test]
1261 fn source_selection_rate() {
1262 let mut p = cfg();
1263 p.source_selection_rate = Some(throttle("5/s"));
1264 assert_eq!(
1265 p.compute_constraints(None),
1266 EffectiveConstraints {
1267 max_concurrent_dispatchers: primary("32"),
1268 max_message_rate: None,
1269 max_message_rate_declared: None,
1270 max_connection_rate: None,
1271 max_source_selection_rate: Some(primary("5/s")),
1272 }
1273 );
1274 }
1275
1276 #[test]
1277 fn render_defaults() {
1278 let c = cfg().compute_constraints(None);
1279 k9::snapshot!(
1280 c.to_human_string(),
1281 "
1282ceilings:
1283 concurrent dispatchers: 32
1284 source: connection_limit
1285
1286"
1287 );
1288 }
1289
1290 #[test]
1291 fn render_primary_message_rate_no_annotation() {
1292 let mut p = cfg();
1295 p.max_message_rate = Some(throttle("1000/s"));
1296 let c = p.compute_constraints(None);
1297 k9::snapshot!(
1298 c.to_human_string(),
1299 "
1300ceilings:
1301 concurrent dispatchers: 32
1302 source: connection_limit
1303 message rate: 1000/s
1304 source: max_message_rate
1305
1306"
1307 );
1308 }
1309
1310 #[test]
1311 fn render_reconnect_cycling_with_annotation() {
1312 let mut p = cfg();
1313 p.max_message_rate = Some(throttle("1000/s"));
1314 p.max_deliveries_per_connection = 10;
1315 p.max_connection_rate = Some(throttle("10/s"));
1316 let c = p.compute_constraints(None);
1317 k9::snapshot!(
1318 c.to_human_string(),
1319 "
1320ceilings:
1321 concurrent dispatchers: 32
1322 source: connection_limit
1323 message rate: 10 × 10/s = 100/s
1324 source: max_deliveries_per_connection × max_connection_rate
1325 declared: max_message_rate = 1000/s ← effectively unreachable
1326 connection rate: 10/s
1327 source: max_connection_rate
1328
1329"
1330 );
1331 }
1332
1333 #[test]
1334 fn render_additional_throttle_winning() {
1335 let mut p = cfg();
1336 p.max_message_rate = Some(throttle("1000/s"));
1337 p.additional_message_rate_throttles
1338 .insert("provider_cap".to_string(), throttle("250/s"));
1339 let c = p.compute_constraints(None);
1340 k9::snapshot!(
1341 c.to_human_string(),
1342 "
1343ceilings:
1344 concurrent dispatchers: 32
1345 source: connection_limit
1346 message rate: 250/s
1347 source: provider_cap
1348 declared: max_message_rate = 1000/s ← effectively unreachable
1349
1350"
1351 );
1352 }
1353}