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")]
385 pub dispatcher_progress_watchdog_timeout: Option<Duration>,
386}
387
388#[cfg(feature = "lua")]
389impl LuaUserData for EgressPathConfig {
390 fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
391 config::impl_pairs_and_index(methods);
392 }
393}
394
395impl Default for EgressPathConfig {
396 fn default() -> Self {
397 Self {
398 connection_limit: Self::default_connection_limit(),
399 tls_prefer_openssl: false,
400 enable_tls: Tls::default(),
401 enable_mta_sts: Self::default_enable_mta_sts(),
402 enable_dane: Self::default_enable_dane(),
403 enable_rset: Self::default_enable_rset(),
404 enable_pipelining: Self::default_enable_pipelining(),
405 max_ready: Self::default_max_ready(),
406 consecutive_connection_failures_before_delay:
407 Self::default_consecutive_connection_failures_before_delay(),
408 smtp_port: Self::default_smtp_port(),
409 max_message_rate: None,
410 max_connection_rate: None,
411 max_deliveries_per_connection: Self::default_max_deliveries_per_connection(),
412 max_recipients_per_batch: Self::default_max_recipients_per_batch(),
413 client_timeouts: SmtpClientTimeouts::default(),
414 system_shutdown_timeout: None,
415 prohibited_hosts: CidrSet::default_prohibited_hosts(),
416 skip_hosts: CidrSet::default(),
417 ehlo_domain: None,
418 allow_smtp_auth_plain_without_tls: false,
419 allow_smtp_auth_plain_without_valid_certificate: false,
420 smtp_auth_plain_username: None,
421 smtp_auth_plain_password: None,
422 aggressive_connection_opening: false,
423 rustls_cipher_suites: vec![],
424 tls_certificate: None,
425 tls_private_key: None,
426 openssl_cipher_list: None,
427 openssl_cipher_suites: None,
428 openssl_options: None,
429 refresh_interval: Self::default_refresh_interval(),
430 refresh_strategy: ConfigRefreshStrategy::default(),
431 additional_message_rate_throttles: OrderMap::default(),
432 additional_connection_limits: OrderMap::default(),
433 source_selection_rate: None,
434 additional_source_selection_rates: OrderMap::default(),
435 provider_name: None,
436 remember_broken_tls: None,
437 opportunistic_tls_reconnect_on_failed_handshake: false,
438 use_lmtp: false,
439 reconnect_strategy: ReconnectStrategy::default(),
440 readyq_pool_name: None,
441 low_memory_reduction_policy: MemoryReductionPolicy::default(),
442 no_memory_reduction_policy: MemoryReductionPolicy::default(),
443 maintainer_wakeup_strategy: WakeupStrategy::default(),
444 dispatcher_wakeup_strategy: WakeupStrategy::default(),
445 try_next_host_on_transport_error: false,
446 ignore_8bit_checks: false,
447 ip_lookup_strategy: IpLookupStrategy::default(),
448 dispatcher_progress_watchdog_timeout: None,
449 }
450 }
451}
452
453impl EgressPathConfig {
454 fn default_connection_limit() -> LimitSpec {
455 LimitSpec::new(32)
456 }
457
458 fn default_enable_mta_sts() -> bool {
459 true
460 }
461
462 fn default_enable_pipelining() -> bool {
463 true
464 }
465
466 fn default_enable_rset() -> bool {
467 true
468 }
469
470 fn default_enable_dane() -> bool {
471 false
472 }
473
474 fn default_max_ready() -> usize {
475 1024
476 }
477
478 fn default_consecutive_connection_failures_before_delay() -> usize {
479 100
480 }
481
482 fn default_smtp_port() -> u16 {
483 25
484 }
485
486 fn default_max_deliveries_per_connection() -> usize {
487 1024
488 }
489
490 fn default_max_recipients_per_batch() -> usize {
491 100
492 }
493
494 fn default_refresh_interval() -> Duration {
495 Duration::from_secs(60)
496 }
497
498 pub fn compute_constraints(
508 &self,
509 additional: Option<&EffectiveConstraints>,
510 ) -> EffectiveConstraints {
511 let mut constraints = self.compute_path_constraints();
512 if let Some(extra) = additional {
513 constraints.merge(extra);
514 }
515 constraints
516 }
517
518 fn compute_path_constraints(&self) -> EffectiveConstraints {
519 let max_concurrent_dispatchers = {
520 let mut best = EffectiveCeiling {
521 value: self.connection_limit.limit as f64,
522 source: CeilingSource::Primary,
523 display: self.connection_limit.limit.to_string(),
524 };
525 for (name, spec) in &self.additional_connection_limits {
526 let value = spec.limit as f64;
527 if value < best.value {
528 best = EffectiveCeiling {
529 value,
530 source: CeilingSource::Additional { name: name.clone() },
531 display: spec.limit.to_string(),
532 };
533 }
534 }
535 best
536 };
537
538 let mut msg_candidates: Vec<EffectiveCeiling> = vec![];
541 if let Some(spec) = &self.max_message_rate {
542 msg_candidates.push(throttle_ceiling(spec, CeilingSource::Primary));
543 }
544 for (name, spec) in &self.additional_message_rate_throttles {
545 msg_candidates.push(throttle_ceiling(
546 spec,
547 CeilingSource::Additional { name: name.clone() },
548 ));
549 }
550 if let Some(spec) = &self.max_connection_rate {
551 let k = self.max_deliveries_per_connection as u64;
559 let total = ThrottleSpec {
560 limit: k.saturating_mul(spec.limit),
561 period: spec.period,
562 max_burst: None,
563 force_local: false,
564 };
565 msg_candidates.push(EffectiveCeiling {
566 value: throttle_rate_per_sec(spec) * k as f64,
567 source: CeilingSource::ReconnectCycling,
568 display: format!("{k} × {spec} = {total}"),
569 });
570 }
571 let max_message_rate = msg_candidates
575 .iter()
576 .min_by(|a, b| a.value.total_cmp(&b.value))
577 .cloned();
578
579 let max_message_rate_declared = match (&max_message_rate, &self.max_message_rate) {
586 (Some(ceiling), Some(declared))
587 if !matches!(ceiling.source, CeilingSource::Primary) =>
588 {
589 Some(declared.to_string())
590 }
591 _ => None,
592 };
593
594 let max_connection_rate = self
595 .max_connection_rate
596 .as_ref()
597 .map(|spec| throttle_ceiling(spec, CeilingSource::Primary));
598
599 let max_source_selection_rate = {
600 let mut candidates: Vec<EffectiveCeiling> = vec![];
601 if let Some(spec) = &self.source_selection_rate {
602 candidates.push(throttle_ceiling(spec, CeilingSource::Primary));
603 }
604 for (name, spec) in &self.additional_source_selection_rates {
605 candidates.push(throttle_ceiling(
606 spec,
607 CeilingSource::Additional { name: name.clone() },
608 ));
609 }
610 candidates
611 .into_iter()
612 .min_by(|a, b| a.value.total_cmp(&b.value))
613 };
614
615 EffectiveConstraints {
616 max_concurrent_dispatchers,
617 max_message_rate,
618 max_message_rate_declared,
619 max_connection_rate,
620 max_source_selection_rate,
621 }
622 }
623}
624
625fn throttle_rate_per_sec(spec: &ThrottleSpec) -> f64 {
626 spec.limit as f64 / spec.period as f64
627}
628
629fn throttle_ceiling(spec: &ThrottleSpec, source: CeilingSource) -> EffectiveCeiling {
630 EffectiveCeiling {
631 value: throttle_rate_per_sec(spec),
632 source,
633 display: spec.to_string(),
634 }
635}
636
637#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
642pub struct EffectiveCeiling {
643 pub value: f64,
646 pub source: CeilingSource,
647 pub display: String,
654}
655
656#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
660#[serde(tag = "kind", rename_all = "snake_case")]
661pub enum CeilingSource {
662 Primary,
666 Additional { name: String },
668 ReconnectCycling,
676 Other { name: String },
681}
682
683#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
687pub struct MxResolution {
688 pub site_name: String,
691
692 pub by_preference: BTreeMap<u16, Vec<String>>,
696
697 pub is_mx: bool,
700
701 pub is_domain_literal: bool,
703
704 pub is_secure: bool,
706}
707
708impl From<&MailExchanger> for MxResolution {
709 fn from(mx: &MailExchanger) -> Self {
710 Self {
711 site_name: mx.site_name.clone(),
712 by_preference: mx.by_pref.clone(),
713 is_mx: mx.is_mx,
714 is_domain_literal: mx.is_domain_literal,
715 is_secure: mx.is_secure,
716 }
717 }
718}
719
720impl MxResolution {
721 pub fn render(&self, out: &mut dyn Write) -> std::fmt::Result {
726 let mut flags: Vec<&str> = vec![];
727 if !self.is_mx {
728 flags.push("synthesised");
729 }
730 if self.is_domain_literal {
731 flags.push("domain literal");
732 }
733 if self.is_secure {
734 flags.push("dnssec verified");
735 }
736 write!(out, "mx (site: {}", self.site_name)?;
737 if !flags.is_empty() {
738 write!(out, ", {}", flags.join(", "))?;
739 }
740 writeln!(out, "):")?;
741 for (pref, hosts) in &self.by_preference {
742 for host in hosts {
743 writeln!(out, " {pref:>4} {host}")?;
744 }
745 }
746 Ok(())
747 }
748
749 pub fn to_human_string(&self) -> String {
750 let mut s = String::new();
751 let _ = self.render(&mut s);
752 s
753 }
754}
755
756#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
765pub struct EffectiveConstraints {
766 pub max_concurrent_dispatchers: EffectiveCeiling,
767 pub max_message_rate: Option<EffectiveCeiling>,
771 pub max_message_rate_declared: Option<String>,
776 pub max_connection_rate: Option<EffectiveCeiling>,
777 pub max_source_selection_rate: Option<EffectiveCeiling>,
778}
779
780impl EffectiveConstraints {
781 pub fn render(&self, out: &mut dyn Write) -> std::fmt::Result {
786 let label_axis = |w: &mut dyn Write, label: &str, value: &str| -> std::fmt::Result {
787 writeln!(w, " {label:<24}{value}")
788 };
789 let label_source =
790 |w: &mut dyn Write, src: &CeilingSource, primary: &str| -> std::fmt::Result {
791 let s = match src {
792 CeilingSource::Primary => primary,
793 CeilingSource::Additional { name } => name.as_str(),
794 CeilingSource::ReconnectCycling => {
795 "max_deliveries_per_connection × max_connection_rate"
796 }
797 CeilingSource::Other { name } => name.as_str(),
798 };
799 writeln!(w, " source: {s}")
800 };
801
802 writeln!(out, "ceilings:")?;
803
804 label_axis(
805 out,
806 "concurrent dispatchers:",
807 &self.max_concurrent_dispatchers.display,
808 )?;
809 label_source(
810 out,
811 &self.max_concurrent_dispatchers.source,
812 "connection_limit",
813 )?;
814
815 if let Some(c) = &self.max_message_rate {
816 label_axis(out, "message rate:", &c.display)?;
817 label_source(out, &c.source, "max_message_rate")?;
818 if let Some(declared) = &self.max_message_rate_declared {
819 writeln!(
820 out,
821 " declared: max_message_rate = {declared} ← effectively unreachable"
822 )?;
823 }
824 }
825
826 if let Some(c) = &self.max_connection_rate {
827 label_axis(out, "connection rate:", &c.display)?;
828 label_source(out, &c.source, "max_connection_rate")?;
829 }
830
831 if let Some(c) = &self.max_source_selection_rate {
832 label_axis(out, "source selection rate:", &c.display)?;
833 label_source(out, &c.source, "source_selection_rate")?;
834 }
835
836 Ok(())
837 }
838
839 pub fn to_human_string(&self) -> String {
840 let mut s = String::new();
841 let _ = self.render(&mut s);
844 s
845 }
846
847 pub fn merge(&mut self, other: &EffectiveConstraints) {
854 let path_declared = self
858 .max_message_rate
859 .as_ref()
860 .filter(|c| matches!(c.source, CeilingSource::Primary))
861 .map(|c| c.display.clone())
862 .or_else(|| self.max_message_rate_declared.clone());
863
864 merge_axis(
865 &mut self.max_concurrent_dispatchers,
866 &other.max_concurrent_dispatchers,
867 );
868 merge_optional(&mut self.max_message_rate, &other.max_message_rate);
869 merge_optional(&mut self.max_connection_rate, &other.max_connection_rate);
870 merge_optional(
871 &mut self.max_source_selection_rate,
872 &other.max_source_selection_rate,
873 );
874
875 if let Some(merged) = &self.max_message_rate {
876 if !matches!(merged.source, CeilingSource::Primary) {
877 if let Some(declared) = path_declared {
878 self.max_message_rate_declared = Some(declared);
879 }
880 }
881 }
882 }
883}
884
885fn merge_axis(a: &mut EffectiveCeiling, b: &EffectiveCeiling) {
886 if b.value.total_cmp(&a.value).is_lt() {
887 *a = b.clone();
888 }
889}
890
891fn merge_optional(a: &mut Option<EffectiveCeiling>, b: &Option<EffectiveCeiling>) {
892 match (a.as_mut(), b) {
893 (Some(av), Some(bv)) if bv.value.total_cmp(&av.value).is_lt() => *av = bv.clone(),
894 (None, Some(bv)) => *a = Some(bv.clone()),
895 _ => {}
896 }
897}
898
899#[cfg(test)]
900mod constraints_tests {
901 use super::*;
902
903 fn cfg() -> EgressPathConfig {
904 EgressPathConfig::default()
905 }
906
907 fn value_from_display(display: &str) -> f64 {
915 if let Some((_, total)) = display.split_once(" = ") {
916 let spec = ThrottleSpec::try_from(total).unwrap();
917 return spec.limit as f64 / spec.period as f64;
918 }
919 if let Ok(spec) = ThrottleSpec::try_from(display) {
920 return spec.limit as f64 / spec.period as f64;
921 }
922 display.parse::<u64>().unwrap() as f64
923 }
924
925 fn primary(display: &str) -> EffectiveCeiling {
926 EffectiveCeiling {
927 value: value_from_display(display),
928 source: CeilingSource::Primary,
929 display: display.to_string(),
930 }
931 }
932
933 fn additional(name: &str, display: &str) -> EffectiveCeiling {
934 EffectiveCeiling {
935 value: value_from_display(display),
936 source: CeilingSource::Additional {
937 name: name.to_string(),
938 },
939 display: display.to_string(),
940 }
941 }
942
943 fn other(name: &str, display: &str) -> EffectiveCeiling {
944 EffectiveCeiling {
945 value: value_from_display(display),
946 source: CeilingSource::Other {
947 name: name.to_string(),
948 },
949 display: display.to_string(),
950 }
951 }
952
953 fn reconnect_cycling(display: &str) -> EffectiveCeiling {
954 EffectiveCeiling {
955 value: value_from_display(display),
956 source: CeilingSource::ReconnectCycling,
957 display: display.to_string(),
958 }
959 }
960
961 fn throttle(s: &str) -> ThrottleSpec {
962 ThrottleSpec::try_from(s).unwrap()
963 }
964
965 #[test]
966 fn rust_compact_round_trips() {
967 let original = EgressPathConfig::default();
972 let s = mod_serde::toml_encode_pretty_compact(&original).unwrap();
973 let parsed: EgressPathConfig = toml::from_str(&s).unwrap();
974 assert_eq!(parsed, original);
975 }
976
977 #[test]
978 fn defaults_only_concurrency() {
979 assert_eq!(
980 cfg().compute_constraints(None),
981 EffectiveConstraints {
982 max_concurrent_dispatchers: primary("32"),
983 max_message_rate: None,
984 max_message_rate_declared: None,
985 max_connection_rate: None,
986 max_source_selection_rate: None,
987 }
988 );
989 }
990
991 #[test]
992 fn additional_connection_limit_wins() {
993 let mut p = cfg();
994 p.additional_connection_limits
995 .insert("provider_shared".to_string(), LimitSpec::new(10));
996 assert_eq!(
997 p.compute_constraints(None),
998 EffectiveConstraints {
999 max_concurrent_dispatchers: additional("provider_shared", "10"),
1000 max_message_rate: None,
1001 max_message_rate_declared: None,
1002 max_connection_rate: None,
1003 max_source_selection_rate: None,
1004 }
1005 );
1006 }
1007
1008 #[test]
1009 fn primary_connection_limit_wins_when_smaller() {
1010 let mut p = cfg();
1011 p.connection_limit = LimitSpec::new(4);
1012 p.additional_connection_limits
1013 .insert("large_pool".to_string(), LimitSpec::new(100));
1014 assert_eq!(
1015 p.compute_constraints(None),
1016 EffectiveConstraints {
1017 max_concurrent_dispatchers: primary("4"),
1018 max_message_rate: None,
1019 max_message_rate_declared: None,
1020 max_connection_rate: None,
1021 max_source_selection_rate: None,
1022 }
1023 );
1024 }
1025
1026 #[test]
1027 fn primary_message_rate_only() {
1028 let mut p = cfg();
1029 p.max_message_rate = Some(throttle("1000/s"));
1030 assert_eq!(
1031 p.compute_constraints(None),
1032 EffectiveConstraints {
1033 max_concurrent_dispatchers: primary("32"),
1034 max_message_rate: Some(primary("1000/s")),
1035 max_message_rate_declared: None,
1036 max_connection_rate: None,
1037 max_source_selection_rate: None,
1038 }
1039 );
1040 }
1041
1042 #[test]
1043 fn additional_message_rate_wins() {
1044 let mut p = cfg();
1045 p.max_message_rate = Some(throttle("1000/s"));
1046 p.additional_message_rate_throttles
1047 .insert("provider_cap".to_string(), throttle("250/s"));
1048 assert_eq!(
1049 p.compute_constraints(None),
1050 EffectiveConstraints {
1051 max_concurrent_dispatchers: primary("32"),
1052 max_message_rate: Some(additional("provider_cap", "250/s")),
1053 max_message_rate_declared: Some("1000/s".to_string()),
1054 max_connection_rate: None,
1055 max_source_selection_rate: None,
1056 }
1057 );
1058 }
1059
1060 #[test]
1061 fn additional_message_rate_throttles_mixed_periods() {
1062 let mut p = cfg();
1067 p.additional_message_rate_throttles
1068 .insert("per_hour".to_string(), throttle("10/hr"));
1069 p.additional_message_rate_throttles
1070 .insert("per_minute".to_string(), throttle("8/min"));
1071 p.additional_message_rate_throttles
1072 .insert("per_second".to_string(), throttle("5/s"));
1073 assert_eq!(
1074 p.compute_constraints(None),
1075 EffectiveConstraints {
1076 max_concurrent_dispatchers: primary("32"),
1077 max_message_rate: Some(additional("per_hour", "10/h")),
1078 max_message_rate_declared: None,
1079 max_connection_rate: None,
1080 max_source_selection_rate: None,
1081 }
1082 );
1083 }
1084
1085 #[test]
1086 fn hourly_rate_preserves_units() {
1087 let mut p = cfg();
1092 p.max_message_rate = Some(throttle("10000/hr"));
1093 assert_eq!(
1094 p.compute_constraints(None),
1095 EffectiveConstraints {
1096 max_concurrent_dispatchers: primary("32"),
1097 max_message_rate: Some(primary("10000/h")),
1098 max_message_rate_declared: None,
1099 max_connection_rate: None,
1100 max_source_selection_rate: None,
1101 }
1102 );
1103 }
1104
1105 #[test]
1106 fn reconnect_cycling_wins() {
1107 let mut p = cfg();
1110 p.max_message_rate = Some(throttle("1000/s"));
1111 p.max_deliveries_per_connection = 10;
1112 p.max_connection_rate = Some(throttle("10/s"));
1113 assert_eq!(
1114 p.compute_constraints(None),
1115 EffectiveConstraints {
1116 max_concurrent_dispatchers: primary("32"),
1117 max_message_rate: Some(reconnect_cycling("10 × 10/s = 100/s")),
1118 max_message_rate_declared: Some("1000/s".to_string()),
1119 max_connection_rate: Some(primary("10/s")),
1120 max_source_selection_rate: None,
1121 }
1122 );
1123 }
1124
1125 #[test]
1126 fn reconnect_cycling_does_not_bind_with_large_k() {
1127 let mut p = cfg();
1131 p.max_message_rate = Some(throttle("1000/s"));
1132 p.max_connection_rate = Some(throttle("10/s"));
1133 assert_eq!(
1134 p.compute_constraints(None),
1135 EffectiveConstraints {
1136 max_concurrent_dispatchers: primary("32"),
1137 max_message_rate: Some(primary("1000/s")),
1138 max_message_rate_declared: None,
1139 max_connection_rate: Some(primary("10/s")),
1140 max_source_selection_rate: None,
1141 }
1142 );
1143 }
1144
1145 #[test]
1146 fn reconnect_cycling_alone_sets_message_rate() {
1147 let mut p = cfg();
1151 p.max_deliveries_per_connection = 5;
1152 p.max_connection_rate = Some(throttle("2/s"));
1153 assert_eq!(
1154 p.compute_constraints(None),
1155 EffectiveConstraints {
1156 max_concurrent_dispatchers: primary("32"),
1157 max_message_rate: Some(reconnect_cycling("5 × 2/s = 10/s")),
1158 max_message_rate_declared: None,
1159 max_connection_rate: Some(primary("2/s")),
1160 max_source_selection_rate: None,
1161 }
1162 );
1163 }
1164
1165 #[test]
1166 fn merge_external_message_rate_wins() {
1167 let mut p = cfg();
1172 p.max_message_rate = Some(throttle("1000/s"));
1173 let external = EffectiveConstraints {
1174 max_concurrent_dispatchers: EffectiveCeiling {
1175 value: f64::INFINITY,
1176 source: CeilingSource::Other {
1177 name: "queue config".to_string(),
1178 },
1179 display: "∞".to_string(),
1180 },
1181 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1182 max_message_rate_declared: None,
1183 max_connection_rate: None,
1184 max_source_selection_rate: None,
1185 };
1186 assert_eq!(
1187 p.compute_constraints(Some(&external)),
1188 EffectiveConstraints {
1189 max_concurrent_dispatchers: primary("32"),
1190 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1191 max_message_rate_declared: Some("1000/s".to_string()),
1192 max_connection_rate: None,
1193 max_source_selection_rate: None,
1194 }
1195 );
1196 }
1197
1198 #[test]
1199 fn merge_external_does_not_bind() {
1200 let mut p = cfg();
1203 p.max_message_rate = Some(throttle("100/s"));
1204 let external = EffectiveConstraints {
1205 max_concurrent_dispatchers: EffectiveCeiling {
1206 value: f64::INFINITY,
1207 source: CeilingSource::Other {
1208 name: "queue config".to_string(),
1209 },
1210 display: "∞".to_string(),
1211 },
1212 max_message_rate: Some(other("scheduled queue max_message_rate", "1000/s")),
1213 max_message_rate_declared: None,
1214 max_connection_rate: None,
1215 max_source_selection_rate: None,
1216 };
1217 assert_eq!(
1218 p.compute_constraints(Some(&external)),
1219 EffectiveConstraints {
1220 max_concurrent_dispatchers: primary("32"),
1221 max_message_rate: Some(primary("100/s")),
1222 max_message_rate_declared: None,
1223 max_connection_rate: None,
1224 max_source_selection_rate: None,
1225 }
1226 );
1227 }
1228
1229 #[test]
1230 fn render_external_wins_with_annotation() {
1231 let mut p = cfg();
1232 p.max_message_rate = Some(throttle("1000/s"));
1233 let external = EffectiveConstraints {
1234 max_concurrent_dispatchers: EffectiveCeiling {
1235 value: f64::INFINITY,
1236 source: CeilingSource::Other {
1237 name: "queue config".to_string(),
1238 },
1239 display: "∞".to_string(),
1240 },
1241 max_message_rate: Some(other("scheduled queue max_message_rate", "100/s")),
1242 max_message_rate_declared: None,
1243 max_connection_rate: None,
1244 max_source_selection_rate: None,
1245 };
1246 let c = p.compute_constraints(Some(&external));
1247 k9::snapshot!(
1248 c.to_human_string(),
1249 "
1250ceilings:
1251 concurrent dispatchers: 32
1252 source: connection_limit
1253 message rate: 100/s
1254 source: scheduled queue max_message_rate
1255 declared: max_message_rate = 1000/s ← effectively unreachable
1256
1257"
1258 );
1259 }
1260
1261 #[test]
1262 fn source_selection_rate() {
1263 let mut p = cfg();
1264 p.source_selection_rate = Some(throttle("5/s"));
1265 assert_eq!(
1266 p.compute_constraints(None),
1267 EffectiveConstraints {
1268 max_concurrent_dispatchers: primary("32"),
1269 max_message_rate: None,
1270 max_message_rate_declared: None,
1271 max_connection_rate: None,
1272 max_source_selection_rate: Some(primary("5/s")),
1273 }
1274 );
1275 }
1276
1277 #[test]
1278 fn render_defaults() {
1279 let c = cfg().compute_constraints(None);
1280 k9::snapshot!(
1281 c.to_human_string(),
1282 "
1283ceilings:
1284 concurrent dispatchers: 32
1285 source: connection_limit
1286
1287"
1288 );
1289 }
1290
1291 #[test]
1292 fn render_primary_message_rate_no_annotation() {
1293 let mut p = cfg();
1296 p.max_message_rate = Some(throttle("1000/s"));
1297 let c = p.compute_constraints(None);
1298 k9::snapshot!(
1299 c.to_human_string(),
1300 "
1301ceilings:
1302 concurrent dispatchers: 32
1303 source: connection_limit
1304 message rate: 1000/s
1305 source: max_message_rate
1306
1307"
1308 );
1309 }
1310
1311 #[test]
1312 fn render_reconnect_cycling_with_annotation() {
1313 let mut p = cfg();
1314 p.max_message_rate = Some(throttle("1000/s"));
1315 p.max_deliveries_per_connection = 10;
1316 p.max_connection_rate = Some(throttle("10/s"));
1317 let c = p.compute_constraints(None);
1318 k9::snapshot!(
1319 c.to_human_string(),
1320 "
1321ceilings:
1322 concurrent dispatchers: 32
1323 source: connection_limit
1324 message rate: 10 × 10/s = 100/s
1325 source: max_deliveries_per_connection × max_connection_rate
1326 declared: max_message_rate = 1000/s ← effectively unreachable
1327 connection rate: 10/s
1328 source: max_connection_rate
1329
1330"
1331 );
1332 }
1333
1334 #[test]
1335 fn render_additional_throttle_winning() {
1336 let mut p = cfg();
1337 p.max_message_rate = Some(throttle("1000/s"));
1338 p.additional_message_rate_throttles
1339 .insert("provider_cap".to_string(), throttle("250/s"));
1340 let c = p.compute_constraints(None);
1341 k9::snapshot!(
1342 c.to_human_string(),
1343 "
1344ceilings:
1345 concurrent dispatchers: 32
1346 source: connection_limit
1347 message rate: 250/s
1348 source: provider_cap
1349 declared: max_message_rate = 1000/s ← effectively unreachable
1350
1351"
1352 );
1353 }
1354}