Skip to content

Commit fc28957

Browse files
committed
refactor: extract _run_once_locked for lazy validation
Deduplicate the double-checked-locking pattern shared by _ensure_section_validated and _ensure_hot_water_group_validated into a single _run_once_locked helper taking the lock map, a done-check and an async body. Behavior preserving; no public API change.
1 parent 2a19f42 commit fc28957

1 file changed

Lines changed: 55 additions & 30 deletions

File tree

src/bsblan/bsblan.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
99

1010
if TYPE_CHECKING:
11-
from collections.abc import Mapping
11+
from collections.abc import Awaitable, Callable, Mapping
1212

1313
import aiohttp
1414
import backoff
@@ -259,6 +259,40 @@ def _apply_bus_specific_api_config(self) -> None:
259259
self._api_data["heating_circuit2"] = {}
260260
self._api_data["staticValues_circuit2"] = {}
261261

262+
async def _run_once_locked(
263+
self,
264+
key: str,
265+
locks: dict[str, asyncio.Lock],
266+
is_done: Callable[[], bool],
267+
body: Callable[[], Awaitable[None]],
268+
) -> None:
269+
"""Run an idempotent async operation at most once per key.
270+
271+
Implements double-checked locking: a fast path when the work is already
272+
done, a per-key lock to serialize concurrent first-time callers, and a
273+
re-check after acquiring the lock so only one caller runs ``body``.
274+
275+
Args:
276+
key (str): Identifier for the one-time operation.
277+
locks (dict[str, asyncio.Lock]): Registry of per-key locks, created
278+
on demand.
279+
is_done (Callable[[], bool]): Predicate returning True when the work
280+
is already complete.
281+
body (Callable[[], Awaitable[None]]): Coroutine factory executed once
282+
while holding the lock.
283+
284+
"""
285+
if is_done():
286+
return
287+
288+
if key not in locks:
289+
locks[key] = asyncio.Lock()
290+
291+
async with locks[key]:
292+
if is_done():
293+
return
294+
await body()
295+
262296
async def _ensure_section_validated(
263297
self, section: SectionLiteral, include: list[str] | None = None
264298
) -> None:
@@ -279,19 +313,7 @@ async def _ensure_section_validated(
279313
if not self._api_validator:
280314
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
281315

282-
# Fast path: skip if already validated (no lock needed)
283-
if self._api_validator.is_section_validated(section):
284-
return
285-
286-
# Get or create lock for this section
287-
if section not in self._section_locks:
288-
self._section_locks[section] = asyncio.Lock()
289-
290-
async with self._section_locks[section]:
291-
# Double-check after acquiring lock (another task may have validated)
292-
if self._api_validator.is_section_validated(section):
293-
return
294-
316+
async def _validate() -> None:
295317
logger.debug("Lazy loading section: %s", section)
296318
response_data = await self._validate_api_section(section, include)
297319

@@ -300,6 +322,13 @@ async def _ensure_section_validated(
300322
):
301323
self._extract_temperature_unit_from_response(response_data)
302324

325+
await self._run_once_locked(
326+
section,
327+
self._section_locks,
328+
lambda: self._api_validator.is_section_validated(section),
329+
_validate,
330+
)
331+
303332
def _should_extract_temperature_unit(
304333
self,
305334
section: SectionLiteral,
@@ -337,24 +366,13 @@ async def _ensure_hot_water_group_validated(
337366
If provided, only these parameters will be validated.
338367
339368
"""
340-
# Fast path: skip if already validated (no lock needed)
341-
if group_name in self._validated_hot_water_groups:
342-
return
343-
344-
if not self._api_validator:
345-
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
346369

347-
if not self._api_data:
348-
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
370+
async def _validate() -> None:
371+
if not self._api_validator:
372+
raise BSBLANError(ErrorMsg.API_VALIDATOR_NOT_INITIALIZED)
349373

350-
# Get or create lock for this group
351-
if group_name not in self._hot_water_group_locks:
352-
self._hot_water_group_locks[group_name] = asyncio.Lock()
353-
354-
async with self._hot_water_group_locks[group_name]:
355-
# Double-check after acquiring lock (another task may have validated)
356-
if group_name in self._validated_hot_water_groups:
357-
return
374+
if not self._api_data:
375+
raise BSBLANError(ErrorMsg.API_DATA_NOT_INITIALIZED)
358376

359377
logger.debug("Lazy loading hot water group: %s", group_name)
360378

@@ -423,6 +441,13 @@ async def _ensure_hot_water_group_validated(
423441
len(params_to_remove),
424442
)
425443

444+
await self._run_once_locked(
445+
group_name,
446+
self._hot_water_group_locks,
447+
lambda: group_name in self._validated_hot_water_groups,
448+
_validate,
449+
)
450+
426451
async def _validate_api_section(
427452
self, section: SectionLiteral, include: list[str] | None = None
428453
) -> dict[str, Any] | None:

0 commit comments

Comments
 (0)