Clawith/backend/app/services/sandbox/api/judge0_backend.py

197 lines
6.8 KiB
Python

"""Judge0 API-based sandbox backend."""
import time
import httpx
from app.services.sandbox.base import BaseSandboxBackend, ExecutionResult, SandboxCapabilities
from app.services.sandbox.config import SandboxConfig
from loguru import logger
# Judge0 language IDs
_JUDGE0_LANGUAGE_IDS = {
"python": 71, # Python 3
"python3": 71,
"bash": 28, # Bash
"sh": 28,
"node": 63, # JavaScript (Node.js)
"javascript": 63,
}
# Default Judge0 API URL
_DEFAULT_JUDGE0_URL = "https://api.judge0.com"
class Judge0Backend(BaseSandboxBackend):
"""Judge0 API-based sandbox backend.
Judge0 (https://judge0.com/) is an open-source online judge system
that provides a REST API for code execution.
"""
name = "judge0"
def __init__(self, config: SandboxConfig):
self.config = config
self.api_url = config.api_url or _DEFAULT_JUDGE0_URL
if not config.api_key:
# Judge0 has a free tier that doesn't require API key
# But for production, you should set one
pass
def get_capabilities(self) -> SandboxCapabilities:
return SandboxCapabilities(
supported_languages=["python", "python3", "bash", "node", "javascript"],
max_timeout=self.config.max_timeout,
max_memory_mb=256,
network_available=False,
filesystem_available=False,
)
async def health_check(self) -> bool:
"""Check if Judge0 API is available."""
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.api_url}/languages",
timeout=5.0
)
return response.status_code == 200
except Exception:
return False
async def execute(
self,
code: str,
language: str,
timeout: int = 30,
work_dir: str | None = None,
**kwargs
) -> ExecutionResult:
"""Execute code using Judge0 API."""
start_time = time.time()
# Get language ID
lang_id = _JUDGE0_LANGUAGE_IDS.get(language.lower())
if lang_id is None:
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=1,
duration_ms=int((time.time() - start_time) * 1000),
error=f"Unsupported language: {language}. Supported: {', '.join(_JUDGE0_LANGUAGE_IDS.keys())}"
)
try:
async with httpx.AsyncClient() as client:
# Step 1: Submit the code
submit_response = await client.post(
f"{self.api_url}/submissions",
json={
"source_code": code,
"language_id": lang_id,
"cpu_time_limit": timeout,
"memory_limit": self.config.memory_limit,
},
timeout=10.0
)
if submit_response.status_code != 201:
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=1,
duration_ms=int((time.time() - start_time) * 1000),
error=f"Failed to submit code: {submit_response.status_code}"
)
submission = submit_response.json()
token = submission.get("token")
if not token:
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=1,
duration_ms=int((time.time() - start_time) * 1000),
error="Failed to get submission token"
)
# Step 2: Poll for result
max_retries = timeout * 2 # Poll every 0.5 seconds
for _ in range(max_retries):
await client.get(f"{self.api_url}/submissions/{token}")
result_response = await client.get(
f"{self.api_url}/submissions/{token}",
params={"fields": "stdout,stderr,status,time,memory,exit_code"},
timeout=5.0
)
if result_response.status_code == 200:
result = result_response.json()
status = result.get("status", {})
# Check if still processing
if status.get("id") <= 2: # In Queue or Processing
await client.sleep(0.5)
continue
# Completed
duration_ms = int((time.time() - start_time) * 1000)
stdout = result.get("stdout", "") or ""
stderr = result.get("stderr", "") or ""
exit_code = result.get("exit_code", 1)
# Truncate output
stdout = stdout[:10000]
stderr = stderr[:5000]
return ExecutionResult(
success=status.get("id") == 3, # Accepted
stdout=stdout,
stderr=stderr,
exit_code=exit_code,
duration_ms=duration_ms,
error=None if status.get("id") == 3 else status.get("description", "Execution failed")
)
await client.sleep(0.5)
# Timeout waiting for result
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=124,
duration_ms=int((time.time() - start_time) * 1000),
error=f"Code execution timed out after {timeout}s"
)
except httpx.TimeoutException:
duration_ms = int((time.time() - start_time) * 1000)
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=124,
duration_ms=duration_ms,
error=f"Code execution timed out after {timeout}s"
)
except Exception as e:
duration_ms = int((time.time() - start_time) * 1000)
logger.exception(f"[Judge0] Execution error")
return ExecutionResult(
success=False,
stdout="",
stderr="",
exit_code=1,
duration_ms=duration_ms,
error=f"Judge0 execution error: {str(e)[:200]}"
)