diff --git a/CHANGELOG.md b/CHANGELOG.md index c9f42551..df055ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ # Changelog +## [0.9.21] - 2025-09-21 + +### πŸ”§ Redis Connection Pool Fix +- **πŸ› Fixed "max number of clients reached" error**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° критичСская ошибка ΠΏΡ€Π΅Π²Ρ‹ΡˆΠ΅Π½ΠΈΡ Π»ΠΈΠΌΠΈΡ‚Π° соСдинСний Redis + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½ `aioredis.ConnectionPool` с ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½ΠΈΠ΅ΠΌ `max_connections=20` для 5 микросСрвисов + - Π Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½ΠΎ ΠΏΠ΅Ρ€Π΅ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ соСдинСний вмСсто создания Π½ΠΎΠ²Ρ‹Ρ… для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ запроса + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΎ ΠΏΡ€Π°Π²ΠΈΠ»ΡŒΠ½ΠΎΠ΅ Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ connection pool ΠΏΡ€ΠΈ shutdown прилоТСния + - Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π° ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок соСдинСния с автоматичСским ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ΠΌ +- **πŸ“Š Health Monitoring**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½ `/health` endpoint для ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° состояния Redis + - ΠžΡ‚ΠΎΠ±Ρ€Π°ΠΆΠ°Π΅Ρ‚ количСство Π°ΠΊΡ‚ΠΈΠ²Π½Ρ‹Ρ… соСдинСний, использованиС памяти, Π²Π΅Ρ€ΡΠΈΡŽ Redis + - ΠŸΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ Π΄ΠΈΠ°Π³Π½ΠΎΡΡ‚ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ с соСдинСниями Π² production +- **πŸ”„ Connection Management**: ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½ΠΎ ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ соСдинСниями + - Один connection pool для всСх ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ Redis + - АвтоматичСскоС ΠΏΠ΅Ρ€Π΅ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ ΠΏΡ€ΠΈ ΠΏΠΎΡ‚Π΅Ρ€Π΅ соСдинСния + - ΠšΠΎΡ€Ρ€Π΅ΠΊΡ‚Π½ΠΎΠ΅ Π·Π°ΠΊΡ€Ρ‹Ρ‚ΠΈΠ΅ всСх соСдинСний ΠΏΡ€ΠΈ остановкС прилоТСния + +### πŸ§ͺ TypeScript Warnings Fix +- **🏷️ Type Annotations**: Π”ΠΎΠ±Π°Π²Π»Π΅Π½Ρ‹ явныС Ρ‚ΠΈΠΏΡ‹ для устранСния implicit `any` ошибок + - Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Ρ‹ Ρ‚ΠΈΠΏΡ‹ Π² `RolesModal.tsx` для ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€ΠΎΠ² `roleName` ΠΈ `r` + - УстранСны всС TypeScript warnings Π² admin panel + +### πŸš€ CI/CD Improvements +- **⚑ Mypy Optimization**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Π° ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ° OOM (exit status 137) Π² CI + - ΠžΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Π½ `mypy.ini` с ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠ΅ΠΌ тяТСлых зависимостСй + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½ `dmypy` с fallback Π½Π° ΠΎΠ±Ρ‹Ρ‡Π½Ρ‹ΠΉ `mypy` + - ΠžΠ³Ρ€Π°Π½ΠΈΡ‡Π΅Π½Π° ΠΎΠ±Π»Π°ΡΡ‚ΡŒ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΈ Ρ‚ΠΈΠΏΠΎΠ² Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΊΡ€ΠΈΡ‚ΠΈΡ‡Π½Ρ‹ΠΌΠΈ модулями + - Π”ΠΎΠ±Π°Π²Π»Π΅Π½Π° ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° доступной памяти ΠΏΠ΅Ρ€Π΅Π΄ запуском mypy +- **🐳 Docker Build**: Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½Ρ‹ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡ‹ с PyTorch зависимостями + - Π£Π²Π΅Π»ΠΈΡ‡Π΅Π½ `UV_HTTP_TIMEOUT=300` для Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ Π±ΠΎΠ»ΡŒΡˆΠΈΡ… ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠ² + - УстановлСн `TORCH_CUDA_AVAILABLE=0` для прСдотвращСния CUDA зависимостСй + - Π£ΠΏΡ€ΠΎΡ‰Π΅Π½Ρ‹ зависимости PyTorch Π² `pyproject.toml` для совмСстимости с Python 3.13 + ## [0.9.20] - 2025-09-10 ### πŸ› Authors Endpoint Critical Fix diff --git a/main.py b/main.py index cafbd5aa..ed153f82 100644 --- a/main.py +++ b/main.py @@ -143,6 +143,18 @@ async def spa_handler(request: Request) -> Response: return JSONResponse({"error": "Admin panel not built"}, status_code=404) +async def health_handler(request: Request) -> Response: + """Health check endpoint with Redis monitoring""" + try: + redis_info = await redis.get_info() + return JSONResponse( + {"status": "healthy", "redis": {"connected": redis.is_connected, "ping": await redis.ping(), **redis_info}} + ) + except Exception as e: + logger.error(f"Health check failed: {e}") + return JSONResponse({"status": "unhealthy", "error": str(e)}, status_code=500) + + async def shutdown() -> None: """ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΊΠ° сСрвСра ΠΈ освобоТдСниС рСсурсов""" logger.info("ΠžΡΡ‚Π°Π½ΠΎΠ²ΠΊΠ° сСрвСра") @@ -293,6 +305,8 @@ app = Starlette( # OAuth ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚Ρ‹ Route("/oauth/{provider}", oauth_login, methods=["GET"]), Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]), + # Health check endpoint + Route("/health", health_handler, methods=["GET"]), # БтатичСскиС Ρ„Π°ΠΉΠ»Ρ‹ (CSS, JS, изобраТСния) Mount("/assets", app=StaticFiles(directory=str(DIST_DIR / "assets"))), # ΠšΠΎΡ€Π½Π΅Π²ΠΎΠΉ ΠΌΠ°Ρ€ΡˆΡ€ΡƒΡ‚ для Π°Π΄ΠΌΠΈΠ½-ΠΏΠ°Π½Π΅Π»ΠΈ diff --git a/storage/redis.py b/storage/redis.py index f64fda68..022b059e 100644 --- a/storage/redis.py +++ b/storage/redis.py @@ -23,30 +23,28 @@ class RedisService: self._client: aioredis.Redis | None = None self._redis_url = redis_url # Π˜ΡΠΏΡ€Π°Π²Π»Π΅Π½ΠΎ Π½Π° _redis_url self._is_available = aioredis is not None + self._connection_pool: aioredis.ConnectionPool | None = None if not self._is_available: logger.warning("Redis is not available - aioredis not installed") async def close(self) -> None: - """Close Redis connection""" + """Close Redis connection and connection pool""" if self._client: - # Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π΅ соСдинСниС Ссли Π΅ΡΡ‚ΡŒ try: await self._client.close() except Exception as e: - logger.error(f"Error closing Redis connection: {e}") - # Для тСста disconnect_exception_handling - if str(e) == "Disconnect error": - # БохраняСм ΠΊΠ»ΠΈΠ΅Π½Ρ‚ для тСста - self._last_close_error = e - raise - # Для Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ просто Π»ΠΎΠ³ΠΈΡ€ΡƒΠ΅ΠΌ + logger.error(f"Error closing Redis client: {e}") finally: - # БохраняСм ΠΊΠ»ΠΈΠ΅Π½Ρ‚ для тСста disconnect_exception_handling - if hasattr(self, "_last_close_error") and str(self._last_close_error) == "Disconnect error": - pass - else: - self._client = None + self._client = None + + if self._connection_pool: + try: + await self._connection_pool.disconnect() + except Exception as e: + logger.error(f"Error closing Redis connection pool: {e}") + finally: + self._connection_pool = None # ДобавляСм ΠΌΠ΅Ρ‚ΠΎΠ΄ disconnect ΠΊΠ°ΠΊ алиас для close async def disconnect(self) -> None: @@ -54,16 +52,13 @@ class RedisService: await self.close() async def connect(self) -> bool: - """Connect to Redis""" + """Connect to Redis with connection pooling""" try: - if self._client: - # Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰Π΅Π΅ соСдинСниС - try: - await self._client.close() - except Exception as e: - logger.error(f"Error closing Redis connection: {e}") + # Π—Π°ΠΊΡ€Ρ‹Π²Π°Π΅ΠΌ ΡΡƒΡ‰Π΅ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠ΅ соСдинСния + await self.close() - self._client = aioredis.from_url( + # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ connection pool + self._connection_pool = aioredis.ConnectionPool.from_url( self._redis_url, encoding="utf-8", decode_responses=True, @@ -71,16 +66,20 @@ class RedisService: socket_timeout=5, retry_on_timeout=True, health_check_interval=30, + max_connections=20, # 20 соСдинСний + retry_on_error=[ConnectionError, TimeoutError], ) + + # Π‘ΠΎΠ·Π΄Π°Π΅ΠΌ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ с connection pool + self._client = aioredis.Redis(connection_pool=self._connection_pool) + # Test connection await self._client.ping() - logger.info("Successfully connected to Redis") + logger.info("Successfully connected to Redis with connection pooling") return True except Exception: logger.exception("Failed to connect to Redis") - if self._client: - await self._client.close() - self._client = None + await self.close() return False @property @@ -95,9 +94,12 @@ class RedisService: return None async def execute(self, command: str, *args: Any) -> Any: - """Execute Redis command with reconnection logic""" + """Execute Redis command with connection pooling""" if not self.is_connected: - await self.connect() + logger.warning("Redis not connected, attempting to connect...") + if not await self.connect(): + logger.error("Failed to connect to Redis") + return None try: cmd_method = getattr(self._client, command.lower(), None) @@ -122,8 +124,8 @@ class RedisService: except Exception: logger.exception("Redis retry failed") return None - except Exception: - logger.exception("Redis command failed") + except Exception as e: + logger.error(f"Redis command {command} failed: {e}") return None async def get(self, key: str) -> str | bytes | None: @@ -251,6 +253,22 @@ class RedisService: except Exception: return False + async def get_info(self) -> dict[str, Any]: + """Get Redis server info""" + if not self.is_connected or self._client is None: + return {} + try: + info = await self._client.info() + return { + "connected_clients": info.get("connected_clients", 0), + "used_memory": info.get("used_memory_human", "0B"), + "redis_version": info.get("redis_version", "unknown"), + "uptime_in_seconds": info.get("uptime_in_seconds", 0), + } + except Exception as e: + logger.error(f"Failed to get Redis info: {e}") + return {} + async def execute_pipeline(self, commands: list[tuple[str, tuple[Any, ...]]]) -> list[Any]: """ ВыполняСт список ΠΊΠΎΠΌΠ°Π½Π΄ Ρ‡Π΅Ρ€Π΅Π· pipeline для Π»ΡƒΡ‡ΡˆΠ΅ΠΉ ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ.