From e8fa682926a36eab0bfdfcc69c91f3a4ded9dc15 Mon Sep 17 00:00:00 2001 From: Andrey Date: Tue, 16 Sep 2025 17:49:49 +0300 Subject: [PATCH] Remove PID management functionality from the bot, including related endpoints and references in the codebase. Update Dockerfile to optimize the build process by separating build and runtime stages. Enhance healthcheck implementation in Dockerfile to use Python instead of curl. Update README to reflect the removal of PID file management and related endpoints. --- Dockerfile | 57 +++++++----- Dockerfile.optimized | 61 ------------- README.md | 52 ----------- bot.py | 13 --- services/infrastructure/__init__.py | 2 - services/infrastructure/http_server.py | 93 +------------------- services/infrastructure/pid_manager.py | 117 ------------------------- 7 files changed, 38 insertions(+), 357 deletions(-) delete mode 100644 Dockerfile.optimized delete mode 100644 services/infrastructure/pid_manager.py diff --git a/Dockerfile b/Dockerfile index 0dfdd48..7a5c6ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,49 @@ -# Используем официальный Python образ -FROM python:3.9-slim +########################################### +# Этап 1: Сборщик (Builder) +########################################### +FROM python:3.9-slim as builder -# Устанавливаем рабочую директорию -WORKDIR /app - -# Устанавливаем системные зависимости +# Устанавливаем ВСЁ для сборки RUN apt-get update && apt-get install -y \ gcc \ g++ \ - curl \ + python3-dev \ && rm -rf /var/lib/apt/lists/* -# Копируем файл зависимостей +WORKDIR /app COPY requirements.txt . -# Устанавливаем Python зависимости -RUN pip install --no-cache-dir -r requirements.txt +# Устанавливаем зависимости в отдельную папку +RUN pip install --no-cache-dir --target /install -r requirements.txt -# Копируем исходный код приложения -COPY . . + +########################################### +# Этап 2: Финальный образ (Runtime) +########################################### +FROM python:3.9-alpine as runtime + +# Устанавливаем ТОЛЬКО НЕОБХОДИМЫЕ рантайм-зависимости +# curl НЕ НУЖЕН - используем встроенный Python для healthcheck +RUN apk add --no-cache \ + # Минимальные библиотеки для работы Python-пакетов + libstdc++ + +# Создаем пользователя (в Alpine Linux другие команды) +RUN addgroup -g 1001 app && \ + adduser -D -u 1001 -G app app + +WORKDIR /app + +# Копируем зависимости из сборщика +COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages # Создаем директории для данных -RUN mkdir -p database logs - -# Создаем пользователя для безопасности -RUN groupadd --gid 1001 app && \ - useradd --create-home --shell /bin/bash --uid 1001 --gid 1001 app && \ +RUN mkdir -p database logs && \ chown -R 1001:1001 /app + +# Копируем исходный код +COPY --chown=1001:1001 . . + USER 1001:1001 # Открываем порты @@ -36,9 +53,9 @@ EXPOSE 8081 ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 -# Добавляем healthcheck +# Healthcheck БЕЗ curl - используем встроенный Python HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:8081/health || exit 1 + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8081/health', timeout=5)" || exit 1 # Команда по умолчанию -CMD ["python", "main.py"] +CMD ["python", "main.py"] \ No newline at end of file diff --git a/Dockerfile.optimized b/Dockerfile.optimized deleted file mode 100644 index 7a5c6ef..0000000 --- a/Dockerfile.optimized +++ /dev/null @@ -1,61 +0,0 @@ -########################################### -# Этап 1: Сборщик (Builder) -########################################### -FROM python:3.9-slim as builder - -# Устанавливаем ВСЁ для сборки -RUN apt-get update && apt-get install -y \ - gcc \ - g++ \ - python3-dev \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app -COPY requirements.txt . - -# Устанавливаем зависимости в отдельную папку -RUN pip install --no-cache-dir --target /install -r requirements.txt - - -########################################### -# Этап 2: Финальный образ (Runtime) -########################################### -FROM python:3.9-alpine as runtime - -# Устанавливаем ТОЛЬКО НЕОБХОДИМЫЕ рантайм-зависимости -# curl НЕ НУЖЕН - используем встроенный Python для healthcheck -RUN apk add --no-cache \ - # Минимальные библиотеки для работы Python-пакетов - libstdc++ - -# Создаем пользователя (в Alpine Linux другие команды) -RUN addgroup -g 1001 app && \ - adduser -D -u 1001 -G app app - -WORKDIR /app - -# Копируем зависимости из сборщика -COPY --from=builder --chown=1001:1001 /install /usr/local/lib/python3.9/site-packages - -# Создаем директории для данных -RUN mkdir -p database logs && \ - chown -R 1001:1001 /app - -# Копируем исходный код -COPY --chown=1001:1001 . . - -USER 1001:1001 - -# Открываем порты -EXPOSE 8081 - -# Устанавливаем переменные окружения -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# Healthcheck БЕЗ curl - используем встроенный Python -HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8081/health', timeout=5)" || exit 1 - -# Команда по умолчанию -CMD ["python", "main.py"] \ No newline at end of file diff --git a/README.md b/README.md index 61ecf85..2144027 100644 --- a/README.md +++ b/README.md @@ -98,56 +98,6 @@ docker logs anon-bot curl http://localhost:8081/health ``` -## 📄 PID файл и мониторинг процесса - -AnonBot автоматически создает PID файл для отслеживания процесса и предоставляет детальную информацию о состоянии через HTTP эндпоинты. - -### PID файл - -- **Расположение**: `/tmp/anon_bot.pid` -- **Содержимое**: PID процесса бота -- **Автоматическое управление**: создается при запуске, удаляется при остановке -- **Проверка дублирования**: предотвращает запуск нескольких экземпляров - -### Эндпоинт /status - -Предоставляет детальную информацию о процессе: - -```bash -curl http://localhost:8081/status -``` - -**Пример ответа:** -```json -{ - "status": "running", - "pid": 12345, - "uptime": "2ч 15м", - "memory_usage_mb": 45.2, - "cpu_percent": 0.1, - "timestamp": 1705312200.5 -} -``` - -**Поля ответа:** -- `status` - статус процесса (running/stopped/not_found/error) -- `pid` - идентификатор процесса -- `uptime` - время работы в человекочитаемом формате -- `memory_usage_mb` - использование памяти в МБ -- `cpu_percent` - загрузка CPU в процентах -- `timestamp` - время ответа - -### Тестирование - -Для тестирования PID функционала можно использовать curl: - -```bash -# Проверка статуса процесса -curl http://localhost:8081/status - -# Проверка всех эндпоинтов -curl http://localhost:8081/ -``` ## 📁 Структура проекта @@ -208,7 +158,6 @@ AnonBot/ │ │ ├── logging_utils.py # Утилиты для контекстного логирования │ │ ├── metrics.py # Prometheus метрики │ │ ├── http_server.py # HTTP сервер для метрик -│ │ └── pid_manager.py # Менеджер PID файлов │ ├── rate_limiting/ # Rate limiting │ │ ├── __init__.py │ │ ├── rate_limit_config.py # Конфигурация rate limiting @@ -1186,7 +1135,6 @@ AnonBot поддерживает экспорт метрик в формате P - **http://localhost:8081/metrics** - экспорт метрик Prometheus - **http://localhost:8081/health** - проверка здоровья бота - **http://localhost:8081/ready** - готовность к работе (readiness probe) -- **http://localhost:8081/status** - информация о процессе (PID, uptime, использование ресурсов) - **http://localhost:8081/** - информация о сервисе ### Доступные метрики diff --git a/bot.py b/bot.py index 79f2e1a..9788d26 100644 --- a/bot.py +++ b/bot.py @@ -12,7 +12,6 @@ from config import config from loader import loader from services.infrastructure.http_server import start_http_server, stop_http_server from services.infrastructure.logger import get_logger -from services.infrastructure.pid_manager import get_pid_manager, cleanup_pid_file from services.infrastructure.metrics_updater import start_metrics_updater, stop_metrics_updater from config.constants import DEFAULT_HTTP_HOST, DEFAULT_HTTP_PORT @@ -23,7 +22,6 @@ logger = get_logger(__name__) async def main(): """Главная функция для запуска бота""" http_runner = None - pid_manager = None try: logger.info("🤖 Запуск бота анонимных вопросов...") @@ -31,13 +29,6 @@ async def main(): logger.info(f"💾 База данных: {config.DATABASE_PATH}") logger.info(f"👑 Администраторы: {config.ADMINS}") - # Создаем PID файл для отслеживания процесса - logger.info("📄 Создание PID файла...") - pid_manager = get_pid_manager("anon_bot") - if not pid_manager.create_pid_file(): - logger.error("❌ Не удалось создать PID файл, завершаем работу") - return - logger.info(f"✅ PID файл создан: {pid_manager.get_pid_file_path()}") # Запускаем HTTP сервер для метрик и health check logger.info("🌐 Запуск HTTP сервера для метрик...") @@ -65,10 +56,6 @@ async def main(): logger.info("🛑 Остановка HTTP сервера...") await stop_http_server(http_runner) - # Очищаем PID файл - if pid_manager: - logger.info("📄 Очистка PID файла...") - pid_manager.cleanup_pid_file() logger.info("🛑 Бот остановлен") diff --git a/services/infrastructure/__init__.py b/services/infrastructure/__init__.py index eeefe09..877c965 100644 --- a/services/infrastructure/__init__.py +++ b/services/infrastructure/__init__.py @@ -7,7 +7,6 @@ from .logger import get_logger, setup_logging from .metrics import MetricsService, get_metrics_service from .metrics_updater import MetricsUpdater, get_metrics_updater, start_metrics_updater, stop_metrics_updater from .db_metrics_decorator import track_db_operation, track_db_connection -from .pid_manager import PIDManager, get_pid_manager, cleanup_pid_file from .logging_decorators import ( log_function_call, log_business_event, log_fsm_transition, log_handler, log_service, log_business, log_fsm, @@ -25,7 +24,6 @@ __all__ = [ 'MetricsService', 'get_metrics_service', 'MetricsUpdater', 'get_metrics_updater', 'start_metrics_updater', 'stop_metrics_updater', 'track_db_operation', 'track_db_connection', - 'PIDManager', 'get_pid_manager', 'cleanup_pid_file', 'log_function_call', 'log_business_event', 'log_fsm_transition', 'log_handler', 'log_service', 'log_business', 'log_fsm', 'log_quiet', 'log_middleware', 'log_utility', diff --git a/services/infrastructure/http_server.py b/services/infrastructure/http_server.py index 9c61c61..fe501c7 100644 --- a/services/infrastructure/http_server.py +++ b/services/infrastructure/http_server.py @@ -31,7 +31,6 @@ class HTTPServer: self.app.router.add_get('/metrics', self.metrics_handler) self.app.router.add_get('/health', self.health_handler) self.app.router.add_get('/ready', self.ready_handler) - self.app.router.add_get('/status', self.status_handler) self.app.router.add_get('/', self.root_handler) async def metrics_handler(self, request: Request) -> Response: @@ -167,95 +166,6 @@ class HTTPServer: status=HTTP_STATUS_INTERNAL_SERVER_ERROR ) - async def status_handler(self, request: Request) -> Response: - """Handle /status endpoint for process status information.""" - try: - import os - import time - import psutil - - # Получаем PID текущего процесса - current_pid = os.getpid() - - try: - # Получаем информацию о процессе - process = psutil.Process(current_pid) - create_time = process.create_time() - uptime_seconds = time.time() - create_time - - # Логируем для диагностики - import datetime - create_time_str = datetime.datetime.fromtimestamp(create_time).strftime('%Y-%m-%d %H:%M:%S') - current_time_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - logger.info(f"Process PID {current_pid}: created at {create_time_str}, current time {current_time_str}, uptime {uptime_seconds:.1f}s") - - # Форматируем uptime - if uptime_seconds < 60: - uptime_str = f"{int(uptime_seconds)}с" - elif uptime_seconds < 3600: - minutes = int(uptime_seconds // 60) - uptime_str = f"{minutes}м" - elif uptime_seconds < 86400: - hours = int(uptime_seconds // 3600) - minutes = int((uptime_seconds % 3600) // 60) - uptime_str = f"{hours}ч {minutes}м" - else: - days = int(uptime_seconds // 86400) - hours = int((uptime_seconds % 86400) // 3600) - uptime_str = f"{days}д {hours}ч" - - # Проверяем, что процесс активен - if process.is_running(): - status = "running" - else: - status = "stopped" - - # Формируем ответ - response_data = { - "status": status, - "pid": current_pid, - "uptime": uptime_str, - "memory_usage_mb": round(process.memory_info().rss / 1024 / 1024, 2), - "cpu_percent": process.cpu_percent(), - "timestamp": time.time() - } - - import json - return Response( - text=json.dumps(response_data, ensure_ascii=False), - content_type='application/json', - status=200 - ) - - except psutil.NoSuchProcess: - # Процесс не найден - response_data = { - "status": "not_found", - "error": "Process not found", - "timestamp": time.time() - } - - import json - return Response( - text=json.dumps(response_data, ensure_ascii=False), - content_type='application/json', - status=404 - ) - - except Exception as e: - logger.error(f"Status check failed: {e}") - import json - response_data = { - "status": "error", - "error": str(e), - "timestamp": time.time() - } - - return Response( - text=json.dumps(response_data, ensure_ascii=False), - content_type='application/json', - status=500 - ) async def root_handler(self, request: Request) -> Response: """Обработчик корневого эндпоинта""" @@ -268,8 +178,7 @@ class HTTPServer: "endpoints": { "metrics": "/metrics", "health": "/health", - "ready": "/ready", - "status": "/status" + "ready": "/ready" }, "uptime": time.time() - self.start_time } diff --git a/services/infrastructure/pid_manager.py b/services/infrastructure/pid_manager.py deleted file mode 100644 index 32b227a..0000000 --- a/services/infrastructure/pid_manager.py +++ /dev/null @@ -1,117 +0,0 @@ -""" -PID менеджер для управления PID файлом процесса -""" -import os -import sys -from pathlib import Path -from typing import Optional - -from loguru import logger - - -class PIDManager: - """Менеджер для управления PID файлом процесса""" - - def __init__(self, service_name: str = "anon_bot", pid_dir: str = "/tmp"): - self.service_name = service_name - self.pid_dir = Path(pid_dir) - self.pid_file_path = self.pid_dir / f"{service_name}.pid" - self.pid: Optional[int] = None - - def create_pid_file(self) -> bool: - """Создать PID файл""" - try: - # Создаем директорию для PID файлов, если она не существует - self.pid_dir.mkdir(parents=True, exist_ok=True) - - # Проверяем, не запущен ли уже процесс - if self.pid_file_path.exists(): - try: - with open(self.pid_file_path, 'r') as f: - existing_pid = int(f.read().strip()) - - # Проверяем, жив ли процесс с этим PID - if self._is_process_running(existing_pid): - logger.error(f"Процесс {self.service_name} уже запущен с PID {existing_pid}") - return False - else: - logger.warning(f"Найден устаревший PID файл для {existing_pid}, удаляем его") - self.pid_file_path.unlink() - - except (ValueError, OSError) as e: - logger.warning(f"Не удалось прочитать существующий PID файл: {e}, удаляем его") - self.pid_file_path.unlink() - - # Получаем PID текущего процесса - self.pid = os.getpid() - - # Создаем PID файл - with open(self.pid_file_path, 'w') as f: - f.write(str(self.pid)) - - logger.info(f"PID файл создан: {self.pid_file_path} (PID: {self.pid})") - return True - - except Exception as e: - logger.error(f"Не удалось создать PID файл: {e}") - return False - - def cleanup_pid_file(self) -> None: - """Очистить PID файл""" - try: - if self.pid_file_path.exists(): - # Проверяем, что PID файл принадлежит нашему процессу - with open(self.pid_file_path, 'r') as f: - file_pid = int(f.read().strip()) - - if file_pid == self.pid: - self.pid_file_path.unlink() - logger.info(f"PID файл удален: {self.pid_file_path}") - else: - logger.warning(f"PID файл содержит другой PID ({file_pid}), не удаляем") - - except Exception as e: - logger.error(f"Ошибка при удалении PID файла: {e}") - - def get_pid(self) -> Optional[int]: - """Получить PID процесса""" - return self.pid - - def get_pid_file_path(self) -> Path: - """Получить путь к PID файлу""" - return self.pid_file_path - - - def _is_process_running(self, pid: int) -> bool: - """Проверить, запущен ли процесс с указанным PID""" - try: - # В Unix-системах отправляем сигнал 0 для проверки существования процесса - os.kill(pid, 0) - return True - except (OSError, ProcessLookupError): - return False - - - -# Глобальный экземпляр PID менеджера -_pid_manager: Optional[PIDManager] = None - - -def get_pid_manager(service_name: str = "anon_bot", pid_dir: str = "/tmp") -> PIDManager: - """Получить экземпляр PID менеджера""" - global _pid_manager - if _pid_manager is None: - _pid_manager = PIDManager(service_name, pid_dir) - return _pid_manager - - -def create_pid_file(service_name: str = "anon_bot", pid_dir: str = "/tmp") -> bool: - """Создать PID файл""" - pid_manager = get_pid_manager(service_name, pid_dir) - return pid_manager.create_pid_file() - - -def cleanup_pid_file() -> None: - """Очистить PID файл""" - if _pid_manager: - _pid_manager.cleanup_pid_file()