SPB Git forge
29commits 1branches 0releases
684.0 KBsize
maindefault branch
2 days agolast push
Python 95.8% Shell 4.2%
4.5 KB · 99 lines python
Raw Blame History
1"""Sélection du nœud le plus efficient pour une app (ou un lot d'apps : plan)."""2from . import config, registry345def eligible(node, m, projected, exclude=()):6    """Retourne (ok, raison)."""7    a = node["alias"]8    pin = m["placement"].get("pin")9    if node.get("role") != "worker" and not (pin == a):10        return False, "rôle %s" % node.get("role")11    if not node.get("online"):12        return False, "hors ligne"13    if a in config.RESERVED and pin != a:14        return False, "réservé : %s" % config.RESERVED[a]15    if a in exclude or a in (m["placement"].get("avoid") or []):16        return False, "exclu"17    if pin and pin != a:18        return False, "épinglé sur %s" % pin19    missing = [r for r in m["requires"]["runtimes"] if not node["runtimes"].get(r)]20    if missing:21        return False, "runtimes manquants : %s" % ",".join(missing)22    need = max(float(m["requires"].get("ram_gb") or 1), m.get("ram_mb_observed", 512) * 1.5 / 1024)23    free = node["free_gb"] - projected[a]["ram_gb"]24    if free - need < 2.0:25        return False, "RAM insuffisante (%.1f G libres, besoin %.1f G)" % (free, need)26    if node["disk_free_gb"] < (m.get("size_mb", 0) / 1024) * 1.5 + 20:27        return False, "disque insuffisant"28    busy = set(node.get("ports", [])) | projected[a]["ports"]29    if not _is_home(a, m):30        for p in m["requires"].get("ports") or []:31            if p in busy:32                return False, "port %s occupé" % p33    return True, ""343536def _is_home(alias, m):37    """Le nœud héberge déjà cette app (source d'import ou registre) : ses propres ports ne la bloquent pas."""38    return alias == m.get("source_node") or alias == registry.node_of(m["app"])394041STAY_BONUS = 0.05   # évite de déplacer des Go de données pour un gain de score marginal424344def score(node, m, projected):45    a = node["alias"]46    need = max(float(m["requires"].get("ram_gb") or 1), m.get("ram_mb_observed", 512) * 1.5 / 1024)47    free_after = node["free_gb"] - projected[a]["ram_gb"] - need48    ram_ratio = max(0.0, free_after / max(node["ram_gb"], 1))49    idle = max(0.0, 1.0 - (node["load1"] + 0.5 * projected[a]["apps"]) / max(node["cores"], 1))50    disk = min(1.0, node["disk_free_gb"] / 500.0)51    cores = min(1.0, node["cores"] / 32.0)52    hosted = len(registry.apps_on(a)) + projected[a]["apps"]53    s = 0.45 * ram_ratio + 0.25 * idle + 0.10 * disk + 0.15 * cores - 0.04 * hosted54    if m["placement"].get("prefer") == a:55        s += 0.1556    if _is_home(a, m):57        s += STAY_BONUS58    return round(s, 4)596061def rank(m, scan, projected=None, exclude=()):62    projected = projected or empty_projection(scan)63    rows = []64    for a, node in scan.items():65        ok, why = eligible(node, m, projected, exclude)66        rows.append({"node": a, "ok": ok, "why": why, "score": score(node, m, projected) if ok else -1})67    rows.sort(key=lambda r: (-r["ok"], -r["score"]))68    return rows697071def empty_projection(scan):72    return {a: {"ram_gb": 0.0, "apps": 0, "ports": set()} for a in scan}737475def plan(manifests, scan, exclude=()):76    """Placement glouton : apps les plus lourdes d'abord, projection des ressources consommées."""77    projected = empty_projection(scan)78    order = sorted(manifests.values(), key=lambda m: -(m.get("ram_mb_observed", 512) + m.get("size_mb", 0) / 20.0))79    out = []80    for m in order:81        rows = rank(m, scan, projected, exclude)82        best = rows[0] if rows and rows[0]["ok"] else None83        if best:84            a = best["node"]85            need = max(float(m["requires"].get("ram_gb") or 1), m.get("ram_mb_observed", 512) * 1.5 / 1024)86            projected[a]["ram_gb"] += need87            projected[a]["apps"] += 188            projected[a]["ports"] |= set(m["requires"].get("ports") or [])89        pin = m["placement"].get("pin")90        blockers = []91        if not best:92            cand = [r for r in rows if not r["ok"] and (r["node"] == pin if pin else (scan[r["node"]].get("online") and scan[r["node"]].get("role") == "worker"))]93            cand.sort(key=lambda r: -(scan[r["node"]].get("ram_gb") or 0))94            blockers = [(r["node"], r["why"]) for r in cand][:8]95        out.append({"app": m["app"], "node": best["node"] if best else None, "score": best["score"] if best else None,96                    "current": registry.node_of(m["app"]) or m.get("source_node"), "pin": pin, "reason": m["placement"].get("reason", ""),97                    "size_mb": m.get("size_mb", 0), "alternatives": [(r["node"], r["score"]) for r in rows[1:4] if r["ok"]], "blockers": blockers})98    return out99