fix(ip-pool): reconcile stale leases against live clones (§2.8, W1)
A job killed mid-flight never released its IP lease; with only 4 pool addresses a single leak exhausted the pool ("IP pool exhausted after 60s"). IpSlotPool.reconcile() releases leases whose owner clone-dir is gone; vm cleanup runs it only against the real build_vms base. Closes TODO §2.8.
This commit is contained in:
@@ -115,3 +115,45 @@ def test_non_dict_lease_json_treated_as_empty(tmp_path: Path) -> None:
|
||||
pool = make_pool(tmp_path)
|
||||
ip = pool.acquire("owner-1")
|
||||
assert ip == "10.0.0.1"
|
||||
|
||||
|
||||
# ── reconcile (TODO §2.8) ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_reconcile_releases_leases_with_dead_owners(tmp_path: Path) -> None:
|
||||
pool = make_pool(tmp_path)
|
||||
pool.acquire("clone-alive") # 10.0.0.1
|
||||
pool.acquire("clone-dead") # 10.0.0.2
|
||||
|
||||
released = pool.reconcile({"clone-alive"})
|
||||
|
||||
assert released == ["10.0.0.2"]
|
||||
leases = json.loads((tmp_path / "ip-pool.json").read_text())
|
||||
assert leases["10.0.0.1"] == "clone-alive" # kept
|
||||
assert leases["10.0.0.2"] is None # released
|
||||
|
||||
|
||||
def test_reconcile_keeps_all_when_all_owners_live(tmp_path: Path) -> None:
|
||||
pool = make_pool(tmp_path)
|
||||
pool.acquire("clone-a")
|
||||
pool.acquire("clone-b")
|
||||
assert pool.reconcile({"clone-a", "clone-b"}) == []
|
||||
leases = json.loads((tmp_path / "ip-pool.json").read_text())
|
||||
assert leases["10.0.0.1"] == "clone-a"
|
||||
assert leases["10.0.0.2"] == "clone-b"
|
||||
|
||||
|
||||
def test_reconcile_dry_run_reports_without_writing(tmp_path: Path) -> None:
|
||||
pool = make_pool(tmp_path)
|
||||
pool.acquire("clone-dead") # 10.0.0.1
|
||||
|
||||
released = pool.reconcile(set(), dry_run=True)
|
||||
|
||||
assert released == ["10.0.0.1"]
|
||||
leases = json.loads((tmp_path / "ip-pool.json").read_text())
|
||||
assert leases["10.0.0.1"] == "clone-dead" # untouched
|
||||
|
||||
|
||||
def test_reconcile_empty_pool_returns_empty(tmp_path: Path) -> None:
|
||||
pool = make_pool(tmp_path) # no acquires → all leases None
|
||||
assert pool.reconcile(set()) == []
|
||||
|
||||
Reference in New Issue
Block a user