관리-도구
편집 파일: lve_sm.py
# coding=utf-8 # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2023 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT # pylint: disable=not-an-iterable import inspect import contextlib from dataclasses import dataclass from datetime import timedelta from typing import TypeAlias, NamedTuple, AbstractSet, Self from lvestats.orm import BurstingEventType from .utils import bootstrap_gen from ._logs import logger from .common import ( BurstingMultipliers, LveId, Timestamp, LveLimits, ApplyLveSettings, LveStats, InvalidStateError, LveUsage, LveState, calc_bursted_limits, infer_lve_state, ) from .history import LveHistory from .notify import Emitter, Signal class LveStateManager: @dataclass(frozen=True, slots=True) class Disappered(RuntimeError): now: Timestamp @dataclass(frozen=True, slots=True) class Burst: now: Timestamp @dataclass(frozen=True, slots=True) class Unburst: now: Timestamp # NOTE(vlebedev): Seems that namedtuples are ~20% faster to create compared to # classes - dataclasses or usual ones. class UpdateReadings(NamedTuple): now: Timestamp normal_limits: LveLimits | None stats: LveStats | None usage: LveUsage | None Request: TypeAlias = UpdateReadings | Disappered | Burst | Unburst @dataclass(frozen=True, slots=True) class _StateSwitched(RuntimeError): pass def __init__( self, now: Timestamp, lve_id: LveId, bursting_multipliers: BurstingMultipliers, initial_normal_limits: LveLimits, initial_stats: LveStats, initial_usage: LveUsage, quota: timedelta, quota_window: timedelta, apply_lve_settings: ApplyLveSettings, initial_history: LveHistory = LveHistory(), fail_fast: bool = True, ): if quota > quota_window: raise ValueError('Bursting quota must not exceed bursting quota window!') # NOTE(vlebedev): Constants start here. self._lve_id = lve_id self._bursting_multipliers = bursting_multipliers self._quota_sec = quota.total_seconds() self._quota_window_sec = quota_window.total_seconds() self._fail_fast = fail_fast # NOTE(vlebedev): Constants end here. # NOTE(vlebedev): "Input" variables start here. # "Input" variables are those that are read from external source (Request) and must not # be altered by internal logic. self._now = now self._normal_limits = initial_normal_limits self._stats = initial_stats self._usage = initial_usage self._state = infer_lve_state( initial_stats, initial_normal_limits, initial_usage, ) # NOTE(vlebedev): "Input" variables end here. # NOTE(vlebedev): "Output" variables start here. # "Output" variables are a channel that communicates internal state to external world. # Internal logic is responsible for mutating these vairables. self._history = initial_history # NOTE(vlebedev): "Output" variables end here. # NOTE(vlebedev): Utility stuff starts here. # These variables are not supposed to rewritten by internal logic. def apply_lve_settings_wrapper(cmd) -> None: if isinstance(cmd, self.Burst): limits = calc_bursted_limits(self._normal_limits, bursting_multipliers) elif isinstance(cmd, self.Unburst): limits = self._normal_limits else: raise ValueError(f'Unexpected command: {cmd}') apply_lve_settings(self._lve_id, limits) self._apply_lve_settings = apply_lve_settings_wrapper root_handler_gen = self._create_root_handler_gen() def step(request): response = root_handler_gen.send(request) if isinstance(response, Exception): raise response return response self.step = step self._on_state_changed = Emitter() @property def lve_id(self) -> LveId: return self._lve_id @property def now(self) -> Timestamp: return self._now @property def state(self) -> LveState: return self._state @property def is_bursted(self) -> bool: return self._state in {LveState.BURSTED, LveState.OVERUSING} @property def is_unbursted(self) -> bool: return self._state == LveState.UNBURSTED @property def is_overusing(self) -> bool: return self._state == LveState.OVERUSING @property def history_contains_overusing(self) -> bool: return self._history.contains_overusing @property def history(self) -> LveHistory: return self._history def check_quota_exceeded(self, now: float) -> bool: return self._history.get_overusing_duration(now) >= self._quota_sec @property def on_state_changed(self) -> Signal: return self._on_state_changed def trim_history(self, now: Timestamp) -> None: cutoff = Timestamp(int(now - self._quota_window_sec)) self._history = self._history.trim(cutoff) @bootstrap_gen def _create_root_handler_gen(self): # NOTE(vlebedev): This coroutine is solely responsible for modifying "input" variables. # No mutations of "input" variables must happen outside of it. state_to_handler_factory = { LveState.EXISTED: self._create_existed_state_handler_gen, LveState.UNBURSTED: self._create_unbursted_state_handler_gen, LveState.BURSTED: self._create_bursted_state_handler_gen, LveState.OVERUSING: self._create_overusing_state_handler_gen, } prev_state = next_state = self._state subhandler = state_to_handler_factory[next_state](next_state) request, response = None, None while True: try: request, response = (yield response), None # NOTE(vlebedev): "Input" variables mutations start here. # It's important that it happens before invoking state handler # so that state handler sees updated state. assert request.now >= self._now, "Time is not expected to go backward!" self._now = request.now if isinstance(request, self.UpdateReadings): if request.normal_limits is not None: self._normal_limits = request.normal_limits if request.stats is not None: self._stats = request.stats if request.usage is not None: self._usage = request.usage next_state = infer_lve_state( self._stats, self._normal_limits, self._usage, ) elif isinstance(request, self.Disappered): next_state = LveState.EXISTED self._state = next_state # NOTE(vlebedev): "Input" variables mutations end here. # TODO(vlebedev): Refactor subhandler management - it turned out to be quite messy. if inspect.getgeneratorstate(subhandler) != inspect.GEN_CLOSED: subhandler_exc = None if isinstance(request, self.Disappered): subhandler_exc = request elif next_state != prev_state: subhandler_exc = self._StateSwitched() if subhandler_exc is not None: try: with contextlib.suppress(StopIteration, type(subhandler_exc)): subhandler.throw(subhandler_exc) except (StopIteration, type(subhandler_exc)): logger.debug( 'LVE "%s": state subhandler for "%s" state terminated', self._lve_id, next_state ) except Exception: if self._fail_fast: raise logger.exception( 'LVE "%s": state subhandler for "%s" state unexpectedly failed during termination!', self._lve_id, next_state ) else: try: subhandler.close() except Exception: if self._fail_fast: raise logger.exception( 'LVE "%s": state subhandler for "%s" state did not terminate properly ' 'and failed during force-close!', self._lve_id, next_state ) if inspect.getgeneratorstate(subhandler) == inspect.GEN_CLOSED and next_state != prev_state: subhandler = state_to_handler_factory[next_state](next_state) logger.debug('LVE "%s": state subhandler for "%s" state created', self._lve_id, next_state) if inspect.getgeneratorstate(subhandler) != inspect.GEN_CLOSED: try: response = subhandler.send(request) except StopIteration: logger.debug('LVE "%s": state subhandler for "%s" state finished', self._lve_id, next_state) except Exception: if self._fail_fast: raise logger.exception( 'LVE "%s": state subhandler for "%s" state failed while handling "%s" request!', self._lve_id, next_state, request, ) # NOTE(vlebedev): Trigger signal handlers only after all internal state has been updated. if next_state != prev_state: logger.debug( 'LVE "%s": transitioned from "%s" to "%s" state', self._lve_id, prev_state, next_state, ) try: self._on_state_changed(prev_state, next_state) except Exception: if self._fail_fast: raise logger.exception('LVE "%s": some "on_state_changed" listeners failed!', self._lve_id) prev_state = next_state except Exception as e: if self._fail_fast: raise e logger.critical('LVE "%s": top handler unexpectadly failed!', self._lve_id, exc_info=e) @bootstrap_gen def _create_unbursted_state_handler_gen(self, prev_state: LveState): request, response = None, None try: while True: request, response = (yield response), None if isinstance(request, self.Unburst): response = InvalidStateError('Already unbursted!') elif isinstance(request, self.Burst): try: self._apply_lve_settings(request) except Exception as e: response = e else: break except self._StateSwitched: if self._state == LveState.BURSTED: logger.warning( 'LVE "%s": was set to bursted externally, without internal request from adjuster', self._lve_id, ) raise logger.debug('LVE "%s": waiting for bursting enabled confirmation', self._lve_id) try: yield from self._wait_for_confirmation_from_readings({LveState.BURSTED, LveState.OVERUSING}) except self.Disappered: logger.debug( 'LVE "%s": disappeared while waiting for bursting enabled or overusing started confirmation', self._lve_id, ) raise except self._StateSwitched: logger.warning( 'LVE "%s": was switched to "%s" while waiting for bursting enabled or overusing started confirmation', self._lve_id, self._state, ) raise assert self._state in {LveState.BURSTED, LveState.OVERUSING} logger.debug( 'LVE "%s": confirmed from readings that bursting switched to enabled%s', self._lve_id, " and overusing has started" if self._state == LveState.OVERUSING else "", ) @bootstrap_gen def _create_bursted_state_handler_gen(self, prev_state: LveState): request, response = None, None try: while True: request, response = (yield response), None if isinstance(request, self.Unburst): try: self._apply_lve_settings(request) except Exception as e: response = e else: break elif isinstance(request, self.Burst): response = InvalidStateError('Already bursted!') except self._StateSwitched: if self._state == LveState.UNBURSTED: logger.warning( 'LVE "%s": was set to unbursted externally, without internal request from adjuster', self._lve_id, ) raise logger.debug('LVE "%s": waiting for bursting disabled confirmation', self._lve_id) try: yield from self._wait_for_confirmation_from_readings({LveState.UNBURSTED}) except self.Disappered: logger.debug('LVE "%s": disappeared while waiting for bursting disabled confirmation', self._lve_id) raise except self._StateSwitched: logger.warning( 'LVE "%s": was switched to "%s" while waiting for bursting disabled confirmation', self._lve_id, self._state, ) raise logger.debug('LVE "%s": confirmed from readings that bursting switched to disabled', self._lve_id) @bootstrap_gen def _create_overusing_state_handler_gen(self, prev_state: LveState): # TODO(vlebedev): Do not use absolute time but relative to the latest event with `now`. self._history = self._history.append(self._now, BurstingEventType.STARTED) request, response = None, None try: try: while True: request, response = (yield response), None if isinstance(request, self.Unburst): try: self._apply_lve_settings(request) except Exception as e: response = e else: break elif isinstance(request, self.Burst): response = InvalidStateError('LVE is overusing i.e. is also already bursted!') except self._StateSwitched: if self._state == LveState.UNBURSTED: logger.warning( 'LVE "%s": was set to unbursted externally, without internal request from adjuster', self._lve_id, ) raise yield from self._wait_for_unbursted_confirmation_from_readings(prev_state) finally: self._history = self._history.append(self._now, BurstingEventType.STOPPED) @bootstrap_gen def _create_existed_state_handler_gen(self, prev_state: LveState): request, response = None, None try: while True: try: request, response = (yield response), None except self.Disappered: logger.warning( 'LVE "%s": disappeared again while waiting for LVE to re-appear', self._lve_id, ) if isinstance(request, (self.Burst, self.Unburst)): response = InvalidStateError('LVE does not exist!') except self._StateSwitched: logger.debug('LVE "%s": got back from the dead', self._lve_id) return def _wait_for_unbursted_confirmation_from_readings(self, prev_state: LveState): try: yield from self._wait_for_confirmation_from_readings({LveState.UNBURSTED}) except self.Disappered: logger.debug('LVE "%s": disappeared while waiting for bursting disabled confirmation', self._lve_id) raise except self._StateSwitched: logger.warning( 'LVE "%s": was switched to "%s" while waiting for bursting disabled confirmation', self._lve_id, self._state, ) raise logger.debug('LVE "%s": confirmed from readings that bursting switched to disabled', self._lve_id) def _wait_for_confirmation_from_readings(self, expected_states: AbstractSet[LveState]): request, response = None, None try: while True: request, response = (yield response), None if isinstance(request, (self.Burst, self.Unburst)): response = InvalidStateError('Waiting for confirmation from readings that limits have switched!') except self._StateSwitched: if self._state not in expected_states: raise _state_to_code = { LveState.EXISTED: "X", LveState.UNBURSTED: "U", LveState.BURSTED: "B", LveState.OVERUSING: "O", } assert _state_to_code.keys() == set(LveState) class LveStateSummary(NamedTuple): @classmethod def for_lve(cls, manager: LveStateManager) -> Self: return cls(manager.state, manager.history, manager.now) state: LveState history: LveHistory now: Timestamp def __str__(self) -> str: intervals = self.history.get_overusing_intervals(self.now) duration = self.history.get_overusing_duration(self.now) state_code = _state_to_code[self.state] return f"{state_code};[{intervals}~={round(duration)}s]"