1use std::collections::BTreeMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11use serde::Deserialize;
12
13use crate::api::unified::{
14 Config, MediaMode, MediaSessionControllerConfig, RtpSessionBufferConfig,
15 RtpTransportBufferConfig,
16};
17use crate::errors::{Result, SessionError};
18
19const DEFAULT_RECIPE_BOOK: &str = include_str!("../../config/performance-recipes.yaml");
20
21#[derive(Debug, Clone, Deserialize)]
27#[serde(rename_all = "camelCase")]
28pub struct PerformanceConfig {
29 pub profile: String,
31 pub capacity: Option<usize>,
33 pub signaling_only_rtp_port: Option<u16>,
35 pub recipe_path: Option<PathBuf>,
37}
38
39impl PerformanceConfig {
40 pub fn profile(profile: impl Into<String>) -> Self {
42 Self {
43 profile: profile.into(),
44 capacity: None,
45 signaling_only_rtp_port: None,
46 recipe_path: None,
47 }
48 }
49
50 pub fn endpoint() -> Self {
52 Self::profile("endpoint")
53 }
54
55 pub fn pbx_media_server(capacity: usize) -> Self {
57 Self::profile("pbx-media-server").with_capacity(capacity)
58 }
59
60 pub fn signaling_only_server_high_performance(capacity: usize) -> Self {
62 Self::profile("signaling-only-server-high-performance")
63 .with_capacity(capacity)
64 .with_signaling_only_rtp_port(9)
65 }
66
67 pub fn with_capacity(mut self, capacity: usize) -> Self {
69 self.capacity = Some(capacity);
70 self
71 }
72
73 pub fn with_signaling_only_rtp_port(mut self, port: u16) -> Self {
75 self.signaling_only_rtp_port = Some(port);
76 self
77 }
78
79 pub fn with_recipe_path(mut self, path: impl Into<PathBuf>) -> Self {
81 self.recipe_path = Some(path.into());
82 self
83 }
84}
85
86#[derive(Debug, Clone, Deserialize)]
88#[serde(rename_all = "camelCase")]
89pub struct PerformanceRecipeBook {
90 pub version: u32,
92 pub performance_profiles: BTreeMap<String, PerformanceRecipe>,
94}
95
96impl PerformanceRecipeBook {
97 pub fn bundled() -> Result<Self> {
99 Self::from_yaml_str(DEFAULT_RECIPE_BOOK, "bundled default performance recipes")
100 }
101
102 pub fn from_path(path: impl AsRef<Path>) -> Result<Self> {
104 let path = path.as_ref();
105 let text = fs::read_to_string(path).map_err(|e| {
106 SessionError::ConfigError(format!(
107 "failed to read performance recipe file '{}': {e}",
108 path.display()
109 ))
110 })?;
111 Self::from_yaml_str(&text, &path.display().to_string())
112 }
113
114 pub fn from_yaml_str(text: &str, source: &str) -> Result<Self> {
116 let book: Self = serde_yaml::from_str(text).map_err(|e| {
117 SessionError::ConfigError(format!(
118 "failed to parse performance recipe book {source}: {e}"
119 ))
120 })?;
121 if book.version != 1 {
122 return Err(SessionError::ConfigError(format!(
123 "unsupported performance recipe book version {}; expected 1",
124 book.version
125 )));
126 }
127 Ok(book)
128 }
129
130 pub fn apply(&self, config: Config, performance: &PerformanceConfig) -> Result<Config> {
132 let recipe = self
133 .performance_profiles
134 .get(&performance.profile)
135 .ok_or_else(|| {
136 let mut names = self
137 .performance_profiles
138 .keys()
139 .cloned()
140 .collect::<Vec<_>>();
141 names.sort();
142 SessionError::ConfigError(format!(
143 "unknown performance profile '{}'; available profiles: {}",
144 performance.profile,
145 names.join(", ")
146 ))
147 })?;
148 recipe.apply(config, performance)
149 }
150}
151
152#[derive(Debug, Clone, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct PerformanceRecipe {
156 pub description: Option<String>,
158 pub requires_capacity: Option<bool>,
160 pub config: PerformanceRecipeConfig,
162}
163
164impl PerformanceRecipe {
165 fn apply(&self, mut config: Config, performance: &PerformanceConfig) -> Result<Config> {
166 if self.requires_capacity.unwrap_or(false) {
167 let capacity = performance.capacity.ok_or_else(|| {
168 SessionError::ConfigError(format!(
169 "performance profile '{}' requires capacity",
170 performance.profile
171 ))
172 })?;
173 if capacity == 0 {
174 return Err(SessionError::ConfigError(
175 "performance capacity must be at least 1".to_string(),
176 ));
177 }
178 }
179
180 let params = RecipeParams {
181 capacity: performance.capacity,
182 signaling_only_rtp_port: performance.signaling_only_rtp_port,
183 };
184 self.config.apply(&mut config, ¶ms)?;
185 config.validate()?;
186 Ok(config)
187 }
188}
189
190#[derive(Debug, Clone, Default, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct PerformanceRecipeConfig {
194 pub channel_capacity: Option<RecipeUsize>,
196 pub auto_180_ringing: Option<bool>,
198 pub auto_100_trying: Option<bool>,
200 pub fast_auto_accept_incoming_calls: Option<bool>,
202 pub sip_udp_diagnostics: Option<bool>,
204 pub sip_transaction_timing_diagnostics: Option<bool>,
206 pub sip_dialog_timing_diagnostics: Option<bool>,
208 pub media_setup_diagnostics: Option<bool>,
210 pub cleanup_diagnostics: Option<bool>,
212 pub cleanup_diagnostic_events: Option<bool>,
214 pub active_call_no_media_timeout_secs: Option<u64>,
216 pub active_call_media_idle_timeout_secs: Option<u64>,
218 pub sip_udp_recv_buffer_size: Option<usize>,
220 pub sip_udp_send_buffer_size: Option<usize>,
222 pub sip_udp_parse_workers: Option<usize>,
224 pub sip_udp_parse_queue_capacity: Option<RecipeUsize>,
226 pub sip_udp_parse_dispatch: Option<RecipeUdpParseDispatch>,
228 pub sip_transport_dispatch_workers: Option<usize>,
230 pub sip_transport_dispatch_queue_capacity: Option<RecipeUsize>,
232 pub sip_transaction_dispatch_workers: Option<usize>,
234 pub sip_transaction_dispatch_queue_capacity: Option<RecipeUsize>,
236 pub sip_transaction_dispatch_priority_burst_max: Option<usize>,
238 pub sip_invite_2xx_retransmit_max_due_per_tick: Option<usize>,
240 pub sip_transaction_command_channel_capacity: Option<usize>,
242 pub sip_dialog_dispatch_workers: Option<usize>,
244 pub sip_dialog_dispatch_queue_capacity: Option<RecipeUsize>,
246 pub session_event_dispatcher_workers: Option<usize>,
248 pub session_event_dispatcher_channel_capacity: Option<RecipeUsize>,
250 pub media_mode: Option<RecipeMediaMode>,
252 pub signaling_only_rtp_port: Option<RecipeU16>,
254 pub media_port_capacity: Option<RecipeMediaPortCapacity>,
256 pub media_session_capacity: Option<RecipeUsize>,
258 pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
260 pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
262 pub media_session_controller_config: Option<RecipeMediaSessionControllerConfig>,
264 pub server_call_capacity: Option<RecipeUsize>,
266 pub server_call_admission_limit: Option<RecipeUsize>,
268 pub server_call_admission_soft_limit: Option<RecipeUsize>,
270 pub server_call_admission_pacing_delay_ms: Option<u64>,
272 pub server_overload_retry_after_secs: Option<u32>,
274}
275
276impl PerformanceRecipeConfig {
277 fn apply(&self, config: &mut Config, params: &RecipeParams) -> Result<()> {
278 if let Some(capacity) = &self.channel_capacity {
279 *config = config
280 .clone()
281 .with_channel_capacity(capacity.resolve(params, "channelCapacity")?);
282 }
283 if let Some(enabled) = self.auto_180_ringing {
284 config.auto_180_ringing = enabled;
285 }
286 if let Some(enabled) = self.auto_100_trying {
287 config.auto_100_trying = enabled;
288 }
289 if let Some(enabled) = self.fast_auto_accept_incoming_calls {
290 config.fast_auto_accept_incoming_calls = enabled;
291 }
292 if let Some(enabled) = self.sip_udp_diagnostics {
293 config.sip_udp_diagnostics = enabled;
294 }
295 if let Some(enabled) = self.sip_transaction_timing_diagnostics {
296 config.sip_transaction_timing_diagnostics = enabled;
297 }
298 if let Some(enabled) = self.sip_dialog_timing_diagnostics {
299 config.sip_dialog_timing_diagnostics = enabled;
300 }
301 if let Some(enabled) = self.media_setup_diagnostics {
302 config.media_setup_diagnostics = enabled;
303 }
304 if let Some(enabled) = self.cleanup_diagnostics {
305 config.cleanup_diagnostics = enabled;
306 }
307 if let Some(enabled) = self.cleanup_diagnostic_events {
308 config.cleanup_diagnostic_events = enabled;
309 }
310 if let Some(seconds) = self.active_call_no_media_timeout_secs {
311 config.active_call_no_media_timeout_secs = seconds;
312 }
313 if let Some(seconds) = self.active_call_media_idle_timeout_secs {
314 config.active_call_media_idle_timeout_secs = seconds;
315 }
316 if let Some(size) = self.sip_udp_recv_buffer_size {
317 config.sip_udp_recv_buffer_size = Some(size);
318 }
319 if let Some(size) = self.sip_udp_send_buffer_size {
320 config.sip_udp_send_buffer_size = Some(size);
321 }
322 if let Some(workers) = self.sip_udp_parse_workers {
323 config.sip_udp_parse_workers = Some(workers);
324 }
325 if let Some(capacity) = &self.sip_udp_parse_queue_capacity {
326 config.sip_udp_parse_queue_capacity =
327 Some(capacity.resolve(params, "sipUdpParseQueueCapacity")?);
328 }
329 if let Some(dispatch) = self.sip_udp_parse_dispatch {
330 config.sip_udp_parse_dispatch = Some(dispatch.into());
331 }
332 if let Some(workers) = self.sip_transport_dispatch_workers {
333 config.sip_transport_dispatch_workers = Some(workers);
334 }
335 if let Some(capacity) = &self.sip_transport_dispatch_queue_capacity {
336 config.sip_transport_dispatch_queue_capacity =
337 Some(capacity.resolve(params, "sipTransportDispatchQueueCapacity")?);
338 }
339 if let Some(workers) = self.sip_transaction_dispatch_workers {
340 config.sip_transaction_dispatch_workers = Some(workers);
341 }
342 if let Some(capacity) = &self.sip_transaction_dispatch_queue_capacity {
343 config.sip_transaction_dispatch_queue_capacity =
344 Some(capacity.resolve(params, "sipTransactionDispatchQueueCapacity")?);
345 }
346 if let Some(max_burst) = self.sip_transaction_dispatch_priority_burst_max {
347 config.sip_transaction_dispatch_priority_burst_max = Some(max_burst);
348 }
349 if let Some(max_due_per_tick) = self.sip_invite_2xx_retransmit_max_due_per_tick {
350 config.sip_invite_2xx_retransmit_max_due_per_tick = Some(max_due_per_tick);
351 }
352 if let Some(capacity) = self.sip_transaction_command_channel_capacity {
353 config.sip_transaction_command_channel_capacity = Some(capacity);
354 }
355 if let Some(workers) = self.sip_dialog_dispatch_workers {
356 config.sip_dialog_dispatch_workers = Some(workers);
357 }
358 if let Some(capacity) = &self.sip_dialog_dispatch_queue_capacity {
359 config.sip_dialog_dispatch_queue_capacity =
360 Some(capacity.resolve(params, "sipDialogDispatchQueueCapacity")?);
361 }
362 if let Some(workers) = self.session_event_dispatcher_workers {
363 config.session_event_dispatcher_workers = workers;
364 }
365 if let Some(capacity) = &self.session_event_dispatcher_channel_capacity {
366 config.session_event_dispatcher_channel_capacity =
367 capacity.resolve(params, "sessionEventDispatcherChannelCapacity")?;
368 }
369 if let Some(capacity) = &self.server_call_capacity {
370 config.server_call_capacity = Some(capacity.resolve(params, "serverCallCapacity")?);
371 }
372 if let Some(limit) = &self.server_call_admission_limit {
373 config.server_call_admission_limit =
374 Some(limit.resolve(params, "serverCallAdmissionLimit")?);
375 }
376 if let Some(limit) = &self.server_call_admission_soft_limit {
377 config.server_call_admission_soft_limit =
378 Some(limit.resolve(params, "serverCallAdmissionSoftLimit")?);
379 }
380 if let Some(delay_ms) = self.server_call_admission_pacing_delay_ms {
381 config.server_call_admission_pacing_delay_ms = Some(delay_ms);
382 }
383 if let Some(seconds) = self.server_overload_retry_after_secs {
384 config.server_overload_retry_after_secs = Some(seconds);
385 }
386 if let Some(port_capacity) = &self.media_port_capacity {
387 *config = config.clone().with_media_port_capacity(
388 port_capacity.start,
389 port_capacity
390 .capacity
391 .resolve(params, "mediaPortCapacity.capacity")?,
392 );
393 }
394 if let Some(capacity) = &self.media_session_capacity {
395 config.media_session_capacity = Some(capacity.resolve(params, "mediaSessionCapacity")?);
396 }
397 if let Some(recipe_config) = &self.media_session_controller_config {
398 let mut controller_config = config.media_session_controller_config.clone();
399 recipe_config.apply(&mut controller_config, params)?;
400 config.media_session_controller_config = controller_config;
401 config.rtp_session_buffer_config = config
402 .media_session_controller_config
403 .rtp_session_buffer_config;
404 config.rtp_transport_buffer_config = config
405 .media_session_controller_config
406 .rtp_transport_buffer_config;
407 }
408 if let Some(recipe_config) = &self.rtp_session_buffer_config {
409 let mut rtp_config = config.rtp_session_buffer_config;
410 recipe_config.apply(&mut rtp_config, params)?;
411 config.rtp_session_buffer_config = rtp_config;
412 config
413 .media_session_controller_config
414 .rtp_session_buffer_config = rtp_config;
415 }
416 if let Some(recipe_config) = &self.rtp_transport_buffer_config {
417 let mut rtp_config = config.rtp_transport_buffer_config;
418 recipe_config.apply(&mut rtp_config, params)?;
419 config.rtp_transport_buffer_config = rtp_config;
420 config
421 .media_session_controller_config
422 .rtp_transport_buffer_config = rtp_config;
423 }
424 if let Some(mode) = self.media_mode {
425 config.media_mode = match mode {
426 RecipeMediaMode::Enabled => MediaMode::Enabled,
427 RecipeMediaMode::SignalingOnly => {
428 let port = self
429 .signaling_only_rtp_port
430 .as_ref()
431 .map(|p| p.resolve(params, "signalingOnlyRtpPort"))
432 .transpose()?
433 .unwrap_or(9);
434 MediaMode::SignalingOnly { sdp_rtp_port: port }
435 }
436 };
437 }
438 Ok(())
439 }
440}
441
442struct RecipeParams {
443 capacity: Option<usize>,
444 signaling_only_rtp_port: Option<u16>,
445}
446
447#[derive(Debug, Clone, Deserialize)]
449#[serde(untagged)]
450pub enum RecipeUsize {
451 Literal(usize),
453 Variable(String),
455}
456
457impl RecipeUsize {
458 fn resolve(&self, params: &RecipeParams, field: &str) -> Result<usize> {
459 match self {
460 Self::Literal(value) => Ok(*value),
461 Self::Variable(name) if name == "$capacity" || name == "capacity" => {
462 params.capacity.ok_or_else(|| {
463 SessionError::ConfigError(format!("{field} requires performance capacity"))
464 })
465 }
466 Self::Variable(name) if name == "$capacity90Percent" || name == "capacity90Percent" => {
467 params
468 .capacity
469 .map(|capacity| capacity.saturating_mul(90).div_ceil(100).max(1))
470 .ok_or_else(|| {
471 SessionError::ConfigError(format!("{field} requires performance capacity"))
472 })
473 }
474 Self::Variable(name) => Err(SessionError::ConfigError(format!(
475 "unsupported variable '{name}' in performance recipe field {field}"
476 ))),
477 }
478 }
479}
480
481#[derive(Debug, Clone, Deserialize)]
483#[serde(untagged)]
484pub enum RecipeU16 {
485 Literal(u16),
487 Variable(String),
489}
490
491impl RecipeU16 {
492 fn resolve(&self, params: &RecipeParams, field: &str) -> Result<u16> {
493 match self {
494 Self::Literal(value) => Ok(*value),
495 Self::Variable(name)
496 if name == "$signalingOnlyRtpPort" || name == "signalingOnlyRtpPort" =>
497 {
498 Ok(params.signaling_only_rtp_port.unwrap_or(9))
499 }
500 Self::Variable(name) => Err(SessionError::ConfigError(format!(
501 "unsupported variable '{name}' in performance recipe field {field}"
502 ))),
503 }
504 }
505}
506
507#[derive(Debug, Clone, Copy, Deserialize)]
509#[serde(rename_all = "kebab-case")]
510pub enum RecipeUdpParseDispatch {
511 SourceHash,
513 RoundRobin,
515}
516
517impl From<RecipeUdpParseDispatch> for rvoip_sip_transport::UdpParseDispatch {
518 fn from(value: RecipeUdpParseDispatch) -> Self {
519 match value {
520 RecipeUdpParseDispatch::SourceHash => Self::SourceHash,
521 RecipeUdpParseDispatch::RoundRobin => Self::RoundRobin,
522 }
523 }
524}
525
526#[derive(Debug, Clone, Copy, Deserialize)]
528#[serde(rename_all = "kebab-case")]
529pub enum RecipeMediaMode {
530 Enabled,
532 SignalingOnly,
534}
535
536#[derive(Debug, Clone, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct RecipeMediaPortCapacity {
540 pub start: u16,
542 pub capacity: RecipeUsize,
544}
545
546#[derive(Debug, Clone, Default, Deserialize)]
548#[serde(rename_all = "camelCase")]
549pub struct RecipeRtpSessionBufferConfig {
550 pub sender_channel_capacity: Option<RecipeUsize>,
552 pub receiver_channel_capacity: Option<RecipeUsize>,
554 pub event_channel_capacity: Option<RecipeUsize>,
556}
557
558impl RecipeRtpSessionBufferConfig {
559 fn apply(&self, config: &mut RtpSessionBufferConfig, params: &RecipeParams) -> Result<()> {
560 if let Some(value) = &self.sender_channel_capacity {
561 config.sender_channel_capacity =
562 value.resolve(params, "rtpSessionBufferConfig.senderChannelCapacity")?;
563 }
564 if let Some(value) = &self.receiver_channel_capacity {
565 config.receiver_channel_capacity =
566 value.resolve(params, "rtpSessionBufferConfig.receiverChannelCapacity")?;
567 }
568 if let Some(value) = &self.event_channel_capacity {
569 config.event_channel_capacity =
570 value.resolve(params, "rtpSessionBufferConfig.eventChannelCapacity")?;
571 }
572 Ok(())
573 }
574}
575
576#[derive(Debug, Clone, Default, Deserialize)]
578#[serde(rename_all = "camelCase")]
579pub struct RecipeRtpTransportBufferConfig {
580 pub event_channel_capacity: Option<RecipeUsize>,
582 pub recv_buffer_size: Option<RecipeUsize>,
584 pub rtcp_recv_buffer_size: Option<RecipeUsize>,
586}
587
588impl RecipeRtpTransportBufferConfig {
589 fn apply(&self, config: &mut RtpTransportBufferConfig, params: &RecipeParams) -> Result<()> {
590 if let Some(value) = &self.event_channel_capacity {
591 config.event_channel_capacity =
592 value.resolve(params, "rtpTransportBufferConfig.eventChannelCapacity")?;
593 }
594 if let Some(value) = &self.recv_buffer_size {
595 config.recv_buffer_size =
596 value.resolve(params, "rtpTransportBufferConfig.recvBufferSize")?;
597 }
598 if let Some(value) = &self.rtcp_recv_buffer_size {
599 config.rtcp_recv_buffer_size =
600 value.resolve(params, "rtpTransportBufferConfig.rtcpRecvBufferSize")?;
601 }
602 Ok(())
603 }
604}
605
606#[derive(Debug, Clone, Default, Deserialize)]
608#[serde(rename_all = "camelCase")]
609pub struct RecipeMediaSessionControllerConfig {
610 pub capacity_hint: Option<RecipeUsize>,
612 pub audio_frame_pool: Option<RecipeAudioFramePoolConfig>,
614 pub rtp_buffer_size: Option<RecipeUsize>,
616 pub rtp_buffer_initial_count: Option<RecipeUsize>,
618 pub rtp_buffer_max_count: Option<RecipeUsize>,
620 pub rtp_session_buffer_config: Option<RecipeRtpSessionBufferConfig>,
622 pub rtp_transport_buffer_config: Option<RecipeRtpTransportBufferConfig>,
624}
625
626impl RecipeMediaSessionControllerConfig {
627 fn apply(
628 &self,
629 config: &mut MediaSessionControllerConfig,
630 params: &RecipeParams,
631 ) -> Result<()> {
632 if let Some(value) = &self.capacity_hint {
633 config.capacity_hint =
634 value.resolve(params, "mediaSessionControllerConfig.capacityHint")?;
635 }
636 if let Some(pool) = &self.audio_frame_pool {
637 pool.apply(config, params)?;
638 }
639 if let Some(value) = &self.rtp_buffer_size {
640 config.rtp_buffer_size =
641 value.resolve(params, "mediaSessionControllerConfig.rtpBufferSize")?;
642 }
643 if let Some(value) = &self.rtp_buffer_initial_count {
644 config.rtp_buffer_initial_count =
645 value.resolve(params, "mediaSessionControllerConfig.rtpBufferInitialCount")?;
646 }
647 if let Some(value) = &self.rtp_buffer_max_count {
648 config.rtp_buffer_max_count =
649 value.resolve(params, "mediaSessionControllerConfig.rtpBufferMaxCount")?;
650 }
651 if let Some(rtp_config) = &self.rtp_session_buffer_config {
652 rtp_config.apply(&mut config.rtp_session_buffer_config, params)?;
653 }
654 if let Some(rtp_config) = &self.rtp_transport_buffer_config {
655 rtp_config.apply(&mut config.rtp_transport_buffer_config, params)?;
656 }
657 Ok(())
658 }
659}
660
661#[derive(Debug, Clone, Default, Deserialize)]
663#[serde(rename_all = "camelCase")]
664pub struct RecipeAudioFramePoolConfig {
665 pub initial_size: Option<RecipeUsize>,
667 pub max_size: Option<RecipeUsize>,
669 pub sample_rate: Option<u32>,
671 pub channels: Option<u8>,
673 pub samples_per_frame: Option<RecipeUsize>,
675}
676
677impl RecipeAudioFramePoolConfig {
678 fn apply(
679 &self,
680 config: &mut MediaSessionControllerConfig,
681 params: &RecipeParams,
682 ) -> Result<()> {
683 if let Some(value) = &self.initial_size {
684 config.audio_frame_pool.initial_size = value.resolve(
685 params,
686 "mediaSessionControllerConfig.audioFramePool.initialSize",
687 )?;
688 }
689 if let Some(value) = &self.max_size {
690 config.audio_frame_pool.max_size = value.resolve(
691 params,
692 "mediaSessionControllerConfig.audioFramePool.maxSize",
693 )?;
694 }
695 if let Some(value) = self.sample_rate {
696 config.audio_frame_pool.sample_rate = value;
697 }
698 if let Some(value) = self.channels {
699 config.audio_frame_pool.channels = value;
700 }
701 if let Some(value) = &self.samples_per_frame {
702 config.audio_frame_pool.samples_per_frame = value.resolve(
703 params,
704 "mediaSessionControllerConfig.audioFramePool.samplesPerFrame",
705 )?;
706 }
707 Ok(())
708 }
709}