github-actions
Sync backend from GitHub 47e8067
5f7afef
Raw History Blame Contribute Delete
5.93 kB
from __future__ import annotations
import os
import time
import uuid
import threading
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from typing import Dict, Optional, Any
from pydantic import BaseModel
from codestar.core.schemas import JudgeResult, JudgeStatus
from codestar.core.engine import JudgeEngine
from codestar.problems.loader import repo
from codestar.server.db import create_submission, calculate_beats
class TaskStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
COMPLETED = "completed"
FAILED = "failed"
class JudgeTaskInfo(BaseModel):
task_id: str
problem_slug: str
status: TaskStatus
created_at: float
started_at: Optional[float] = None
completed_at: Optional[float] = None
result: Optional[JudgeResult] = None
error: Optional[str] = None
class JudgeTaskQueue:
def __init__(self, max_workers: Optional[int] = None, max_tasks: int = 500):
workers = max_workers or int(os.environ.get("JUDGE_WORKERS", 4))
self._executor = ThreadPoolExecutor(max_workers=workers, thread_name_prefix="judge-worker")
self._tasks: Dict[str, JudgeTaskInfo] = {}
self._max_tasks = max_tasks
self._lock = threading.Lock()
self._engine = JudgeEngine()
def submit_task(
self,
problem_slug: str,
code: str,
is_test: bool = False,
custom_testcase: Optional[str] = None,
user_id: Optional[int] = None,
) -> str:
task_id = f"task_{uuid.uuid4().hex[:12]}"
now = time.time()
task_info = JudgeTaskInfo(
task_id=task_id,
problem_slug=problem_slug,
status=TaskStatus.PENDING,
created_at=now,
)
with self._lock:
self._cleanup_old_tasks()
self._tasks[task_id] = task_info
self._executor.submit(
self._execute_task,
task_id=task_id,
problem_slug=problem_slug,
code=code,
is_test=is_test,
custom_testcase=custom_testcase,
user_id=user_id,
)
return task_id
def _execute_task(
self,
task_id: str,
problem_slug: str,
code: str,
is_test: bool,
custom_testcase: Optional[str],
user_id: Optional[int],
):
with self._lock:
if task_id not in self._tasks:
return
self._tasks[task_id].status = TaskStatus.RUNNING
self._tasks[task_id].started_at = time.time()
try:
problem = repo.get(problem_slug)
if not problem:
with self._lock:
self._tasks[task_id].status = TaskStatus.FAILED
self._tasks[task_id].error = f"Problem '{problem_slug}' not found"
self._tasks[task_id].completed_at = time.time()
return
result = self._engine.judge(problem, code, custom_testcase=custom_testcase)
# Calculate beats for AC submissions
if result.status == JudgeStatus.AC:
beats = calculate_beats(problem_slug, result.execution_time_ms, result.peak_memory_mb)
result.beats_runtime_pct = beats["runtime_pct"]
result.beats_memory_pct = beats["memory_pct"]
# Persist submission if not test
if not is_test:
passed_cases = sum(1 for c in result.cases if c.passed)
total_cases = len(result.cases)
status_str = result.status.value if hasattr(result.status, "value") else str(result.status)
err_msg = result.message if result.status != JudgeStatus.AC else None
try:
create_submission(
user_id=user_id,
problem_slug=problem_slug,
status=status_str,
passed_cases=passed_cases,
total_cases=total_cases,
runtime_ms=result.execution_time_ms,
code=code,
error_message=err_msg,
peak_memory_mb=result.peak_memory_mb,
)
except Exception:
pass
with self._lock:
self._tasks[task_id].status = TaskStatus.COMPLETED
self._tasks[task_id].result = result
self._tasks[task_id].completed_at = time.time()
except Exception as e:
with self._lock:
self._tasks[task_id].status = TaskStatus.FAILED
self._tasks[task_id].error = str(e)
self._tasks[task_id].completed_at = time.time()
def get_task(self, task_id: str) -> Optional[JudgeTaskInfo]:
with self._lock:
return self._tasks.get(task_id)
def _cleanup_old_tasks(self, max_age_seconds: float = 3600.0):
"""Clean up tasks completed more than an hour ago, or evict oldest completed tasks if exceeding max capacity."""
now = time.time()
# 1. Clean up expired tasks
to_delete = [
tid for tid, info in self._tasks.items()
if info.completed_at and (now - info.completed_at > max_age_seconds)
]
for tid in to_delete:
del self._tasks[tid]
# 2. Strict capacity bounding: if still exceeding max_tasks, evict oldest completed
if len(self._tasks) > self._max_tasks:
completed_tasks = sorted(
[info for info in self._tasks.values() if info.completed_at],
key=lambda x: x.completed_at or 0.0,
)
excess = len(self._tasks) - self._max_tasks
for info in completed_tasks[:excess]:
self._tasks.pop(info.task_id, None)
# Global singleton instance
judge_queue = JudgeTaskQueue()