diff --git a/apps/api/src/services/iwooos_security_asset_control_plane.py b/apps/api/src/services/iwooos_security_asset_control_plane.py index 715bef153..f2a1b4d86 100644 --- a/apps/api/src/services/iwooos_security_asset_control_plane.py +++ b/apps/api/src/services/iwooos_security_asset_control_plane.py @@ -15,7 +15,7 @@ from src.db.base import get_db_context logger = structlog.get_logger(__name__) _SCHEMA_VERSION = "iwooos_security_asset_control_plane_v1" -_QUERY_TIMEOUT_SECONDS = 6.0 +_QUERY_TIMEOUT_SECONDS = 20.0 _SOURCE_QUERY_TIMEOUT_SECONDS = 4.0 _CACHE_TTL_SECONDS = 30.0 _DISCOVERY_FRESHNESS_SECONDS = 2 * 60 * 60 @@ -1276,6 +1276,7 @@ def build_unavailable_iwooos_security_asset_control_plane( async def _read_public_source( *, project_id: str, + limiter: asyncio.Semaphore, source_id: str, statement: Any, result_mode: str, @@ -1295,9 +1296,10 @@ async def _read_public_source( raise ValueError("unsupported_result_mode") try: - value = await asyncio.wait_for( - execute_source(), timeout=_SOURCE_QUERY_TIMEOUT_SECONDS - ) + async with limiter: + value = await asyncio.wait_for( + execute_source(), timeout=_SOURCE_QUERY_TIMEOUT_SECONDS + ) return value, { "source_id": source_id, "status": "ready", @@ -1370,9 +1372,11 @@ class IwoooSSecurityAssetControlPlaneService: return payload async def _load_snapshot(self) -> dict[str, Any]: + source_limiter = asyncio.Semaphore(1) source_results = await asyncio.gather( _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="asset_discovery", statement=_sql( """ @@ -1408,6 +1412,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="asset_inventory", statement=_sql( """ @@ -1431,6 +1436,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="automation_coverage", statement=_sql( """ @@ -1452,6 +1458,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="asset_relationship", statement=_sql( """ @@ -1474,6 +1481,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="security_compliance", statement=_sql( """ @@ -1494,6 +1502,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="automation_operations", statement=_sql( """ @@ -1509,6 +1518,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="ai_decision_trace", statement=_sql( """ @@ -1525,6 +1535,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="siem_runtime", statement=_sql( """ @@ -1573,6 +1584,7 @@ class IwoooSSecurityAssetControlPlaneService: ), _read_public_source( project_id="awoooi", + limiter=source_limiter, source_id="audit_runtime", statement=_sql( """ diff --git a/apps/api/tests/test_iwooos_security_asset_control_plane.py b/apps/api/tests/test_iwooos_security_asset_control_plane.py index 3ec1b7f0a..803de1509 100644 --- a/apps/api/tests/test_iwooos_security_asset_control_plane.py +++ b/apps/api/tests/test_iwooos_security_asset_control_plane.py @@ -246,6 +246,7 @@ def test_read_public_source_returns_redacted_reason_code_on_query_failure( value, health = asyncio.run( _read_public_source( project_id="awoooi", + limiter=asyncio.Semaphore(1), source_id="audit_runtime", statement="SELECT 1", result_mode="one", @@ -341,7 +342,7 @@ def test_service_uses_canonical_awoooi_project_scope(monkeypatch) -> None: assert payload["summary"]["unavailable_source_count"] == 9 -def test_service_reads_independent_sources_concurrently(monkeypatch) -> None: +def test_service_serializes_sources_for_single_connection_budget(monkeypatch) -> None: service = IwoooSSecurityAssetControlPlaneService() concurrency = {"active": 0, "maximum": 0} @@ -376,7 +377,7 @@ def test_service_reads_independent_sources_concurrently(monkeypatch) -> None: ) payload = asyncio.run(service._load_snapshot()) - assert concurrency["maximum"] == 9 + assert concurrency["maximum"] == 1 assert payload["summary"]["source_count"] == 9 assert payload["summary"]["ready_source_count"] == 9 assert payload["summary"]["unavailable_source_count"] == 0