From 84e0ac4a43d722a47e777ff7eebe0341eb491eee Mon Sep 17 00:00:00 2001 From: anti Date: Thu, 30 Apr 2026 21:52:29 -0400 Subject: [PATCH] fix(topology): cache IPAllocator host set; type repo params as BaseRepository MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _host_set is computed once in __init__ — reserve() and is_free() were rebuilding the full host frozenset on every call. BaseRepository already existed; the Any annotations were just never updated. --- decnet/topology/allocator.py | 10 ++++++---- decnet/topology/persistence.py | 7 ++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/decnet/topology/allocator.py b/decnet/topology/allocator.py index 7600c730..fd16fcc6 100644 --- a/decnet/topology/allocator.py +++ b/decnet/topology/allocator.py @@ -12,9 +12,10 @@ open one. from __future__ import annotations from ipaddress import IPv4Network -from typing import Any, Iterable +from typing import Iterable from decnet.topology.status import TopologyStatus +from decnet.web.db.repository import BaseRepository class AllocatorExhausted(RuntimeError): @@ -34,6 +35,7 @@ class IPAllocator: self._pool: list[str] = [ str(ip) for ip in self._net.hosts() if str(ip) != self._gateway ] + self._host_set: frozenset[str] = frozenset(str(h) for h in self._net.hosts()) self._taken: set[str] = set() self._cursor = 0 @@ -57,7 +59,7 @@ class IPAllocator: def reserve(self, ip: str) -> None: if ip == self._gateway: raise ValueError(f"{ip} is the gateway of {self._net.with_prefixlen}") - if ip not in {str(h) for h in self._net.hosts()}: + if ip not in self._host_set: raise ValueError(f"{ip} not in {self._net.with_prefixlen}") self._taken.add(ip) @@ -65,7 +67,7 @@ class IPAllocator: self._taken.discard(ip) def is_free(self, ip: str) -> bool: - return ip not in self._taken and ip in {str(h) for h in self._net.hosts()} and ip != self._gateway + return ip not in self._taken and ip in self._host_set and ip != self._gateway class SubnetAllocator: @@ -148,7 +150,7 @@ _SUBNET_CLAIMING_STATES: frozenset[str] = frozenset( ) -async def reserved_subnets(repo: Any) -> set[str]: +async def reserved_subnets(repo: BaseRepository) -> set[str]: """All LAN subnets currently claimed by non-torn-down topologies.""" out: set[str] = set() for status in _SUBNET_CLAIMING_STATES: diff --git a/decnet/topology/persistence.py b/decnet/topology/persistence.py index d70e8f4f..1330141f 100644 --- a/decnet/topology/persistence.py +++ b/decnet/topology/persistence.py @@ -5,12 +5,13 @@ from ipaddress import IPv4Address, IPv4Network from typing import Any from decnet.topology.allocator import IPAllocator +from decnet.web.db.repository import BaseRepository from decnet.topology.config import GeneratedTopology from decnet.topology.status import TopologyStatus, assert_transition async def persist( - repo: Any, + repo: BaseRepository, plan: GeneratedTopology, *, target_host_uuid: str | None = None, @@ -90,7 +91,7 @@ async def persist( async def transition_status( - repo: Any, + repo: BaseRepository, topology_id: str, new_status: str, reason: str | None = None, @@ -107,7 +108,7 @@ async def transition_status( await repo.update_topology_status(topology_id, new_status, reason=reason) -async def hydrate(repo: Any, topology_id: str) -> dict[str, Any] | None: +async def hydrate(repo: BaseRepository, topology_id: str) -> dict[str, Any] | None: """Load a topology + children into a single dict for callers. Shape::