200 lines
7.4 KiB
Python
200 lines
7.4 KiB
Python
"""Task Worker for sequential review processing"""
|
||
|
||
import asyncio
|
||
import logging
|
||
from datetime import datetime
|
||
from sqlalchemy import select
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
|
||
from app.database import AsyncSessionLocal
|
||
from app.models import ReviewTask, PullRequest, Repository, Review
|
||
from app.models.review_task import TaskStatusEnum
|
||
from app.agents.reviewer import ReviewerAgent
|
||
from app.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class ReviewTaskWorker:
|
||
"""Worker that processes review tasks sequentially"""
|
||
|
||
def __init__(self):
|
||
self.running = False
|
||
self.current_task_id = None
|
||
self.poll_interval = 10 # секунд между проверками
|
||
|
||
async def start(self):
|
||
"""Start the worker"""
|
||
self.running = True
|
||
logger.info("🚀 Task Worker запущен")
|
||
|
||
while self.running:
|
||
try:
|
||
await self._process_next_task()
|
||
except Exception as e:
|
||
logger.error(f"❌ Ошибка в Task Worker: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
# Подождать перед следующей проверкой
|
||
await asyncio.sleep(self.poll_interval)
|
||
|
||
async def stop(self):
|
||
"""Stop the worker"""
|
||
self.running = False
|
||
logger.info("⏹️ Task Worker остановлен")
|
||
|
||
async def _process_next_task(self):
|
||
"""Process next pending task"""
|
||
async with AsyncSessionLocal() as db:
|
||
# Проверяем есть ли уже выполняющаяся задача
|
||
in_progress_query = select(ReviewTask).where(
|
||
ReviewTask.status == TaskStatusEnum.IN_PROGRESS
|
||
)
|
||
result = await db.execute(in_progress_query)
|
||
in_progress = result.scalar_one_or_none()
|
||
|
||
if in_progress:
|
||
# Уже есть задача в работе, ждем
|
||
logger.debug(f"⏳ Задача #{in_progress.id} уже выполняется")
|
||
return
|
||
|
||
# Берем следующую pending задачу (с приоритетом)
|
||
pending_query = select(ReviewTask).where(
|
||
ReviewTask.status == TaskStatusEnum.PENDING
|
||
).order_by(
|
||
ReviewTask.priority.desc(), # HIGH > NORMAL > LOW
|
||
ReviewTask.created_at.asc() # Старые первыми
|
||
).limit(1)
|
||
|
||
result = await db.execute(pending_query)
|
||
task = result.scalar_one_or_none()
|
||
|
||
if not task:
|
||
# Нет задач в очереди
|
||
return
|
||
|
||
logger.info(f"\n{'='*80}")
|
||
logger.info(f"📋 Начало обработки задачи #{task.id}")
|
||
logger.info(f" PR ID: {task.pull_request_id}")
|
||
logger.info(f" Приоритет: {task.priority}")
|
||
logger.info(f"={'='*80}\n")
|
||
|
||
# Отмечаем задачу как in_progress
|
||
task.status = TaskStatusEnum.IN_PROGRESS
|
||
task.started_at = datetime.utcnow()
|
||
self.current_task_id = task.id
|
||
await db.commit()
|
||
|
||
try:
|
||
# Выполняем review
|
||
await self._execute_review(task, db)
|
||
|
||
# Успешно завершено
|
||
task.status = TaskStatusEnum.COMPLETED
|
||
task.completed_at = datetime.utcnow()
|
||
logger.info(f"✅ Задача #{task.id} успешно завершена")
|
||
|
||
except Exception as e:
|
||
# Ошибка при выполнении
|
||
task.retry_count += 1
|
||
task.error_message = str(e)
|
||
|
||
if task.retry_count >= task.max_retries:
|
||
# Превышено количество попыток
|
||
task.status = TaskStatusEnum.FAILED
|
||
task.completed_at = datetime.utcnow()
|
||
logger.error(f"❌ Задача #{task.id} провалена после {task.retry_count} попыток: {e}")
|
||
else:
|
||
# Вернуть в pending для повторной попытки
|
||
task.status = TaskStatusEnum.PENDING
|
||
logger.warning(f"⚠️ Задача #{task.id} вернулась в очередь (попытка {task.retry_count}/{task.max_retries}): {e}")
|
||
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
finally:
|
||
self.current_task_id = None
|
||
await db.commit()
|
||
|
||
async def _execute_review(self, task: ReviewTask, db: AsyncSession):
|
||
"""Execute review for the task"""
|
||
# Get PR with repository
|
||
result = await db.execute(
|
||
select(PullRequest).where(PullRequest.id == task.pull_request_id)
|
||
)
|
||
pull_request = result.scalar_one_or_none()
|
||
|
||
if not pull_request:
|
||
raise ValueError(f"PullRequest {task.pull_request_id} not found")
|
||
|
||
# Get repository
|
||
result = await db.execute(
|
||
select(Repository).where(Repository.id == pull_request.repository_id)
|
||
)
|
||
repository = result.scalar_one_or_none()
|
||
|
||
if not repository:
|
||
raise ValueError(f"Repository {pull_request.repository_id} not found")
|
||
|
||
# Check if review already exists and is not failed
|
||
existing_review = await db.execute(
|
||
select(Review).where(
|
||
Review.pull_request_id == pull_request.id
|
||
).order_by(Review.started_at.desc())
|
||
)
|
||
review = existing_review.scalar_one_or_none()
|
||
|
||
if review and review.status not in ["failed", "pending"]:
|
||
logger.info(f" Review already exists with status: {review.status}")
|
||
return
|
||
|
||
# Create new review if doesn't exist
|
||
if not review:
|
||
review = Review(
|
||
pull_request_id=pull_request.id,
|
||
status="pending"
|
||
)
|
||
db.add(review)
|
||
await db.commit()
|
||
await db.refresh(review)
|
||
|
||
# Run review agent
|
||
logger.info(f" 🤖 Запуск AI review для PR #{pull_request.pr_number}")
|
||
|
||
agent = ReviewerAgent(db)
|
||
await agent.review_pull_request(
|
||
repository_id=repository.id,
|
||
pr_number=pull_request.pr_number,
|
||
review_id=review.id
|
||
)
|
||
|
||
logger.info(f" ✅ Review завершен для PR #{pull_request.pr_number}")
|
||
|
||
|
||
# Global worker instance
|
||
_worker_instance: ReviewTaskWorker | None = None
|
||
|
||
|
||
async def start_worker():
|
||
"""Start the global worker instance"""
|
||
global _worker_instance
|
||
if _worker_instance is None:
|
||
_worker_instance = ReviewTaskWorker()
|
||
# Запускаем в фоне
|
||
asyncio.create_task(_worker_instance.start())
|
||
|
||
|
||
async def stop_worker():
|
||
"""Stop the global worker instance"""
|
||
global _worker_instance
|
||
if _worker_instance:
|
||
await _worker_instance.stop()
|
||
_worker_instance = None
|
||
|
||
|
||
def get_worker() -> ReviewTaskWorker | None:
|
||
"""Get the current worker instance"""
|
||
return _worker_instance
|
||
|