perf(api): run heavy endpoints in the threadpool (stop freezing the web GUI)

Blocking operations (SMTP send loops, Wazuh/Nessus/Graph HTTP, bulk
re-scoring) were declared `async def` but do synchronous work inline, so they
ran ON the event loop and froze the entire web GUI until they finished
(tester: "Notify All Critical/High" locks TrueVuln the whole time, and the
same at many other places).

Converted the offending endpoints to plain `def` so Starlette runs them in a
worker threadpool off the event loop — the GUI stays responsive while they
run. Verified each has no `await` in its body before converting (mechanical,
behaviour-preserving):

- notifications: notify-critical (the reported one), test email
- assets: coverage-gap, refresh-exposure, sync-wazuh-assets, rescan-asset
- scans: trigger-autoscan
- vulnerabilities: enrich-single, refresh-kev-catalog, wazuh sync,
  override-from-nessus, recompute-priority-scores
- nessus: test-connection, list-scans, scan-host

(Several other heavy endpoints — m365-check, eol-check, exploit-intel,
bulk-enrich, nessus /sync, app-cve-scan, suppress-fp — were already sync def
from earlier in this work.)
This commit is contained in:
2026-07-09 10:03:11 +02:00
parent d54a821054
commit 340e70ce67
5 changed files with 18 additions and 15 deletions
+4 -4
View File
@@ -202,7 +202,7 @@ async def reconcile_lifecycle(
@router.get("/{asset_id}/coverage-gap")
async def asset_coverage_gap(
def asset_coverage_gap(
asset_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
@@ -301,7 +301,7 @@ async def asset_coverage_gap(
@router.post("/refresh-exposure")
async def refresh_exposure(
def refresh_exposure(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
@@ -740,7 +740,7 @@ from app.integrations.wazuh_client import WazuhClient, WazuhAPIError
from app.models.setting import Setting
@router.post("/sync_wazuh", response_model=dict)
async def sync_wazuh_assets(
def sync_wazuh_assets(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
):
@@ -871,7 +871,7 @@ async def sync_wazuh_assets(
@router.post("/{asset_id}/rescan", response_model=dict)
async def rescan_asset(
def rescan_asset(
asset_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
+3 -3
View File
@@ -65,7 +65,7 @@ class NessusScanImportRequest(BaseModel):
# ---------- endpoints ----------
@router.post("/test")
async def test_nessus_connection(
def test_nessus_connection(
db: Session = Depends(get_db),
current_user: User = Depends(RequireAdmin),
):
@@ -103,7 +103,7 @@ async def test_nessus_connection(
@router.get("/scans")
async def list_nessus_scans(
def list_nessus_scans(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor),
):
@@ -177,7 +177,7 @@ def trigger_nessus_sync(
@router.post("/scan-host")
async def nessus_scan_host(
def nessus_scan_host(
payload: NessusScanHostRequest,
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor),
+5 -2
View File
@@ -100,7 +100,8 @@ async def list_notification_logs(
@router.post("/test")
async def send_test_email(
# Sync def → threadpool; the blocking SMTP send won't stall the event loop.
def send_test_email(
test_data: TestEmailRequest,
db: Session = Depends(get_db),
current_user: User = Depends(RequireAdmin)
@@ -133,7 +134,9 @@ async def send_test_email(
@router.post("/notify-critical")
async def notify_unnotified_critical_vulnerabilities(
# Sync def → Starlette runs it in a worker threadpool (off the event loop), so
# the blocking SMTP send loop over thousands of CVEs doesn't freeze the web GUI.
def notify_unnotified_critical_vulnerabilities(
db: Session = Depends(get_db),
current_user: User = Depends(RequireAdmin)
):
+1 -1
View File
@@ -227,7 +227,7 @@ from app.integrations.wazuh_client import WazuhClient, WazuhAPIError
from app.models.setting import Setting
@router.post("/autoscan", response_model=dict)
async def trigger_autoscan(
def trigger_autoscan(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
):
+5 -5
View File
@@ -1346,7 +1346,7 @@ def analyze_vulnerability_with_ai(
@router.post("/{vuln_id}/enrich")
async def enrich_single_vulnerability(
def enrich_single_vulnerability(
vuln_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
@@ -1490,7 +1490,7 @@ async def backfill_cve_dates(
@router.post("/enrich/kev/refresh")
async def refresh_kev_catalog(
def refresh_kev_catalog(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
):
@@ -1653,7 +1653,7 @@ async def unmark_false_positive(
@router.post("/sync/wazuh")
async def sync_vulnerabilities_from_wazuh(
def sync_vulnerabilities_from_wazuh(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor)
):
@@ -2343,7 +2343,7 @@ async def check_incorrect_scores(
@router.post("/override/nessus/{asset_id}")
async def override_from_nessus(
def override_from_nessus(
asset_id: int,
scan_id: Optional[int] = None,
dry_run: bool = Query(True, description="Nur simulieren ohne DB-Änderungen"),
@@ -3036,7 +3036,7 @@ async def refresh_msrc_endpoint(
@router.post("/recompute-scores")
async def recompute_priority_scores(
def recompute_priority_scores(
db: Session = Depends(get_db),
current_user: User = Depends(RequireEditor),
):