| """Microservice graph state used by the simulator.""" |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
| from typing import Any |
|
|
|
|
| @dataclass |
| class Service: |
| name: str |
| cpu: float = 25.0 |
| memory: float = 40.0 |
| latency_p50: float = 12.0 |
| latency_p99: float = 45.0 |
| error_rate: float = 0.001 |
| rps: float = 500.0 |
| dependencies: list[str] = field(default_factory=list) |
| config: dict[str, Any] = field(default_factory=dict) |
| deploy_history: list[dict[str, Any]] = field(default_factory=list) |
| logs: list[str] = field(default_factory=list) |
| current_fault: str | None = None |
| remediated: bool = False |
|
|
|
|
| class MicroserviceGraph: |
| """Small in-memory graph with enough dynamics for RL rollouts.""" |
|
|
| def __init__(self, services: dict[str, Service]): |
| self.services = services |
| self.elapsed_sec = 0 |
| self.event_log: list[dict[str, str]] = [] |
| self.root_cause_service = "" |
| self.root_cause_category = "" |
| self.required_remediations: set[str] = set() |
| self.completed_remediations: set[str] = set() |
|
|
| @classmethod |
| def topology(cls, name: str) -> "MicroserviceGraph": |
| base = { |
| "api-gateway": Service("api-gateway", dependencies=["checkout-service", "user-service"]), |
| "checkout-service": Service("checkout-service", dependencies=["payment-service", "inventory-service"]), |
| "payment-service": Service("payment-service", dependencies=["postgres-primary", "redis-cache"]), |
| "inventory-service": Service("inventory-service", dependencies=["postgres-primary"]), |
| "user-service": Service("user-service", dependencies=["postgres-primary"]), |
| "postgres-primary": Service("postgres-primary", cpu=35.0, memory=55.0, rps=1800.0), |
| "redis-cache": Service("redis-cache", cpu=18.0, memory=35.0, rps=2600.0), |
| } |
| if name == "deep_chain": |
| base["api-gateway"].dependencies = ["checkout-service"] |
| base["checkout-service"].dependencies = ["payment-service"] |
| base["payment-service"].dependencies = ["inventory-service"] |
| base["inventory-service"].dependencies = ["postgres-primary"] |
| elif name == "star": |
| for service in base.values(): |
| if service.name != "api-gateway": |
| service.dependencies = ["api-gateway"] |
| elif name == "mesh": |
| base["checkout-service"].dependencies.append("user-service") |
| base["payment-service"].dependencies.append("inventory-service") |
| base["inventory-service"].dependencies.append("redis-cache") |
| elif name == "bipartite": |
| base["api-gateway"].dependencies = ["checkout-service", "payment-service", "inventory-service"] |
| base["checkout-service"].dependencies = ["postgres-primary", "redis-cache"] |
| base["payment-service"].dependencies = ["postgres-primary", "redis-cache"] |
| base["inventory-service"].dependencies = ["postgres-primary", "redis-cache"] |
| elif name == "diamond": |
| base["api-gateway"].dependencies = ["checkout-service", "user-service"] |
| base["checkout-service"].dependencies = ["payment-service"] |
| base["user-service"].dependencies = ["payment-service"] |
| base["payment-service"].dependencies = ["postgres-primary"] |
| return cls(base) |
|
|
| def tick(self, seconds: int = 30) -> None: |
| self.elapsed_sec += seconds |
| for service in self.services.values(): |
| if service.current_fault and not service.remediated: |
| service.latency_p99 *= 1.03 |
| service.error_rate = min(0.99, service.error_rate + 0.015) |
|
|
| def service_names(self) -> list[str]: |
| return list(self.services) |
|
|
| def get(self, service_name: str) -> Service: |
| if service_name not in self.services: |
| raise KeyError(f"unknown service {service_name}") |
| return self.services[service_name] |
|
|
| def mark_remediated(self, action_key: str, service_name: str) -> bool: |
| key = f"{action_key}:{service_name}" |
| if key not in self.required_remediations: |
| return False |
| self.completed_remediations.add(key) |
| self.services[service_name].remediated = True |
| self.services[service_name].current_fault = None |
| self.services[service_name].cpu = min(self.services[service_name].cpu, 30.0) |
| self.services[service_name].memory = min(self.services[service_name].memory, 45.0) |
| self.services[service_name].latency_p50 = min(self.services[service_name].latency_p50, 25.0) |
| self.services[service_name].latency_p99 = min(self.services[service_name].latency_p99, 95.0) |
| self.services[service_name].error_rate = 0.001 |
| return self.is_recovered() |
|
|
| def is_recovered(self) -> bool: |
| return bool(self.required_remediations) and self.required_remediations.issubset(self.completed_remediations) |
|
|
| def synthetic_success_rate(self) -> float: |
| affected = self.services.get(self.root_cause_service) |
| if affected is None: |
| return 0.0 |
| if self.is_recovered(): |
| return 1.0 |
| return max(0.0, 1.0 - affected.error_rate) |
|
|
| def blast_radius_score(self) -> float: |
| affected = self.services.get(self.root_cause_service) |
| if affected is None: |
| return 0.0 |
| exposure = min(1.0, self.elapsed_sec / 900.0) |
| return max(0.0, 1.0 - (affected.error_rate * 0.65 + exposure * 0.35)) |
|
|
|
|