fester/backend/recommend.py

129 lines
3.4 KiB
Python
Executable File

from __future__ import annotations
import logging
from typing import Any, List, Mapping, Optional
from backend.governor import thermal_cap
from backend.cache_local import make_build_key, cache_hit
log = logging.getLogger(__name__)
# ----------------------------
# analyze cluster health
# ----------------------------
def cluster_heat(nodes: Optional[List[Mapping[str, Any]]]) -> float:
"""Return the mean 1-minute load across ``nodes`` (0..N)."""
if not nodes:
return 0.5
total = 0.0
count = 0
for n in nodes:
agent = n.get("agent") or {}
load = agent.get("load", "1 1 1")
try:
total += float(str(load).split()[0])
except (ValueError, IndexError, AttributeError):
log.debug("could not parse load=%r from node %s", load, n.get("name"))
total += 1.0
count += 1
return total / max(count, 1)
# ----------------------------
# thermal risk scoring
# ----------------------------
def risk_level(node: Mapping[str, Any]) -> str:
"""Return HIGH_RISK / MODERATE / SAFE for a single node."""
agent = node.get("agent") or {}
cap = thermal_cap(agent)
if cap < 0.3:
return "HIGH_RISK"
elif cap < 0.6:
return "MODERATE"
return "SAFE"
# ----------------------------
# predict build cost
# ----------------------------
def estimate_build_cost(nodes: List[Mapping[str, Any]]) -> float:
"""Return the expected build-cost score (lower = cheaper)."""
cost = 0.0
for n in nodes:
agent = n.get("agent") or {}
load = agent.get("load", "1 1 1")
try:
cost += float(str(load).split()[0])
except (ValueError, IndexError, AttributeError):
log.debug("could not parse load=%r from node %s", load, n.get("name"))
cost += 1.0
return cost / max(len(nodes), 1)
# ----------------------------
# recommendation engine
# ----------------------------
def recommend(project: Mapping[str, Any], snapshot: Mapping[str, Any],
nodes: List[Mapping[str, Any]]) -> dict:
recommendations: dict = {
"build_now": True,
"risk": "SAFE",
"cache_likely": False,
"preferred_nodes": [],
"reason": []
}
# ----------------------------
# cache prediction
# ----------------------------
key_sample = make_build_key(
snapshot["hash"],
list(project["targets"].keys())[0],
project.get("build_env", {})
)
if cache_hit(key_sample):
recommendations["cache_likely"] = True
recommendations["reason"].append("Cache hit likely for current snapshot")
# ----------------------------
# cluster load analysis
# ----------------------------
heat = cluster_heat(nodes)
if heat > 3.0:
recommendations["build_now"] = False
recommendations["reason"].append("Cluster overloaded - recommend delay")
# ----------------------------
# node filtering
# ----------------------------
safe_nodes = []
for n in nodes:
if risk_level(n) == "SAFE":
safe_nodes.append(n["name"])
recommendations["preferred_nodes"] = safe_nodes
# ----------------------------
# final risk classification
# ----------------------------
if heat > 4.0:
recommendations["risk"] = "HIGH_RISK"
elif heat > 2.5:
recommendations["risk"] = "MODERATE"
return recommendations