Mini Shell

Direktori : /opt/cloudlinux/venv/lib64/python3.11/site-packages/lvestats/plugins/generic/burster/
Upload File :
Current File : //opt/cloudlinux/venv/lib64/python3.11/site-packages/lvestats/plugins/generic/burster/__init__.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
import atexit
import contextlib
import time
import typing
from datetime import timedelta
from contextlib import ExitStack
from typing import TypedDict, Callable, TYPE_CHECKING, Generator

import sqlalchemy as sa

if TYPE_CHECKING:
    from lvestat import LVEStat
    from lvestats.plugins.generic.analyzers import LVEUsage

from lvestats.orm import BurstingEventType

from ._logs import logger
from .utils import bootstrap_gen
from .config import (
    StartupParams,
    PluginConfig,
    Config,
    ConfigUpdate,
    is_bursting_supported,
    MissingKeysInRawConfig,
)
from .common import (
    BurstingMultipliers,
    LveState,
    SerializedLveId,
    GetNormalLimits,
    ApplyLveSettings,
    AdjustStepData,
    read_normal_limits_from_proc,
    Timestamp,
    PyLveSettingsApplier,
)
from .overload import OverloadChecker, GetStats, read_times_from_proc
from .storage import (
    init_db_schema,
    load_bursting_enabled_intervals_from_db,
    events_saver_running,
    cleanup_running,
    InBurstingEventRow,
)
from .adjust import Adjuster, StepCalculator
from .lve_sm import LveStateManager
from .lves_tracker import LvesTracker, LveStateManagerFactory


class _ExecutePayload(TypedDict):
    lve_active_ids: list[SerializedLveId]
    stats: dict[SerializedLveId, 'LVEStat']
    lve_usage_5s: dict[SerializedLveId, 'LVEUsage']


class LveLimitsBurster:
    """
    Limits Burster plugin
    """

    def __init__(self, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)

        if is_bursting_supported():
            driver_gen = self._create_driver_gen()
            step = driver_gen.send

            # NOTE(vlebedev): It seems that plugins interface of lvestats does not contain any sane way
            #                 to get norified about server being stopped. Let's resort to hacks =/
            @atexit.register
            def cleanup():
                with contextlib.suppress(StopIteration, GeneratorExit):
                    driver_gen.close()
        else:
            logger.info('Bursting Limits feature is not supported in current environment')

            def step(_, /):
                pass
        self._step: Callable[[sa.engine.Engine | ConfigUpdate | _ExecutePayload], None] = step

    @bootstrap_gen
    def _create_driver_gen(self):
        # NOTE(vlebedev): This import requires some shared library to be present in order to succeed,
        #                 so deffer it until it's really needed to make unittests writing/running easier.
        from lveapi import PyLve  # pylint: disable=import-outside-toplevel

        # NOTE(vlebedev): This is supposed to be a composition root.
        # NOTE(vlebedev): Wait until all data required for proper startup is received.
        engine, initial_config = yield from StartupParams.wait()

        pylve = PyLve()
        if not pylve.initialize():
            raise RuntimeError('Failed to initialize PyLve!')

        with adjuster_machinery_running(
            initial_config=initial_config,
            engine=engine,
            get_normal_limits=read_normal_limits_from_proc,
            apply_lve_settings=PyLveSettingsApplier(
                pylve=pylve,
            ),
            read_stats=read_times_from_proc,
        ) as adjuster:
            logger.info('LveLimitsBurster initialized')

            while True:
                msg = yield
                if not isinstance(msg, dict):
                    logger.warning('Unexpected message type: %s', type(msg))
                    continue
                now = Timestamp(int(time.time()))
                lve_active_ids = msg.get('lve_active_ids', [])
                stats = msg.get('stats', {})
                try:
                    lve_usage_by_id = msg["lve_usages_5s"][-1]
                except (KeyError, IndexError):
                    lve_usage_by_id = {}
                adjuster.step(AdjustStepData(
                    now=now,
                    lve_active_ids=lve_active_ids,
                    stats=stats,
                    lve_usages_by_id=lve_usage_by_id,
                ))

    def set_config(self, config: PluginConfig) -> None:
        # NOTE(vlebedev): Currently config dict contains all the keys from _all_ .cfg files parsed by
        #                 lvestats. So there is no point as report fields not present in `Confg` typing
        #                 as "unknown" or something like that - they might well belong to some other plugin =/
        try:
            config_update = ConfigUpdate.from_plugin_config(config)
        except MissingKeysInRawConfig as e:
            logger.info('Missing config keys: %s', e.missing_raw_keys)
        else:
            self._step(config_update)

    def set_db_engine(self, engine: sa.engine.Engine) -> None:
        # NOTE(vlebedev): 'Engine' is thread safe, so there is no problem in requesting connections
        #                 from it on different threads. For more info have a look at this:
        #                 https://groups.google.com/g/sqlalchemy/c/t8i3RSKZGb0/m/QxWshAS3iKgJ
        self._step(engine)

    def execute(self, lve_data: _ExecutePayload) -> None:
        self._step(lve_data)


