fix(topology): cache IPAllocator host set; type repo params as BaseRepository

_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.
This commit is contained in:
2026-04-30 21:52:29 -04:00
parent 257857338c
commit 84e0ac4a43
2 changed files with 10 additions and 7 deletions

View File

@@ -12,9 +12,10 @@ open one.
from __future__ import annotations from __future__ import annotations
from ipaddress import IPv4Network from ipaddress import IPv4Network
from typing import Any, Iterable from typing import Iterable
from decnet.topology.status import TopologyStatus from decnet.topology.status import TopologyStatus
from decnet.web.db.repository import BaseRepository
class AllocatorExhausted(RuntimeError): class AllocatorExhausted(RuntimeError):
@@ -34,6 +35,7 @@ class IPAllocator:
self._pool: list[str] = [ self._pool: list[str] = [
str(ip) for ip in self._net.hosts() if str(ip) != self._gateway 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._taken: set[str] = set()
self._cursor = 0 self._cursor = 0
@@ -57,7 +59,7 @@ class IPAllocator:
def reserve(self, ip: str) -> None: def reserve(self, ip: str) -> None:
if ip == self._gateway: if ip == self._gateway:
raise ValueError(f"{ip} is the gateway of {self._net.with_prefixlen}") 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}") raise ValueError(f"{ip} not in {self._net.with_prefixlen}")
self._taken.add(ip) self._taken.add(ip)
@@ -65,7 +67,7 @@ class IPAllocator:
self._taken.discard(ip) self._taken.discard(ip)
def is_free(self, ip: str) -> bool: 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: 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.""" """All LAN subnets currently claimed by non-torn-down topologies."""
out: set[str] = set() out: set[str] = set()
for status in _SUBNET_CLAIMING_STATES: for status in _SUBNET_CLAIMING_STATES:

View File

@@ -5,12 +5,13 @@ from ipaddress import IPv4Address, IPv4Network
from typing import Any from typing import Any
from decnet.topology.allocator import IPAllocator from decnet.topology.allocator import IPAllocator
from decnet.web.db.repository import BaseRepository
from decnet.topology.config import GeneratedTopology from decnet.topology.config import GeneratedTopology
from decnet.topology.status import TopologyStatus, assert_transition from decnet.topology.status import TopologyStatus, assert_transition
async def persist( async def persist(
repo: Any, repo: BaseRepository,
plan: GeneratedTopology, plan: GeneratedTopology,
*, *,
target_host_uuid: str | None = None, target_host_uuid: str | None = None,
@@ -90,7 +91,7 @@ async def persist(
async def transition_status( async def transition_status(
repo: Any, repo: BaseRepository,
topology_id: str, topology_id: str,
new_status: str, new_status: str,
reason: str | None = None, reason: str | None = None,
@@ -107,7 +108,7 @@ async def transition_status(
await repo.update_topology_status(topology_id, new_status, reason=reason) 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. """Load a topology + children into a single dict for callers.
Shape:: Shape::