refactor(generator): promote nested functions; rename used_combos to seen_service_pairs

_take_ip and _new_decky were closures capturing outer-scope state. Promoted to
module-level with explicit parameters. seen_service_pairs name makes the intent
clear — it prevents the same service frozenset from being assigned repeatedly.
This commit is contained in:
2026-04-30 21:53:45 -04:00
parent 07e6bafff8
commit d1ed2701e7

View File

@@ -74,7 +74,7 @@ def _pick_services(
rng: random.Random, rng: random.Random,
services_explicit: Optional[list[str]], services_explicit: Optional[list[str]],
pool: list[str], pool: list[str],
used_combos: set[frozenset], seen_service_pairs: set[frozenset],
) -> list[str]: ) -> list[str]:
if services_explicit: if services_explicit:
return list(services_explicit) return list(services_explicit)
@@ -85,12 +85,39 @@ def _pick_services(
count = rng.randint(_SVC_MIN, min(_SVC_MAX, len(pool))) # nosec B311 count = rng.randint(_SVC_MIN, min(_SVC_MAX, len(pool))) # nosec B311
chosen = frozenset(rng.sample(pool, count)) # nosec B311 chosen = frozenset(rng.sample(pool, count)) # nosec B311
attempts += 1 attempts += 1
if chosen not in used_combos or attempts > 20: if chosen not in seen_service_pairs or attempts > 20:
break break
used_combos.add(chosen) seen_service_pairs.add(chosen)
return list(chosen) return list(chosen)
def _take_ip(ip_allocs: dict[str, IPAllocator], lan_name: str) -> str:
return ip_allocs[lan_name].next_free()
def _new_decky(
home_lan: str,
*,
counter: list[int],
rng: random.Random,
config: TopologyConfig,
svc_pool: list[str],
seen_service_pairs: set[frozenset],
ip_allocs: dict[str, IPAllocator],
deckies: list[_PlannedDecky],
) -> _PlannedDecky:
counter[0] += 1
name = f"decky-{counter[0]:03d}"
services = _pick_services(rng, config.services_explicit, svc_pool, seen_service_pairs)
decky = _PlannedDecky(
name=name,
services=services,
ips_by_lan={home_lan: _take_ip(ip_allocs, home_lan)},
)
deckies.append(decky)
return decky
def generate( def generate(
config: TopologyConfig, config: TopologyConfig,
*, *,
@@ -108,7 +135,9 @@ def generate(
""" """
rng = random.Random(config.seed) # nosec B311 rng = random.Random(config.seed) # nosec B311
svc_pool = all_service_names() if config.randomize_services else [] svc_pool = all_service_names() if config.randomize_services else []
used_combos: set[frozenset] = set() # Tracks unique service frozensets assigned so far; prevents every decky
# from getting the same randomly-picked combo on small service pools.
seen_service_pairs: set[frozenset] = set()
subnets = SubnetAllocator( subnets = SubnetAllocator(
config.subnet_base_prefix, reserved=reserved_subnets or set() config.subnet_base_prefix, reserved=reserved_subnets or set()
@@ -121,27 +150,9 @@ def generate(
lan.name: IPAllocator(lan.subnet) for lan in lans lan.name: IPAllocator(lan.subnet) for lan in lans
} }
def _take_ip(lan_name: str) -> str:
return ip_allocs[lan_name].next_free()
deckies: list[_PlannedDecky] = [] deckies: list[_PlannedDecky] = []
edges: list[_PlannedEdge] = [] edges: list[_PlannedEdge] = []
decky_counter = 0 decky_counter = [0]
def _new_decky(home_lan: str) -> _PlannedDecky:
nonlocal decky_counter
decky_counter += 1
name = f"decky-{decky_counter:03d}"
services = _pick_services(
rng, config.services_explicit, svc_pool, used_combos
)
decky = _PlannedDecky(
name=name,
services=services,
ips_by_lan={home_lan: _take_ip(home_lan)},
)
deckies.append(decky)
return decky
# Populate each LAN with its own deckies. # Populate each LAN with its own deckies.
for lan in lans: for lan in lans:
@@ -154,7 +165,16 @@ def generate(
if count < 1: if count < 1:
count = 1 # every LAN needs ≥1 decky to host the bridge count = 1 # every LAN needs ≥1 decky to host the bridge
for _ in range(count): for _ in range(count):
decky = _new_decky(lan.name) decky = _new_decky(
lan.name,
counter=decky_counter,
rng=rng,
config=config,
svc_pool=svc_pool,
seen_service_pairs=seen_service_pairs,
ip_allocs=ip_allocs,
deckies=deckies,
)
edges.append( edges.append(
_PlannedEdge( _PlannedEdge(
decky_name=decky.name, decky_name=decky.name,
@@ -178,7 +198,7 @@ def generate(
continue continue
candidates = deckies_by_lan[lan.name] candidates = deckies_by_lan[lan.name]
bridge = rng.choice(candidates) # nosec B311 bridge = rng.choice(candidates) # nosec B311
bridge.ips_by_lan[lan.parent] = _take_ip(lan.parent) bridge.ips_by_lan[lan.parent] = _take_ip(ip_allocs, lan.parent)
forwards = rng.random() < config.bridge_forward_probability # nosec B311 forwards = rng.random() < config.bridge_forward_probability # nosec B311
bridge.forwards_l3 = bridge.forwards_l3 or forwards bridge.forwards_l3 = bridge.forwards_l3 or forwards
# Mark both existing edges as bridge edges for this decky, and # Mark both existing edges as bridge edges for this decky, and
@@ -214,7 +234,7 @@ def generate(
decky = rng.choice(deckies_by_lan[lan.name]) # nosec B311 decky = rng.choice(deckies_by_lan[lan.name]) # nosec B311
if peer.name in decky.ips_by_lan: if peer.name in decky.ips_by_lan:
continue # already connected, skip continue # already connected, skip
decky.ips_by_lan[peer.name] = _take_ip(peer.name) decky.ips_by_lan[peer.name] = _take_ip(ip_allocs, peer.name)
forwards = rng.random() < config.bridge_forward_probability # nosec B311 forwards = rng.random() < config.bridge_forward_probability # nosec B311
decky.forwards_l3 = decky.forwards_l3 or forwards decky.forwards_l3 = decky.forwards_l3 or forwards
for e in edges: for e in edges: