add runtime adapters, scheduler, reconciler, and their unit tests
This commit is contained in:
parent
d1976a5fd8
commit
b63d69c81d
10 changed files with 1471 additions and 28 deletions
|
|
@ -1,7 +1,10 @@
|
|||
"""HAProxy runtime socket adapter — stub for Plan 02."""
|
||||
"""HAProxy runtime socket adapter for managing builder slots."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import io
|
||||
import socket
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
|
|
@ -21,7 +24,12 @@ class SlotHealth:
|
|||
class HAProxyRuntime:
|
||||
"""HAProxy runtime CLI adapter via Unix socket.
|
||||
|
||||
Full implementation in Plan 02.
|
||||
Communicates with HAProxy using the admin socket text protocol.
|
||||
|
||||
Args:
|
||||
socket_path: Path to the HAProxy admin Unix socket.
|
||||
backend: HAProxy backend name (e.g. "all").
|
||||
slot_prefix: Server name prefix used for builder slots.
|
||||
"""
|
||||
|
||||
def __init__(self, socket_path: str, backend: str, slot_prefix: str) -> None:
|
||||
|
|
@ -31,24 +39,76 @@ class HAProxyRuntime:
|
|||
|
||||
def set_slot_addr(self, slot_id: str, ip: str, port: int = 22) -> None:
|
||||
"""Update server address for a slot."""
|
||||
raise NotImplementedError
|
||||
cmd = f"set server {self._backend}/{slot_id} addr {ip} port {port}"
|
||||
resp = self._run(cmd)
|
||||
self._check_response(resp, slot_id)
|
||||
|
||||
def enable_slot(self, slot_id: str) -> None:
|
||||
"""Enable a server slot."""
|
||||
raise NotImplementedError
|
||||
cmd = f"enable server {self._backend}/{slot_id}"
|
||||
resp = self._run(cmd)
|
||||
self._check_response(resp, slot_id)
|
||||
|
||||
def disable_slot(self, slot_id: str) -> None:
|
||||
"""Disable a server slot."""
|
||||
raise NotImplementedError
|
||||
cmd = f"disable server {self._backend}/{slot_id}"
|
||||
resp = self._run(cmd)
|
||||
self._check_response(resp, slot_id)
|
||||
|
||||
def slot_is_up(self, slot_id: str) -> bool:
|
||||
"""Return True when HAProxy health status is UP for slot."""
|
||||
raise NotImplementedError
|
||||
health = self.read_slot_health()
|
||||
entry = health.get(slot_id)
|
||||
return entry is not None and entry.status == "UP"
|
||||
|
||||
def slot_session_count(self, slot_id: str) -> int:
|
||||
"""Return current active session count for slot."""
|
||||
raise NotImplementedError
|
||||
health = self.read_slot_health()
|
||||
entry = health.get(slot_id)
|
||||
if entry is None:
|
||||
raise HAProxyError(f"Slot not found in HAProxy stats: {slot_id}")
|
||||
return entry.scur
|
||||
|
||||
def read_slot_health(self) -> dict[str, SlotHealth]:
|
||||
"""Return full stats snapshot for all slots."""
|
||||
raise NotImplementedError
|
||||
"""Return full stats snapshot for all slots in the backend."""
|
||||
raw = self._run("show stat")
|
||||
reader = csv.DictReader(io.StringIO(raw))
|
||||
result: dict[str, SlotHealth] = {}
|
||||
for row in reader:
|
||||
pxname = row.get("# pxname", "").strip()
|
||||
svname = row.get("svname", "").strip()
|
||||
if pxname == self._backend and svname.startswith(self._slot_prefix):
|
||||
result[svname] = SlotHealth(
|
||||
status=row.get("status", "").strip(),
|
||||
scur=int(row.get("scur", "0")),
|
||||
qcur=int(row.get("qcur", "0")),
|
||||
)
|
||||
return result
|
||||
|
||||
def _run(self, command: str) -> str:
|
||||
"""Send a command to the HAProxy admin socket and return the response."""
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
try:
|
||||
sock.connect(self._socket_path)
|
||||
sock.sendall((command + "\n").encode())
|
||||
sock.shutdown(socket.SHUT_WR)
|
||||
chunks: list[bytes] = []
|
||||
while True:
|
||||
chunk = sock.recv(4096)
|
||||
if not chunk:
|
||||
break
|
||||
chunks.append(chunk)
|
||||
return b"".join(chunks).decode()
|
||||
except FileNotFoundError as e:
|
||||
raise HAProxyError(f"HAProxy socket not found: {self._socket_path}") from e
|
||||
except ConnectionRefusedError as e:
|
||||
raise HAProxyError(f"Connection refused to HAProxy socket: {self._socket_path}") from e
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
@staticmethod
|
||||
def _check_response(response: str, slot_id: str) -> None:
|
||||
"""Raise HAProxyError if the response indicates an error."""
|
||||
stripped = response.strip()
|
||||
if stripped.startswith(("No such", "Unknown")):
|
||||
raise HAProxyError(f"HAProxy error for {slot_id}: {stripped}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue