Skip to content

Commit b350753

Browse files
committed
perf(sync): concurrent automation/script config fetches in discovery
T6: Replace sequential automation and script config HTTP fetches with asyncio.gather() bounded by a semaphore (max 10 concurrent). For a typical HA instance with 50+ automations, this reduces sync time from O(n × latency) to O(latency + n/10 × latency). Also: remove redundant `import logging` in sandbox runner and fix duplicate MPLCONFIGDIR env var. Made-with: Cursor
1 parent 9a3f5a7 commit b350753

2 files changed

Lines changed: 43 additions & 35 deletions

File tree

src/dal/sync.py

Lines changed: 42 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import asyncio
56
import logging
67
import time
78
from datetime import UTC, datetime
@@ -349,24 +350,31 @@ async def _sync_automation_entities(self, entities: list[Any]) -> dict[str, int]
349350
scripts = [e for e in entities if e.domain == "script"]
350351
scenes = [e for e in entities if e.domain == "scene"]
351352

352-
# --- Automations ---
353+
# --- Automations (concurrent config fetches) ---
353354
seen_automation_ids: set[str] = set()
355+
_FETCH_CONCURRENCY = 10
356+
sem = asyncio.Semaphore(_FETCH_CONCURRENCY)
357+
358+
async def _fetch_config(aid: str) -> tuple[str, dict[str, Any] | None]:
359+
async with sem:
360+
try:
361+
return aid, await self.ha.get_automation_config(aid)
362+
except (httpx.HTTPError, TimeoutError, ConnectionError) as exc:
363+
logger.warning("Failed to fetch config for automation %s: %s", aid, exc)
364+
return aid, None
365+
366+
automation_meta: list[tuple[Any, str]] = []
354367
for entity in automations:
355368
attrs = entity.attributes or {}
356369
ha_automation_id = attrs.get("id", entity.entity_id.split(".", 1)[-1])
357370
seen_automation_ids.add(ha_automation_id)
371+
automation_meta.append((entity, ha_automation_id))
358372

359-
# Fetch full config from HA (trigger/condition/action)
360-
config: dict[str, Any] | None = None
361-
try:
362-
config = await self.ha.get_automation_config(ha_automation_id)
363-
except (httpx.HTTPError, TimeoutError, ConnectionError) as exc:
364-
logger.warning(
365-
"Failed to fetch config for automation %s: %s",
366-
ha_automation_id,
367-
exc,
368-
)
373+
config_results = await asyncio.gather(*(_fetch_config(aid) for _, aid in automation_meta))
374+
config_map: dict[str, dict[str, Any] | None] = dict(config_results)
369375

376+
for entity, ha_automation_id in automation_meta:
377+
attrs = entity.attributes or {}
370378
await self.automation_repo.upsert(
371379
{
372380
"ha_automation_id": ha_automation_id,
@@ -375,7 +383,7 @@ async def _sync_automation_entities(self, entities: list[Any]) -> dict[str, int]
375383
"state": entity.state or "off",
376384
"mode": attrs.get("mode", "single"),
377385
"last_triggered": attrs.get("last_triggered"),
378-
"config": config,
386+
"config": config_map.get(ha_automation_id),
379387
}
380388
)
381389
stats["automations_synced"] += 1
@@ -385,28 +393,31 @@ async def _sync_automation_entities(self, entities: list[Any]) -> dict[str, int]
385393
for stale_id in existing_automation_ids - seen_automation_ids:
386394
await self.automation_repo.delete(stale_id)
387395

388-
# --- Scripts ---
396+
# --- Scripts (concurrent config fetches) ---
389397
seen_script_ids: set[str] = set()
398+
399+
async def _fetch_script_config(sid: str) -> tuple[str, dict[str, Any] | None]:
400+
async with sem:
401+
try:
402+
return sid, await self.ha.get_script_config(sid)
403+
except (httpx.HTTPError, TimeoutError, ConnectionError) as exc:
404+
logger.warning("Failed to fetch config for script %s: %s", sid, exc)
405+
return sid, None
406+
407+
script_meta: list[tuple[Any, str]] = []
390408
for entity in scripts:
391-
attrs = entity.attributes or {}
392409
seen_script_ids.add(entity.entity_id)
393-
394-
# Fetch full config from HA (sequence/fields)
395410
script_id = entity.entity_id.split(".", 1)[-1]
396-
sequence: list[Any] | None = None
397-
fields: dict[str, Any] | None = None
398-
try:
399-
script_config = await self.ha.get_script_config(script_id)
400-
if script_config:
401-
sequence = script_config.get("sequence")
402-
fields = script_config.get("fields")
403-
except (httpx.HTTPError, TimeoutError, ConnectionError) as exc:
404-
logger.warning(
405-
"Failed to fetch config for script %s: %s",
406-
script_id,
407-
exc,
408-
)
411+
script_meta.append((entity, script_id))
412+
413+
script_results = await asyncio.gather(
414+
*(_fetch_script_config(sid) for _, sid in script_meta)
415+
)
416+
script_config_map: dict[str, dict[str, Any] | None] = dict(script_results)
409417

418+
for entity, script_id in script_meta:
419+
attrs = entity.attributes or {}
420+
sc = script_config_map.get(script_id)
410421
await self.script_repo.upsert(
411422
{
412423
"entity_id": entity.entity_id,
@@ -415,8 +426,8 @@ async def _sync_automation_entities(self, entities: list[Any]) -> dict[str, int]
415426
"mode": attrs.get("mode", "single"),
416427
"icon": attrs.get("icon"),
417428
"last_triggered": attrs.get("last_triggered"),
418-
"sequence": sequence,
419-
"fields": fields,
429+
"sequence": sc.get("sequence") if sc else None,
430+
"fields": sc.get("fields") if sc else None,
420431
}
421432
)
422433
stats["scripts_synced"] += 1

src/sandbox/runner.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,7 @@ async def _build_command(
345345
if policy.use_gvisor and not await self._is_gvisor_available():
346346
# Create a copy of the policy with gVisor disabled
347347
# Also disable seccomp as it may not be available on all platforms (e.g., macOS)
348-
import logging
349-
350-
logging.getLogger(__name__).warning(
348+
logger.warning(
351349
"gVisor (runsc) not available - running with standard container isolation"
352350
)
353351
policy = policy.model_copy(
@@ -394,7 +392,6 @@ async def _build_command(
394392
# The DS Team agent parses JSON from stdout; stray warnings
395393
# (e.g. pandas pyarrow DeprecationWarning) break extraction.
396394
cmd.extend(["--env", "PYTHONWARNINGS=ignore::DeprecationWarning"])
397-
cmd.extend(["--env", "MPLCONFIGDIR=/tmp"])
398395

399396
# Matplotlib needs a writable config dir; the sandbox has no home dir.
400397
cmd.extend(["--env", "MPLCONFIGDIR=/tmp/matplotlib"])

0 commit comments

Comments
 (0)