@contextlib.contextmanager
def adjuster_machinery_running(
    initial_config: Config,
    engine: sa.engine.Engine,
    apply_lve_settings: ApplyLveSettings,
    get_normal_limits: GetNormalLimits,
    read_stats: GetStats,
) -> Generator[Adjuster, None, None]:
    now = Timestamp(int(time.time()))
    cutoff = Timestamp(int(now - initial_config.bursting_quota_window.total_seconds()))
    init_db_schema(engine)
    lve_to_history = load_bursting_enabled_intervals_from_db(
        engine=engine,
        cutoff=typing.cast(Timestamp, cutoff),
        server_id=initial_config.server_id,
    )
    logger.debug('Loaded intervals: %s', len(lve_to_history))

    with ExitStack() as deffer:
        deffer.enter_context(cleanup_running(
            engine=engine,
            server_id=initial_config.server_id,
            cleanup_interval=timedelta(days=1),
            history_window=timedelta(days=30),
            fail_fast=initial_config.fail_fast,
        ))

        write_event = deffer.enter_context(events_saver_running(
            engine=engine,
            server_id=initial_config.server_id,
            dump_interval=initial_config.db_dump_period,
        ))

        adjuster = Adjuster(
            lves_tracker=(lves_tracker := LvesTracker(
                create_lve_manager=LveStateManagerFactory(
                    _lve_to_history=lve_to_history,
                    _apply_lve_settings=apply_lve_settings,
                    _quota=initial_config.bursting_quota,
                    _quota_window=initial_config.bursting_quota_window,
                    _bursting_multipliers=BurstingMultipliers(
                        initial_config.bursting_cpu_multiplier,
                        initial_config.bursting_io_multiplier,
                    ),
                    _fail_fast=initial_config.fail_fast,
                ),
                fail_fast=initial_config.fail_fast,
            )),
            get_normal_limits=get_normal_limits,
            step_calculator=StepCalculator(
                overload_threshold=1.0 - initial_config.idle_time_threshold,
            ),
            is_server_overloaded=OverloadChecker(
                idle_time_threshold=initial_config.idle_time_threshold,
                get_stats=read_stats,
                max_samples_number=initial_config.idle_time_samples,
            ),
            fail_fast=initial_config.fail_fast,
        )

        @lves_tracker.on_manager_added.register
        def on_new_lve_manager_created(manager: LveStateManager) -> None:
            lve_id = manager.lve_id

            @manager.on_state_changed.register
            def on_lve_state_chagned(old_state: LveState, new_state: LveState) -> None:
                assert old_state != new_state

                now = Timestamp(int(time.time()))

                if new_state == LveState.OVERUSING:
                    write_event(InBurstingEventRow(
                        lve_id=lve_id,
                        timestamp=now,
                        event_type=BurstingEventType.STARTED,
                    ))
                elif old_state == LveState.OVERUSING:
                    write_event(InBurstingEventRow(
                        lve_id=lve_id,
                        timestamp=now,
                        event_type=BurstingEventType.STOPPED,
                    ))

        yield adjuster

Zerion Mini Shell 1.0