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