haxorqt
Home
Console
Upload
information
Create File
Create Folder
About
:
/
opt
/
cloudlinux
/
venv
/
lib
/
python3.11
/
site-packages
/
xray
/
internal
/
Filename :
fpm_utils.py
back
Copy
# -*- coding: utf-8 -*- # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2021 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT import logging import os from dataclasses import dataclass from typing import Optional from xray import gettext as _ from .constants import fpm_stat_storage, fpm_reload_timeout from .exceptions import XRayError from .utils import dbm_storage, filelock, timestamp logger = logging.getLogger('fpm_utils') @dataclass class FPMReloadController: """ Class to control the frequency of FPM service reloading """ fpm_service: str @property def latest_reload(self) -> Optional[int]: """ Get the timestamp of latest reload of FPM service """ try: with dbm_storage(fpm_stat_storage) as _fpm_reload_stat: try: return int(_fpm_reload_stat[self.fpm_service].decode()) except KeyError: return None except RuntimeError as e: raise XRayError(_('Failed to get the timestamp of latest FPM reload: %s') % str(e), flag='warning') def save_latest_reload(self): """ Save the timestamp of latest reload of FPM service """ try: # save timestamp of latest reload of corresponding service with dbm_storage(fpm_stat_storage) as _fpm_reload_stat: _fpm_reload_stat[self.fpm_service] = str(timestamp()) except RuntimeError as e: raise XRayError( _('Failed to save the timestamp of latest FPM reload %s, but main operation executed') % str(e), flag='warning', ) def delta(self) -> float: """ Get the time delta between current time and saved timestamp """ try: # we store timestamp in seconds, # but timeout -- fpm_reload_timeout -- in minutes return (timestamp() - self.latest_reload) / 60 except (TypeError, XRayError): # 1 hour, # big enough for non-blocking flow in case of unexpected errors return 60.0 def restrict(self) -> bool: """ Check if FPM reload should be blocked """ return self.delta() <= fpm_reload_timeout def reserve_reload_slot(self) -> bool: """ Atomically decide whether THIS caller may perform the throttled reload and, if so, reserve the slot before returning. ``restrict`` (the check) and ``save_latest_reload`` (the reserve) must run as a single critical section shared across processes. Otherwise several concurrent user-mode user-agent processes can each observe an expired window, all pass the check, and each run the service reload -- preserving the server-wide DoS this throttle is meant to close (a tenant who owns a mod_php DirectAdmin domain firing N parallel start/stop requests triggers N global `service httpd restart`s). A cross-process exclusive flock makes check-and-reserve atomic: at most one caller per window records the timestamp and returns True; the losers re-read the just-saved timestamp under the same lock and return False. Reserve-before-act (the timestamp is saved BEFORE the reload runs, not after) is deliberate: the reservation must be visible to concurrent callers immediately, and a reload that later fails should still consume the window (the throttle errs toward FEWER restarts). Root/operator callers do not use this method -- they are never throttled. Returns True if the slot was reserved (caller must proceed with the reload); False if throttled (caller must skip). Any lock/storage failure fails safe (returns False) rather than risk an unthrottled restart. """ lock_path = '%s.%s.lock' % (fpm_stat_storage, self.fpm_service) try: lock_fd = os.open(lock_path, os.O_CREAT | os.O_RDWR | os.O_CLOEXEC, 0o600) except OSError as e: logger.warning('Failed to open reload lock %s: %s', lock_path, e) return False try: with filelock(lock_fd): if self.restrict(): return False self.save_latest_reload() return True except XRayError as e: # filelock exhausted its retries, or save_latest_reload failed. logger.warning('Failed to reserve reload slot for %s: %s', self.fpm_service, e) return False finally: os.close(lock_fd)