spb/maclustr-dispatch
Public
Python 95.8%
Shell 4.2%
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