### 🔧 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
This commit is contained in:
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
|||||||
# Changelog
|
# 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
|
## [0.9.20] - 2025-09-10
|
||||||
|
|
||||||
### 🐛 Authors Endpoint Critical Fix
|
### 🐛 Authors Endpoint Critical Fix
|
||||||
|
|||||||
14
main.py
14
main.py
@@ -143,6 +143,18 @@ async def spa_handler(request: Request) -> Response:
|
|||||||
return JSONResponse({"error": "Admin panel not built"}, status_code=404)
|
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:
|
async def shutdown() -> None:
|
||||||
"""Остановка сервера и освобождение ресурсов"""
|
"""Остановка сервера и освобождение ресурсов"""
|
||||||
logger.info("Остановка сервера")
|
logger.info("Остановка сервера")
|
||||||
@@ -293,6 +305,8 @@ app = Starlette(
|
|||||||
# OAuth маршруты
|
# OAuth маршруты
|
||||||
Route("/oauth/{provider}", oauth_login, methods=["GET"]),
|
Route("/oauth/{provider}", oauth_login, methods=["GET"]),
|
||||||
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
|
Route("/oauth/{provider}/callback", oauth_callback, methods=["GET"]),
|
||||||
|
# Health check endpoint
|
||||||
|
Route("/health", health_handler, methods=["GET"]),
|
||||||
# Статические файлы (CSS, JS, изображения)
|
# Статические файлы (CSS, JS, изображения)
|
||||||
Mount("/assets", app=StaticFiles(directory=str(DIST_DIR / "assets"))),
|
Mount("/assets", app=StaticFiles(directory=str(DIST_DIR / "assets"))),
|
||||||
# Корневой маршрут для админ-панели
|
# Корневой маршрут для админ-панели
|
||||||
|
|||||||
@@ -23,47 +23,42 @@ class RedisService:
|
|||||||
self._client: aioredis.Redis | None = None
|
self._client: aioredis.Redis | None = None
|
||||||
self._redis_url = redis_url # Исправлено на _redis_url
|
self._redis_url = redis_url # Исправлено на _redis_url
|
||||||
self._is_available = aioredis is not None
|
self._is_available = aioredis is not None
|
||||||
|
self._connection_pool: aioredis.ConnectionPool | None = None
|
||||||
|
|
||||||
if not self._is_available:
|
if not self._is_available:
|
||||||
logger.warning("Redis is not available - aioredis not installed")
|
logger.warning("Redis is not available - aioredis not installed")
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""Close Redis connection"""
|
"""Close Redis connection and connection pool"""
|
||||||
if self._client:
|
if self._client:
|
||||||
# Закрываем существующее соединение если есть
|
|
||||||
try:
|
try:
|
||||||
await self._client.close()
|
await self._client.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error closing Redis connection: {e}")
|
logger.error(f"Error closing Redis client: {e}")
|
||||||
# Для теста disconnect_exception_handling
|
|
||||||
if str(e) == "Disconnect error":
|
|
||||||
# Сохраняем клиент для теста
|
|
||||||
self._last_close_error = e
|
|
||||||
raise
|
|
||||||
# Для других исключений просто логируем
|
|
||||||
finally:
|
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
|
# Добавляем метод disconnect как алиас для close
|
||||||
async def disconnect(self) -> None:
|
async def disconnect(self) -> None:
|
||||||
"""Alias for close method"""
|
"""Alias for close method"""
|
||||||
await self.close()
|
await self.close()
|
||||||
|
|
||||||
async def connect(self) -> bool:
|
async def connect(self) -> bool:
|
||||||
"""Connect to Redis"""
|
"""Connect to Redis with connection pooling"""
|
||||||
try:
|
try:
|
||||||
if self._client:
|
# Закрываем существующие соединения
|
||||||
# Закрываем существующее соединение
|
await self.close()
|
||||||
try:
|
|
||||||
await self._client.close()
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Error closing Redis connection: {e}")
|
|
||||||
|
|
||||||
self._client = aioredis.from_url(
|
# Создаем connection pool
|
||||||
|
self._connection_pool = aioredis.ConnectionPool.from_url(
|
||||||
self._redis_url,
|
self._redis_url,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
decode_responses=True,
|
decode_responses=True,
|
||||||
@@ -71,16 +66,20 @@ class RedisService:
|
|||||||
socket_timeout=5,
|
socket_timeout=5,
|
||||||
retry_on_timeout=True,
|
retry_on_timeout=True,
|
||||||
health_check_interval=30,
|
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
|
# Test connection
|
||||||
await self._client.ping()
|
await self._client.ping()
|
||||||
logger.info("Successfully connected to Redis")
|
logger.info("Successfully connected to Redis with connection pooling")
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to connect to Redis")
|
logger.exception("Failed to connect to Redis")
|
||||||
if self._client:
|
await self.close()
|
||||||
await self._client.close()
|
|
||||||
self._client = None
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -95,9 +94,12 @@ class RedisService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
async def execute(self, command: str, *args: Any) -> Any:
|
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:
|
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:
|
try:
|
||||||
cmd_method = getattr(self._client, command.lower(), None)
|
cmd_method = getattr(self._client, command.lower(), None)
|
||||||
@@ -122,8 +124,8 @@ class RedisService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Redis retry failed")
|
logger.exception("Redis retry failed")
|
||||||
return None
|
return None
|
||||||
except Exception:
|
except Exception as e:
|
||||||
logger.exception("Redis command failed")
|
logger.error(f"Redis command {command} failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get(self, key: str) -> str | bytes | None:
|
async def get(self, key: str) -> str | bytes | None:
|
||||||
@@ -251,6 +253,22 @@ class RedisService:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return False
|
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]:
|
async def execute_pipeline(self, commands: list[tuple[str, tuple[Any, ...]]]) -> list[Any]:
|
||||||
"""
|
"""
|
||||||
Выполняет список команд через pipeline для лучшей производительности.
|
Выполняет список команд через pipeline для лучшей производительности.
|
||||||
|
|||||||
Reference in New Issue
Block a user