129 lines
3.4 KiB
Python
Executable File
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
|