PranavKK1201 commited on
Commit ·
4b5c463
1
Parent(s): 3a871a0
added VIP nodes, which have a higher weightage
Browse files- client.py +4 -0
- models.py +12 -0
- server/AntiAtropos_environment.py +24 -7
- simulator.py +15 -1
- stability.py +6 -1
client.py
CHANGED
|
@@ -79,10 +79,12 @@ class AntiAtroposEnv(
|
|
| 79 |
NodeObservation(
|
| 80 |
node_id=n.get("node_id", ""),
|
| 81 |
status=NodeStatus(n.get("status", NodeStatus.HEALTHY)),
|
|
|
|
| 82 |
queue_depth=n.get("queue_depth", 0),
|
| 83 |
latency_ms=n.get("latency_ms", 0.0),
|
| 84 |
incoming_request_rate=n.get("incoming_request_rate", 0.0),
|
| 85 |
cpu_utilization=n.get("cpu_utilization", 0.0),
|
|
|
|
| 86 |
done=n.get("done", False),
|
| 87 |
reward=n.get("reward", 0.0),
|
| 88 |
)
|
|
@@ -102,6 +104,8 @@ class AntiAtroposEnv(
|
|
| 102 |
step=obs_data.get("step", 0),
|
| 103 |
max_steps=obs_data.get("max_steps", 100),
|
| 104 |
sla_violations=obs_data.get("sla_violations", 0),
|
|
|
|
|
|
|
| 105 |
done=payload.get("done", False),
|
| 106 |
reward=payload.get("reward", 0.0),
|
| 107 |
)
|
|
|
|
| 79 |
NodeObservation(
|
| 80 |
node_id=n.get("node_id", ""),
|
| 81 |
status=NodeStatus(n.get("status", NodeStatus.HEALTHY)),
|
| 82 |
+
is_vip=n.get("is_vip", False),
|
| 83 |
queue_depth=n.get("queue_depth", 0),
|
| 84 |
latency_ms=n.get("latency_ms", 0.0),
|
| 85 |
incoming_request_rate=n.get("incoming_request_rate", 0.0),
|
| 86 |
cpu_utilization=n.get("cpu_utilization", 0.0),
|
| 87 |
+
importance_weight=n.get("importance_weight", 1.0),
|
| 88 |
done=n.get("done", False),
|
| 89 |
reward=n.get("reward", 0.0),
|
| 90 |
)
|
|
|
|
| 104 |
step=obs_data.get("step", 0),
|
| 105 |
max_steps=obs_data.get("max_steps", 100),
|
| 106 |
sla_violations=obs_data.get("sla_violations", 0),
|
| 107 |
+
invalid_action_count=obs_data.get("invalid_action_count", 0),
|
| 108 |
+
vip_failure_count=obs_data.get("vip_failure_count", 0),
|
| 109 |
done=payload.get("done", False),
|
| 110 |
reward=payload.get("reward", 0.0),
|
| 111 |
)
|
models.py
CHANGED
|
@@ -40,6 +40,7 @@ class NodeObservation(BaseModel):
|
|
| 40 |
"""Telemetry for a single service instance (node)."""
|
| 41 |
node_id: str
|
| 42 |
status: NodeStatus
|
|
|
|
| 43 |
|
| 44 |
# All numerical telemetry is normalized to [0, 1] for RL stability.
|
| 45 |
queue_depth: float = Field(
|
|
@@ -72,6 +73,12 @@ class NodeObservation(BaseModel):
|
|
| 72 |
description="Estimated CPU load [0.0, 1.0].",
|
| 73 |
)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
# Episode interaction fields (handled by framework)
|
| 76 |
done: bool = False
|
| 77 |
reward: float = 0.0
|
|
@@ -129,6 +136,11 @@ class ClusterObservation(BaseModel):
|
|
| 129 |
description="Number of forbidden actions (e.g. SHED_LOAD on critical nodes).",
|
| 130 |
)
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
nodes: list[NodeObservation]
|
| 133 |
|
| 134 |
# Episode interaction fields (handled by framework)
|
|
|
|
| 40 |
"""Telemetry for a single service instance (node)."""
|
| 41 |
node_id: str
|
| 42 |
status: NodeStatus
|
| 43 |
+
is_vip: bool = False
|
| 44 |
|
| 45 |
# All numerical telemetry is normalized to [0, 1] for RL stability.
|
| 46 |
queue_depth: float = Field(
|
|
|
|
| 73 |
description="Estimated CPU load [0.0, 1.0].",
|
| 74 |
)
|
| 75 |
|
| 76 |
+
importance_weight: float = Field(
|
| 77 |
+
default=1.0,
|
| 78 |
+
ge=0.0,
|
| 79 |
+
description="Business criticality weight. VIP nodes have higher impact on scoring.",
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
# Episode interaction fields (handled by framework)
|
| 83 |
done: bool = False
|
| 84 |
reward: float = 0.0
|
|
|
|
| 136 |
description="Number of forbidden actions (e.g. SHED_LOAD on critical nodes).",
|
| 137 |
)
|
| 138 |
|
| 139 |
+
vip_failure_count: int = Field(
|
| 140 |
+
default=0,
|
| 141 |
+
description="Number of failed VIP nodes in the current observation.",
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
nodes: list[NodeObservation]
|
| 145 |
|
| 146 |
# Episode interaction fields (handled by framework)
|
server/AntiAtropos_environment.py
CHANGED
|
@@ -156,24 +156,38 @@ class AntiAtroposEnvironment(Environment):
|
|
| 156 |
return total_capacity_units * COST_PER_CAPACITY_UNIT_PER_HOUR
|
| 157 |
|
| 158 |
def _avg_latency(self, nodes: list[dict]) -> float:
|
| 159 |
-
"""Computes mean latency across
|
| 160 |
-
|
| 161 |
-
if not active:
|
| 162 |
return float("inf")
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
|
| 165 |
def _error_rate(self, nodes: list[dict]) -> float:
|
| 166 |
-
"""Calculates
|
| 167 |
-
total_incoming = sum(n.get("incoming_request_rate", 0.0) for n in nodes)
|
| 168 |
if total_incoming <= 0:
|
| 169 |
return 0.0
|
| 170 |
|
| 171 |
# dropped_requests already includes both:
|
| 172 |
# - explicit shedding (SHED_LOAD)
|
| 173 |
# - traffic sent to FAILED nodes in simulator._update_queues
|
| 174 |
-
total_drops = sum(n.get("dropped_requests", 0.0) for n in nodes)
|
| 175 |
return min(1.0, total_drops / total_incoming)
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
def _build_observation(self) -> ClusterObservation:
|
| 178 |
"""Assembles the ClusterObservation from the current observed simulator state."""
|
| 179 |
node_obs = [
|
|
@@ -184,6 +198,8 @@ class AntiAtroposEnvironment(Environment):
|
|
| 184 |
latency_ms=min(1.0, max(0.0, float(n["latency_ms"]) / MAX_LATENCY_NORM)),
|
| 185 |
incoming_request_rate=min(1.0, max(0.0, float(n["incoming_request_rate"]) / MAX_REQUEST_RATE_NORM)),
|
| 186 |
cpu_utilization=min(1.0, max(0.0, float(n["cpu_utilization"]))),
|
|
|
|
|
|
|
| 187 |
done=False,
|
| 188 |
reward=0.0,
|
| 189 |
)
|
|
@@ -207,6 +223,7 @@ class AntiAtroposEnvironment(Environment):
|
|
| 207 |
max_steps=MAX_STEPS,
|
| 208 |
sla_violations=self._sla_violations,
|
| 209 |
invalid_action_count=self._sim.invalid_action_count,
|
|
|
|
| 210 |
done=False,
|
| 211 |
reward=0.0,
|
| 212 |
)
|
|
|
|
| 156 |
return total_capacity_units * COST_PER_CAPACITY_UNIT_PER_HOUR
|
| 157 |
|
| 158 |
def _avg_latency(self, nodes: list[dict]) -> float:
|
| 159 |
+
"""Computes importance-weighted mean latency across the cluster."""
|
| 160 |
+
if not nodes:
|
|
|
|
| 161 |
return float("inf")
|
| 162 |
+
|
| 163 |
+
weighted_latency = 0.0
|
| 164 |
+
total_weight = 0.0
|
| 165 |
+
for n in nodes:
|
| 166 |
+
weight = float(n.get("importance_weight", 1.0))
|
| 167 |
+
latency = MAX_LATENCY_NORM if n["status"] == NodeStatus.FAILED else float(n["latency_ms"])
|
| 168 |
+
weighted_latency += weight * latency
|
| 169 |
+
total_weight += weight
|
| 170 |
+
|
| 171 |
+
if total_weight <= 0:
|
| 172 |
+
return float("inf")
|
| 173 |
+
return weighted_latency / total_weight
|
| 174 |
|
| 175 |
def _error_rate(self, nodes: list[dict]) -> float:
|
| 176 |
+
"""Calculates an importance-weighted fraction of dropped or lost requests."""
|
| 177 |
+
total_incoming = sum(float(n.get("incoming_request_rate", 0.0)) * float(n.get("importance_weight", 1.0)) for n in nodes)
|
| 178 |
if total_incoming <= 0:
|
| 179 |
return 0.0
|
| 180 |
|
| 181 |
# dropped_requests already includes both:
|
| 182 |
# - explicit shedding (SHED_LOAD)
|
| 183 |
# - traffic sent to FAILED nodes in simulator._update_queues
|
| 184 |
+
total_drops = sum(float(n.get("dropped_requests", 0.0)) * float(n.get("importance_weight", 1.0)) for n in nodes)
|
| 185 |
return min(1.0, total_drops / total_incoming)
|
| 186 |
|
| 187 |
+
def _vip_failure_count(self, nodes: list[dict]) -> int:
|
| 188 |
+
"""Counts failed VIP nodes for reporting and diagnostics."""
|
| 189 |
+
return sum(1 for n in nodes if n.get("is_vip") and n["status"] == NodeStatus.FAILED)
|
| 190 |
+
|
| 191 |
def _build_observation(self) -> ClusterObservation:
|
| 192 |
"""Assembles the ClusterObservation from the current observed simulator state."""
|
| 193 |
node_obs = [
|
|
|
|
| 198 |
latency_ms=min(1.0, max(0.0, float(n["latency_ms"]) / MAX_LATENCY_NORM)),
|
| 199 |
incoming_request_rate=min(1.0, max(0.0, float(n["incoming_request_rate"]) / MAX_REQUEST_RATE_NORM)),
|
| 200 |
cpu_utilization=min(1.0, max(0.0, float(n["cpu_utilization"]))),
|
| 201 |
+
is_vip=bool(n.get("is_vip", False)),
|
| 202 |
+
importance_weight=float(n.get("importance_weight", 1.0)),
|
| 203 |
done=False,
|
| 204 |
reward=0.0,
|
| 205 |
)
|
|
|
|
| 223 |
max_steps=MAX_STEPS,
|
| 224 |
sla_violations=self._sla_violations,
|
| 225 |
invalid_action_count=self._sim.invalid_action_count,
|
| 226 |
+
vip_failure_count=self._vip_failure_count(self._nodes_true),
|
| 227 |
done=False,
|
| 228 |
reward=0.0,
|
| 229 |
)
|
simulator.py
CHANGED
|
@@ -59,6 +59,12 @@ T3_SURGE_MAGNITUDE: float = 70.0 # Extra req/tick added to node-1 and node-2
|
|
| 59 |
# In Task 3, these receive the surge. Forcing the agent to SCALE.
|
| 60 |
CRITICAL_NODES: list[str] = ["node-0", "node-1", "node-2"]
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
class NodeStatus(str, Enum):
|
| 64 |
HEALTHY = "HEALTHY"
|
|
@@ -70,9 +76,11 @@ class NodeStatus(str, Enum):
|
|
| 70 |
class NodeState:
|
| 71 |
node_id: str
|
| 72 |
status: NodeStatus = NodeStatus.HEALTHY
|
|
|
|
| 73 |
|
| 74 |
# Physics parameters
|
| 75 |
capacity: float = DEFAULT_CAPACITY
|
|
|
|
| 76 |
queue_depth: float = 0.0
|
| 77 |
latency_ms: float = BASE_LATENCY_MS
|
| 78 |
incoming_request_rate: float = 0.0
|
|
@@ -95,7 +103,9 @@ class NodeState:
|
|
| 95 |
return {
|
| 96 |
"node_id": self.node_id,
|
| 97 |
"status": self.status,
|
|
|
|
| 98 |
"capacity": self.capacity,
|
|
|
|
| 99 |
"queue_depth": int(self.queue_depth),
|
| 100 |
"latency_ms": round(self.latency_ms, 2),
|
| 101 |
"incoming_request_rate": round(self.incoming_request_rate, 2),
|
|
@@ -161,7 +171,11 @@ class ClusterSimulator:
|
|
| 161 |
|
| 162 |
def _reset_nodes(self) -> None:
|
| 163 |
self._nodes = [
|
| 164 |
-
NodeState(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
for i in range(self._n_nodes)
|
| 166 |
]
|
| 167 |
|
|
|
|
| 59 |
# In Task 3, these receive the surge. Forcing the agent to SCALE.
|
| 60 |
CRITICAL_NODES: list[str] = ["node-0", "node-1", "node-2"]
|
| 61 |
|
| 62 |
+
# VIP / business-critical node weights.
|
| 63 |
+
# node-0 is the payment portal, so its queue growth or failure matters more.
|
| 64 |
+
VIP_NODE_WEIGHTS: dict[str, float] = {
|
| 65 |
+
"node-0": 4.0,
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
|
| 69 |
class NodeStatus(str, Enum):
|
| 70 |
HEALTHY = "HEALTHY"
|
|
|
|
| 76 |
class NodeState:
|
| 77 |
node_id: str
|
| 78 |
status: NodeStatus = NodeStatus.HEALTHY
|
| 79 |
+
is_vip: bool = False
|
| 80 |
|
| 81 |
# Physics parameters
|
| 82 |
capacity: float = DEFAULT_CAPACITY
|
| 83 |
+
importance_weight: float = 1.0
|
| 84 |
queue_depth: float = 0.0
|
| 85 |
latency_ms: float = BASE_LATENCY_MS
|
| 86 |
incoming_request_rate: float = 0.0
|
|
|
|
| 103 |
return {
|
| 104 |
"node_id": self.node_id,
|
| 105 |
"status": self.status,
|
| 106 |
+
"is_vip": self.is_vip,
|
| 107 |
"capacity": self.capacity,
|
| 108 |
+
"importance_weight": self.importance_weight,
|
| 109 |
"queue_depth": int(self.queue_depth),
|
| 110 |
"latency_ms": round(self.latency_ms, 2),
|
| 111 |
"incoming_request_rate": round(self.incoming_request_rate, 2),
|
|
|
|
| 171 |
|
| 172 |
def _reset_nodes(self) -> None:
|
| 173 |
self._nodes = [
|
| 174 |
+
NodeState(
|
| 175 |
+
node_id=f"node-{i}",
|
| 176 |
+
is_vip=f"node-{i}" in VIP_NODE_WEIGHTS,
|
| 177 |
+
importance_weight=VIP_NODE_WEIGHTS.get(f"node-{i}", 1.0),
|
| 178 |
+
)
|
| 179 |
for i in range(self._n_nodes)
|
| 180 |
]
|
| 181 |
|
stability.py
CHANGED
|
@@ -75,7 +75,12 @@ def compute_lyapunov(nodes: list[dict]) -> float:
|
|
| 75 |
Returns:
|
| 76 |
Scalar Lyapunov energy ≥ 0.
|
| 77 |
"""
|
| 78 |
-
return float(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def compute_drift(v_prev: float, v_curr: float) -> float:
|
|
|
|
| 75 |
Returns:
|
| 76 |
Scalar Lyapunov energy ≥ 0.
|
| 77 |
"""
|
| 78 |
+
return float(
|
| 79 |
+
sum(
|
| 80 |
+
float(n.get("importance_weight", 1.0)) * (n["queue_depth"] ** 2)
|
| 81 |
+
for n in nodes
|
| 82 |
+
)
|
| 83 |
+
)
|
| 84 |
|
| 85 |
|
| 86 |
def compute_drift(v_prev: float, v_curr: float) -> float:
|