Remove .env_example file and implement MetricsUpdater service for enhanced metrics tracking. Update bot.py to start and stop metrics updater, and improve database connection handling in CRUD operations with metrics tracking. Update README with details on metrics issues and fixes.

This commit is contained in:
2025-09-08 23:18:55 +03:00
parent 596a2fa813
commit 23c30a78e2
11 changed files with 744 additions and 49 deletions

View File

@@ -14,9 +14,7 @@ from models.question import Question, QuestionStatus
from models.user import User
from models.user_block import UserBlock
from models.user_settings import UserSettings
from services.infrastructure.logger import get_logger
logger = get_logger(__name__)
from services.infrastructure.db_metrics_decorator import track_db_operation
class ConnectionPool:
@@ -43,30 +41,79 @@ class ConnectionPool:
await conn.execute("PRAGMA temp_store=MEMORY")
return conn
async def _is_connection_valid(self, conn) -> bool:
"""Проверка валидности соединения"""
try:
if conn is None:
return False
# Выполняем простой запрос для проверки соединения
cursor = await conn.execute("SELECT 1")
await cursor.fetchone()
return True
except Exception:
return False
async def get_connection(self):
"""Получение соединения из пула"""
try:
# Пытаемся получить соединение из пула
return self._pool.get_nowait()
conn = self._pool.get_nowait()
# Проверяем, что соединение еще активно
if await self._is_connection_valid(conn):
return conn
else:
# Соединение неактивно, закрываем его и создаем новое
await conn.close()
async with self._lock:
self._created_connections -= 1
except asyncio.QueueEmpty:
# Если пул пуст, создаем новое соединение
async with self._lock:
if self._created_connections < self.pool_size:
self._created_connections += 1
return await self._create_connection()
pass
# Если пул пуст или соединение неактивно, создаем новое
async with self._lock:
if self._created_connections < self.pool_size:
self._created_connections += 1
return await self._create_connection()
else:
# Ждем освобождения соединения из пула
conn = await self._pool.get()
# Проверяем валидность полученного соединения
if await self._is_connection_valid(conn):
return conn
else:
# Ждем освобождения соединения
return await self._pool.get()
# Соединение неактивно, закрываем и создаем новое
await conn.close()
async with self._lock:
self._created_connections -= 1
return await self._create_connection()
async def return_connection(self, conn):
"""Возврат соединения в пул"""
if conn is None:
return
try:
self._pool.put_nowait(conn)
# Проверяем валидность соединения перед возвратом в пул
if await self._is_connection_valid(conn):
self._pool.put_nowait(conn)
else:
# Соединение неактивно, закрываем его
await conn.close()
async with self._lock:
self._created_connections -= 1
except asyncio.QueueFull:
# Если пул полон, закрываем соединение
await conn.close()
async with self._lock:
self._created_connections -= 1
except Exception as e:
# В случае любой ошибки, закрываем соединение
try:
await conn.close()
except:
pass
async with self._lock:
self._created_connections -= 1
async def close_all(self):
"""Закрытие всех соединений"""
@@ -74,6 +121,15 @@ class ConnectionPool:
conn = await self._pool.get()
await conn.close()
self._created_connections = 0
def get_pool_stats(self) -> dict:
"""Получение статистики пула соединений"""
return {
"pool_size": self.pool_size,
"created_connections": self._created_connections,
"available_connections": self._pool.qsize(),
"utilization_percent": (self._created_connections / self.pool_size) * 100 if self.pool_size > 0 else 0
}
# Глобальный пул соединений
@@ -90,9 +146,10 @@ def get_connection_pool(db_path: str, pool_size: int = DEFAULT_CONNECTION_POOL_S
class BaseCRUD:
"""Базовый класс для CRUD операций"""
def __init__(self, db_path: str):
def __init__(self, db_path: str, logger=None):
self.db_path = db_path
self.pool = get_connection_pool(db_path)
self.logger = logger
@asynccontextmanager
async def get_connection(self):
@@ -116,9 +173,11 @@ class BaseCRUD:
class UserCRUD(BaseCRUD):
"""CRUD операции для пользователей"""
@track_db_operation("INSERT", "users")
async def create(self, user: User) -> User:
"""Создание нового пользователя"""
logger.info(f"👤 Создание пользователя: {user.telegram_id} ({user.first_name})")
if self.logger:
self.logger.info(f"👤 Создание пользователя: {user.telegram_id} ({user.first_name})")
async with self.get_connection() as conn:
cursor = await conn.execute("""
INSERT INTO users
@@ -135,7 +194,8 @@ class UserCRUD(BaseCRUD):
))
user.id = cursor.lastrowid
await conn.commit()
logger.info(f"✅ Пользователь создан с ID: {user.id}")
if self.logger:
self.logger.info(f"✅ Пользователь создан с ID: {user.id}")
return user
async def create_batch(self, users: List[User]) -> List[User]:
@@ -143,7 +203,8 @@ class UserCRUD(BaseCRUD):
if not users:
return []
logger.info(f"📦 Создание {len(users)} пользователей batch операцией")
if self.logger:
self.logger.info(f"📦 Создание {len(users)} пользователей batch операцией")
async with self.get_connection() as conn:
try:
# Подготавливаем данные для batch вставки
@@ -172,14 +233,17 @@ class UserCRUD(BaseCRUD):
user.id = first_id + i
await conn.commit()
logger.info(f"✅ Создано {len(users)} пользователей batch операцией")
if self.logger:
self.logger.info(f"✅ Создано {len(users)} пользователей batch операцией")
return users
except Exception as e:
await conn.rollback()
logger.error(f"❌ Ошибка при batch создании пользователей: {e}")
if self.logger:
self.logger.error(f"❌ Ошибка при batch создании пользователей: {e}")
raise
@track_db_operation("SELECT", "users")
async def get_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Получение пользователя по Telegram ID"""
async with self.get_connection() as conn:
@@ -202,6 +266,7 @@ class UserCRUD(BaseCRUD):
return self._row_to_user(row)
return None
@track_db_operation("UPDATE", "users")
async def update(self, user: User) -> User:
"""Обновление пользователя"""
async with self.get_connection() as conn:
@@ -221,6 +286,7 @@ class UserCRUD(BaseCRUD):
await conn.commit()
return user
@track_db_operation("DELETE", "users")
async def delete(self, telegram_id: int) -> bool:
"""Удаление пользователя"""
async with self.get_connection() as conn:
@@ -230,6 +296,7 @@ class UserCRUD(BaseCRUD):
await conn.commit()
return cursor.rowcount > 0
@track_db_operation("SELECT", "all_users")
async def get_all(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей"""
async with self.get_connection() as conn:
@@ -241,6 +308,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "all_users_cursor")
async def get_all_users_cursor(
self,
last_id: int,
@@ -271,6 +339,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "all_users_asc")
async def get_all_users_asc(self, limit: int = 100, offset: int = 0) -> List[User]:
"""Получение всех пользователей в порядке возрастания"""
async with self.get_connection() as conn:
@@ -282,6 +351,7 @@ class UserCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_user(row) for row in rows]
@track_db_operation("SELECT", "stats")
async def get_stats(self) -> Dict[str, Any]:
"""Получение статистики пользователей"""
async with self.get_connection() as conn:
@@ -327,9 +397,11 @@ class UserCRUD(BaseCRUD):
class QuestionCRUD(BaseCRUD):
"""CRUD операции для вопросов"""
@track_db_operation("INSERT", "questions")
async def create(self, question: Question) -> Question:
"""Создание нового вопроса"""
logger.info(f"❓ Создание вопроса от {question.from_user_id} к {question.to_user_id}")
if self.logger:
self.logger.info(f"❓ Создание вопроса от {question.from_user_id} к {question.to_user_id}")
async with self.get_connection() as conn:
# Вычисляем user_question_number для получателя
if question.user_question_number is None:
@@ -355,7 +427,8 @@ class QuestionCRUD(BaseCRUD):
))
question.id = cursor.lastrowid
await conn.commit()
logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}")
if self.logger:
self.logger.info(f"✅ Вопрос создан с ID: {question.id}, номер для пользователя: {question.user_question_number}")
return question
async def create_batch(self, questions: List[Question]) -> List[Question]:
@@ -363,7 +436,8 @@ class QuestionCRUD(BaseCRUD):
if not questions:
return []
logger.info(f"📦 Создание {len(questions)} вопросов batch операцией")
if self.logger:
self.logger.info(f"📦 Создание {len(questions)} вопросов batch операцией")
async with self.get_connection() as conn:
try:
# Группируем вопросы по получателям для вычисления user_question_number
@@ -412,14 +486,17 @@ class QuestionCRUD(BaseCRUD):
question.id = first_id + i
await conn.commit()
logger.info(f"✅ Создано {len(questions)} вопросов batch операцией")
if self.logger:
self.logger.info(f"✅ Создано {len(questions)} вопросов batch операцией")
return questions
except Exception as e:
await conn.rollback()
logger.error(f"❌ Ошибка при batch создании вопросов: {e}")
if self.logger:
self.logger.error(f"❌ Ошибка при batch создании вопросов: {e}")
raise
@track_db_operation("SELECT", "questions")
async def get_by_id(self, question_id: int) -> Optional[Question]:
"""Получение вопроса по ID"""
async with self.get_connection() as conn:
@@ -435,6 +512,7 @@ class QuestionCRUD(BaseCRUD):
return self._row_to_question(row)
return None
@track_db_operation("SELECT", "questions")
async def get_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Question]:
"""Получение вопросов для пользователя (оптимизированная версия с JOIN)"""
@@ -460,6 +538,7 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows]
@track_db_operation("SELECT", "questions_with_authors")
async def get_by_to_user_with_authors(self, to_user_id: int, status: Optional[QuestionStatus] = None,
limit: int = 50, offset: int = 0) -> List[Tuple[Question, Optional[User]]]:
"""Получение вопросов для пользователя с информацией об авторах (оптимизированный запрос)"""
@@ -526,6 +605,7 @@ class QuestionCRUD(BaseCRUD):
continue
return result
@track_db_operation("SELECT", "questions_cursor")
async def get_by_to_user_cursor(
self,
to_user_id: int,
@@ -567,6 +647,7 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows]
@track_db_operation("SELECT", "questions_asc")
async def get_by_to_user_asc(
self,
to_user_id: int,
@@ -597,9 +678,11 @@ class QuestionCRUD(BaseCRUD):
rows = await cursor.fetchall()
return [self._row_to_question(row) for row in rows]
@track_db_operation("UPDATE", "questions")
async def update(self, question: Question) -> Question:
"""Обновление вопроса"""
logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})")
if self.logger:
self.logger.info(f"📝 Обновление вопроса {question.id} (статус: {question.status.value})")
async with self.get_connection() as conn:
# Если вопрос помечается как удаленный, нужно пересчитать номера
if question.status.value == 'deleted':
@@ -638,7 +721,8 @@ class QuestionCRUD(BaseCRUD):
AND id != ?
""", (to_user_id, deleted_number, question.id))
logger.info(f"🗑️ Вопрос {question.id} помечен как удаленный, пересчитаны номера для пользователя {to_user_id}")
if self.logger:
self.logger.info(f"🗑️ Вопрос {question.id} помечен как удаленный, пересчитаны номера для пользователя {to_user_id}")
else:
# Обычное обновление
await conn.execute("""
@@ -663,9 +747,11 @@ class QuestionCRUD(BaseCRUD):
))
await conn.commit()
logger.info(f"✅ Вопрос {question.id} обновлен")
if self.logger:
self.logger.info(f"✅ Вопрос {question.id} обновлен")
return question
@track_db_operation("DELETE", "questions")
async def delete(self, question_id: int) -> bool:
"""Удаление вопроса с пересчетом user_question_number"""
async with self.get_connection() as conn:
@@ -698,9 +784,11 @@ class QuestionCRUD(BaseCRUD):
""", (to_user_id, deleted_number))
await conn.commit()
logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}")
if self.logger:
self.logger.info(f"🗑️ Вопрос {question_id} удален, пересчитаны номера для пользователя {to_user_id}")
return True
@track_db_operation("SELECT", "questions_unread_count")
async def get_unread_count(self, to_user_id: int) -> int:
"""Получение количества непрочитанных вопросов"""
async with self.get_connection() as conn:
@@ -711,6 +799,7 @@ class QuestionCRUD(BaseCRUD):
row = await cursor.fetchone()
return row[0]
@track_db_operation("SELECT", "questions_count_by_to_user")
async def get_count_by_to_user(self, to_user_id: int, status: Optional[QuestionStatus] = None) -> int:
"""Получение общего количества вопросов для пользователя"""
async with self.get_connection() as conn:
@@ -725,6 +814,7 @@ class QuestionCRUD(BaseCRUD):
row = await cursor.fetchone()
return row[0]
@track_db_operation("SELECT", "questions_stats")
async def get_stats(self) -> Dict[str, Any]:
"""Получение статистики вопросов"""
async with self.get_connection() as conn:
@@ -787,6 +877,7 @@ class QuestionCRUD(BaseCRUD):
class UserBlockCRUD(BaseCRUD):
"""CRUD операции для блокировок пользователей"""
@track_db_operation("INSERT", "user_blocks")
async def create(self, user_block: UserBlock) -> UserBlock:
"""Создание блокировки"""
async with self.get_connection() as conn:
@@ -801,6 +892,7 @@ class UserBlockCRUD(BaseCRUD):
await conn.commit()
return user_block
@track_db_operation("SELECT", "user_blocks")
async def is_blocked(self, blocker_id: int, blocked_id: int) -> bool:
"""Проверка, заблокирован ли пользователь"""
async with self.get_connection() as conn:
@@ -811,6 +903,7 @@ class UserBlockCRUD(BaseCRUD):
row = await cursor.fetchone()
return row[0] > 0
@track_db_operation("SELECT", "user_blocks")
async def get_blocked_users(self, blocker_id: int) -> List[int]:
"""Получение списка заблокированных пользователей"""
async with self.get_connection() as conn:
@@ -821,6 +914,7 @@ class UserBlockCRUD(BaseCRUD):
return [row[0] for row in rows]
@track_db_operation("DELETE", "user_blocks")
async def delete(self, blocker_id: int, blocked_id: int) -> bool:
"""Удаление блокировки"""
async with self.get_connection() as conn:
@@ -835,6 +929,7 @@ class UserBlockCRUD(BaseCRUD):
class UserSettingsCRUD(BaseCRUD):
"""CRUD операции для настроек пользователей"""
@track_db_operation("INSERT", "user_settings")
async def create(self, settings: UserSettings) -> UserSettings:
"""Создание настроек пользователя"""
async with self.get_connection() as conn:
@@ -853,6 +948,7 @@ class UserSettingsCRUD(BaseCRUD):
await conn.commit()
return settings
@track_db_operation("SELECT", "user_settings")
async def get_by_user_id(self, user_id: int) -> Optional[UserSettings]:
"""Получение настроек пользователя"""
async with self.get_connection() as conn:
@@ -864,6 +960,7 @@ class UserSettingsCRUD(BaseCRUD):
return self._row_to_settings(row)
return None
@track_db_operation("UPDATE", "user_settings")
async def update(self, settings: UserSettings) -> UserSettings:
"""Обновление настроек пользователя"""
async with self.get_connection() as conn:
@@ -881,6 +978,7 @@ class UserSettingsCRUD(BaseCRUD):
await conn.commit()
return settings
@track_db_operation("DELETE", "user_settings")
async def delete(self, user_id: int) -> bool:
"""Удаление настроек пользователя"""
async with self.get_connection() as conn: