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