diff --git a/src/config/src/config.rs b/src/config/src/config.rs index 9148d17e90..51b7fb7e51 100644 --- a/src/config/src/config.rs +++ b/src/config/src/config.rs @@ -665,6 +665,7 @@ pub fn load_config() -> Result<(), anyhow::Error> { } static CHROME_LAUNCHER_OPTIONS: tokio::sync::OnceCell> = tokio::sync::OnceCell::const_new(); +static CHROME_SEMAPHORE: std::sync::OnceLock = std::sync::OnceLock::new(); pub async fn get_chrome_launch_options() -> &'static Option { CHROME_LAUNCHER_OPTIONS @@ -672,6 +673,16 @@ pub async fn get_chrome_launch_options() -> &'static Option { .await } +/// Caps concurrent headless Chrome processes at the CPU limit; hold a permit while one runs. +pub fn chrome_semaphore() -> &'static tokio::sync::Semaphore { + CHROME_SEMAPHORE.get_or_init(|| { + let cfg = get_config(); + // cpu_num is floored at 2 and real_cpu_num ignores the memory limit, so take the smaller. + let permits = cfg.limit.cpu_num.min(cfg.limit.real_cpu_num); + tokio::sync::Semaphore::new(max(1, permits)) + }) +} + async fn init_chrome_launch_options() -> Option { let cfg = get_config(); if !cfg.chrome.chrome_enabled || !cfg.common.report_server_url.is_empty() { @@ -2746,7 +2757,7 @@ pub struct Limit { help = "Count-based alerts whose row sentinel exceeds this switch to a COUNT(*) decision query plus a 100-row payload sample (alerts_2.md 4.4c). Clamped up to the 100-row floor." )] pub alert_hybrid_count_threshold: i64, - #[env_config(name = "ZO_ALERT_SCHEDULE_CONCURRENCY", default = 5)] + #[env_config(name = "ZO_ALERT_SCHEDULE_CONCURRENCY", default = 50)] pub alert_schedule_concurrency: i64, #[env_config( name = "ZO_ALERT_MAX_GROUPS", @@ -6608,4 +6619,13 @@ mod tests { assert_eq!(select_ext_auth_salt("", "stored"), "stored"); assert_eq!(select_ext_auth_salt("", ""), LEGACY_EXT_AUTH_SALT); } + + #[test] + fn test_chrome_semaphore_never_exceeds_cpu_limit() { + let cfg = get_config(); + let permits = chrome_semaphore().available_permits(); + assert!(permits >= 1); + assert!(permits <= cfg.limit.real_cpu_num); + assert!(permits <= cfg.limit.cpu_num); + } } diff --git a/src/core/src/dashboards/reports.rs b/src/core/src/dashboards/reports.rs index 0985c92673..7588e1380f 100644 --- a/src/core/src/dashboards/reports.rs +++ b/src/core/src/dashboards/reports.rs @@ -886,6 +886,9 @@ pub enum GenerateReportError { #[error(transparent)] JoinHandlerError(#[from] tokio::task::JoinError), + #[error(transparent)] + ChromeSemaphoreError(#[from] tokio::sync::AcquireError), + #[error("[REPORT] main element not rendered yet for dashboard {dashboard_id}: {e}")] MainElementNotRendered { dashboard_id: String, @@ -941,6 +944,7 @@ async fn generate_report( ); } + let _chrome_permit = config::chrome_semaphore().acquire().await?; log::info!("launching browser for dashboard {dashboard_id}"); let (mut browser, mut handler) = Browser::launch(get_chrome_launch_options().await.as_ref().unwrap().clone()).await?; diff --git a/src/report_server/src/report.rs b/src/report_server/src/report.rs index 787b22e0f7..3bd8d87469 100644 --- a/src/report_server/src/report.rs +++ b/src/report_server/src/report.rs @@ -168,6 +168,7 @@ pub async fn generate_report( // Only one tab is supported for now let tab_id = &dashboard.tabs[0]; + let _chrome_permit = config::chrome_semaphore().acquire().await?; log::info!("launching browser for dashboard {dashboard_id}"); let browser_config = get_chrome_launch_options() .await