import logging
from dataclasses import dataclass
from typing import Dict, Any
from olc.alg.alg_context import AlgContext
from olc.alg.algorithm import T, ParaName
from olc.alg.flow.flow_alg import (
FlowAbstractAlgorithm,
MIN_WAL,
MAX_WAL,
DELTA,
CURRENT_USAGE,
)
from olc.event.sny_event import Event
logger = logging.getLogger(__name__)
KP = "Kp"
KI = "Ki"
KD = "kd"
MIN_RESOURCE = "minResource"
MAX_RESOURCE = "maxResource"
@dataclass
class PidParams:
kp: float
ki: float
kd: float
@dataclass
class ResourceRange:
min_resource: float
max_resource: float
class PidAlgorithm(FlowAbstractAlgorithm):
NAME = "PID"
def __init__(self, auto: bool = False):
super().__init__(auto)
self.para_names[MIN_WAL] = ParaName(MIN_WAL, 0.1)
self.para_names[MAX_WAL] = ParaName(MAX_WAL, 3)
self.para_names[DELTA] = ParaName(DELTA, 5)
self.para_names[KP] = ParaName(KP, 0.5)
self.para_names[KI] = ParaName(KI, 0)
self.para_names[KD] = ParaName(KD, 0.5)
self.para_names[MIN_RESOURCE] = ParaName(MIN_RESOURCE, 0.5)
self.para_names[MAX_RESOURCE] = ParaName(MAX_RESOURCE, 0.5)
self.para_names[CURRENT_USAGE] = ParaName(
CURRENT_USAGE, "localresource.memory", True
)
self._pid_states = {}
Event.get_instance().olc_clean_rule.connect(self.subscribe_clean)
def _get_pid_state(self, group_name):
if group_name not in self._pid_states:
self._pid_states[group_name] = {
"integral": 0.0,
"lastError": 0.0,
"lastDirection": 0.0,
}
return self._pid_states[group_name]
def make_decision(self, alg_context: AlgContext[T]) -> T:
previous = alg_context.previous
init_result = alg_context.init_result
group_name = alg_context.group.name if alg_context.group else "default"
kp = alg_context.get_float_value_by_param(KP, self.para_names[KP].default_value)
ki = alg_context.get_float_value_by_param(KI, self.para_names[KI].default_value)
kd = alg_context.get_float_value_by_param(KD, self.para_names[KD].default_value)
delta = alg_context.get_float_value_by_param(
DELTA, self.para_names[DELTA].default_value
)
min_resource = alg_context.get_float_value_by_param(
MIN_RESOURCE, self.para_names[MIN_RESOURCE].default_value
)
max_resource = alg_context.get_float_value_by_param(
MAX_RESOURCE, self.para_names[MAX_RESOURCE].default_value
)
current_usage = alg_context.get_float_value_by_dynamic(CURRENT_USAGE)
current_usage = max(0.0, min(1.0, current_usage))
state = self._get_pid_state(group_name)
if current_usage < min_resource:
direction = 1
error = min_resource - current_usage
elif current_usage > max_resource:
direction = -1
error = current_usage - max_resource
else:
return previous
pid_out = self._calculate_output(
error,
PidParams(kp, ki, kd),
state,
direction,
ResourceRange(min_resource, max_resource),
)
error_rate = abs(error - state.get("lastError", 0))
if error_rate > 0.2:
pid_out *= 0.5
adjustment = direction * pid_out * init_result
if adjustment > 0:
adjustment = max(adjustment, delta)
elif adjustment < 0:
adjustment = min(adjustment, -delta)
new_threshold = previous + adjustment
min_wal = alg_context.get_float_value_by_param(
MIN_WAL, self.para_names[MIN_WAL].default_value
)
max_wal = alg_context.get_float_value_by_param(
MAX_WAL, self.para_names[MAX_WAL].default_value
)
new_threshold = max(
init_result * min_wal, min(init_result * max_wal, new_threshold)
)
return int(new_threshold)
@staticmethod
def _calculate_output(
error: float,
pid_params: PidParams,
state: Dict[str, float],
direction: int,
resource_range: ResourceRange,
):
if state.get("lastError") != 0 and state.get("lastDirection") != direction:
state["integral"] = 0
logger.debug("direction switching =%s, state=%s", direction, state)
state["lastDirection"] = direction
state["integral"] += error
integral_limit = (resource_range.max_resource - resource_range.min_resource) * 10
state["integral"] = max(-integral_limit, min(integral_limit, state["integral"]))
p = pid_params.kp * error
i = pid_params.ki * state["integral"]
d = pid_params.kd * (error - state["lastError"])
state["lastError"] = error
return p + i + d
def subscribe_clean(self, sender, **kwargs):
self._pid_states.clear()
logger.info("pid_states clear")
def check(self, params: dict[str, Any]) -> bool:
if not super().check(params):
return False
if not self.valid_min_max_param(MIN_RESOURCE, MAX_RESOURCE, params):
return False
if not self.valid_min_max_param(MIN_WAL, MAX_WAL, params):
return False
return True