atropos/atroposlib/orchestration/controller.py

67 lines
2.5 KiB
Python

import logging
import math
from typing import Optional, Dict, Any, List
from .metrics import WorkloadMetrics
logger = logging.getLogger(__name__)
class ScalingController:
"""
Decides the "Desired Actor Count" based on workload metrics.
Uses a dampened calculation with hysteresis to avoid flapping.
"""
def __init__(
self,
min_actors: int = 1,
max_actors: int = 20,
target_pressure: float = 1.0,
scaling_threshold: float = 0.2, # ±20%
cooldown_seconds: int = 60,
max_step_change: int = 4
):
self.min_actors = min_actors
self.max_actors = max_actors
self.target_pressure = target_pressure
self.scaling_threshold = scaling_threshold
self.cooldown_seconds = cooldown_seconds
self.max_step_change = max_step_change
self.last_action_timestamp = 0
self.current_desired = min_actors
def calculate_desired(self, metrics: WorkloadMetrics, current_actors: int) -> int:
"""
Decides the next target for the number of environment actors.
"""
now = metrics.timestamp
pressure = metrics.rollout_pressure
# 1. Check cooldown
if now - self.last_action_timestamp < self.cooldown_seconds:
return self.current_desired
# 2. Sensitivity check (Hysteresis)
# If work is roughly satisfying target, don't change anything.
if abs(pressure - self.target_pressure) < self.scaling_threshold:
return self.current_desired
# 3. Calculate target
# Target = Current * (Current_Pressure / Ideal_Pressure)
# This is a dampened proportional controller.
raw_target = math.ceil(current_actors * (pressure / self.target_pressure))
# 4. Apply step constraints (Rate Limiting)
# Don't add/remove more than max_step_change in a single move.
diff = raw_target - current_actors
if abs(diff) > self.max_step_change:
raw_target = current_actors + (self.max_step_change if diff > 0 else -self.max_step_change)
# 5. Apply world bounds
final_target = max(self.min_actors, min(self.max_actors, raw_target))
if final_target != current_actors:
self.last_action_timestamp = now
self.current_desired = final_target
logger.info(f"Controller DECISION: Scale {current_actors} -> {final_target} (Pressure: {pressure:.2f})")
return final_target