Spaces:
Sleeping
Sleeping
Aman Githala commited on
Commit ·
e121f2e
1
Parent(s): 7ea29fa
Add runtime, FastAPI main, and worker
Browse files- Dockerfile +40 -0
- install_runtime.sh +21 -0
- main.py +85 -0
- requirements.txt +12 -0
- supervisord.conf +36 -0
- worker.py +114 -0
Dockerfile
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use a lightweight base with Python
|
| 2 |
+
FROM python:3.10-slim-bullseye
|
| 3 |
+
|
| 4 |
+
# Install system dependencies
|
| 5 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
+
redis-server \
|
| 7 |
+
supervisor \
|
| 8 |
+
curl \
|
| 9 |
+
xz-utils \
|
| 10 |
+
&& rm -rf /var/lib/apt/apt-get/lists/*
|
| 11 |
+
|
| 12 |
+
WORKDIR /app
|
| 13 |
+
|
| 14 |
+
# --- Install Piston Engine ---
|
| 15 |
+
RUN curl -L https://github.com/engineer-man/piston/releases/download/v2.0.0/piston-v2.0.0.tar.gz | tar -xz \
|
| 16 |
+
&& mv piston-v2.0.0 /app/piston
|
| 17 |
+
|
| 18 |
+
# Copy requirements and install Python dependencies
|
| 19 |
+
COPY requirements.txt .
|
| 20 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 21 |
+
|
| 22 |
+
# Copy all application files
|
| 23 |
+
COPY . .
|
| 24 |
+
|
| 25 |
+
# --- Runtime Installation Phase ---
|
| 26 |
+
RUN chmod +x install_runtimes.sh && ./install_runtimes.sh && rm install_runtimes.sh
|
| 27 |
+
|
| 28 |
+
# --- Security & Permissions ---
|
| 29 |
+
RUN useradd -m appuser
|
| 30 |
+
RUN mkdir -p /tmp/piston_jobs && chown -R appuser:appuser /app /tmp/piston_jobs
|
| 31 |
+
|
| 32 |
+
# Set environment variables
|
| 33 |
+
ENV PISTON_URL=http://localhost:2000/api/v2/execute
|
| 34 |
+
ENV PYTHONUNBUFFERED=1
|
| 35 |
+
|
| 36 |
+
# Hugging Face Spaces port
|
| 37 |
+
EXPOSE 7860
|
| 38 |
+
|
| 39 |
+
# Start Supervisor
|
| 40 |
+
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
install_runtime.sh
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Start Piston in the background temporarily
|
| 3 |
+
/app/piston/piston-server &
|
| 4 |
+
PISTON_PID=$!
|
| 5 |
+
|
| 6 |
+
# Wait for Piston to wake up
|
| 7 |
+
sleep 5
|
| 8 |
+
|
| 9 |
+
# Install common runtimes
|
| 10 |
+
languages=("python" "javascript" "cpp" "java")
|
| 11 |
+
|
| 12 |
+
for lang in "${languages[@]}"; do
|
| 13 |
+
echo "Installing $lang..."
|
| 14 |
+
curl -X POST http://localhost:2000/api/v2/packages \
|
| 15 |
+
-H "Content-Type: application/json" \
|
| 16 |
+
-d "{\"language\": \"$lang\", \"version\": \"*\"}"
|
| 17 |
+
done
|
| 18 |
+
|
| 19 |
+
# Shutdown the temporary Piston process
|
| 20 |
+
kill $PISTON_PID
|
| 21 |
+
echo "Runtimes installed successfully."
|
main.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import json
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
from fastapi import FastAPI, HTTPException, Depends, Header, BackgroundTasks
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from jose import jwt, JWTError
|
| 8 |
+
from redis import Redis
|
| 9 |
+
from rq import Queue
|
| 10 |
+
import httpx
|
| 11 |
+
|
| 12 |
+
# --- Configuration ---
|
| 13 |
+
# These should be set in Hugging Face Space Secrets
|
| 14 |
+
SUPABASE_JWT_SECRET = os.getenv("SUPABASE_JWT_SECRET", "")
|
| 15 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 16 |
+
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
|
| 17 |
+
|
| 18 |
+
app = FastAPI(title="Piston Execution Gateway")
|
| 19 |
+
redis_conn = Redis(host='localhost', port=6379)
|
| 20 |
+
q = Queue('executions', connection=redis_conn)
|
| 21 |
+
|
| 22 |
+
# --- Models ---
|
| 23 |
+
class CodeExecutionRequest(BaseModel):
|
| 24 |
+
problem_id: str
|
| 25 |
+
source_code: str
|
| 26 |
+
language: str
|
| 27 |
+
is_submit: bool = False
|
| 28 |
+
|
| 29 |
+
class ExecutionResponse(BaseModel):
|
| 30 |
+
job_id: str
|
| 31 |
+
status: str
|
| 32 |
+
|
| 33 |
+
# --- Middleware: JWT Verification ---
|
| 34 |
+
async def verify_supabase_jwt(authorization: str = Header(None)):
|
| 35 |
+
if not authorization or not authorization.startswith("Bearer "):
|
| 36 |
+
raise HTTPException(status_code=401, detail="Missing or invalid token")
|
| 37 |
+
|
| 38 |
+
token = authorization.split(" ")[1]
|
| 39 |
+
try:
|
| 40 |
+
# Validate the token using Supabase JWT Secret
|
| 41 |
+
payload = jwt.decode(
|
| 42 |
+
token,
|
| 43 |
+
SUPABASE_JWT_SECRET,
|
| 44 |
+
algorithms=["HS256"],
|
| 45 |
+
options={"verify_aud": False}
|
| 46 |
+
)
|
| 47 |
+
return payload # Contains user_id (sub)
|
| 48 |
+
except JWTError:
|
| 49 |
+
raise HTTPException(status_code=401, detail="Token verification failed")
|
| 50 |
+
|
| 51 |
+
# --- Core Logic ---
|
| 52 |
+
@app.post("/execute", response_model=ExecutionResponse)
|
| 53 |
+
async def execute_code(
|
| 54 |
+
request: CodeExecutionRequest,
|
| 55 |
+
user: dict = Depends(verify_supabase_jwt)
|
| 56 |
+
):
|
| 57 |
+
"""
|
| 58 |
+
Validates user, pushes job to Redis queue, and returns immediately.
|
| 59 |
+
"""
|
| 60 |
+
job_id = f"job_{int(time.time() * 1000)}_{user.get('sub')[:8]}"
|
| 61 |
+
|
| 62 |
+
# Payload for the worker
|
| 63 |
+
job_data = {
|
| 64 |
+
"job_id": job_id,
|
| 65 |
+
"user_id": user.get("sub"),
|
| 66 |
+
"problem_id": request.problem_id,
|
| 67 |
+
"source_code": request.source_code,
|
| 68 |
+
"language": request.language,
|
| 69 |
+
"is_submit": request.is_submit,
|
| 70 |
+
"timestamp": time.time()
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
# Push to Redis Queue
|
| 74 |
+
# We use 'worker.process_job' which we will define in the next file
|
| 75 |
+
q.enqueue("worker.process_job", job_data, job_id=job_id)
|
| 76 |
+
|
| 77 |
+
return {"job_id": job_id, "status": "queued"}
|
| 78 |
+
|
| 79 |
+
@app.get("/health")
|
| 80 |
+
async def health_check():
|
| 81 |
+
return {"status": "online", "queue_size": len(q)}
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
import uvicorn
|
| 85 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
supabase
|
| 4 |
+
python-jose[cryptography]
|
| 5 |
+
python-multipart
|
| 6 |
+
redis
|
| 7 |
+
rq
|
| 8 |
+
pydantic
|
| 9 |
+
pydantic-settings
|
| 10 |
+
httpx
|
| 11 |
+
requests
|
| 12 |
+
supervisor
|
supervisord.conf
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[supervisord]
|
| 2 |
+
nodaemon=true
|
| 3 |
+
user=root
|
| 4 |
+
logfile=/app/supervisord.log
|
| 5 |
+
pidfile=/var/run/supervisord.pid
|
| 6 |
+
|
| 7 |
+
[program:redis]
|
| 8 |
+
command=redis-server --port 6379 --bind 127.0.0.1 --maxmemory 256mb --maxmemory-policy allkeys-lru
|
| 9 |
+
autostart=true
|
| 10 |
+
autorestart=true
|
| 11 |
+
stdout_logfile=/app/redis.log
|
| 12 |
+
stderr_logfile=/app/redis.err
|
| 13 |
+
|
| 14 |
+
[program:piston]
|
| 15 |
+
command=/app/piston/piston-server
|
| 16 |
+
directory=/app/piston
|
| 17 |
+
autostart=true
|
| 18 |
+
autorestart=true
|
| 19 |
+
stdout_logfile=/app/piston.log
|
| 20 |
+
stderr_logfile=/app/piston.err
|
| 21 |
+
|
| 22 |
+
[program:fastapi]
|
| 23 |
+
command=uvicorn main:app --host 0.0.0.0 --port 7860 --workers 4
|
| 24 |
+
directory=/app
|
| 25 |
+
autostart=true
|
| 26 |
+
autorestart=true
|
| 27 |
+
stdout_logfile=/app/fastapi.log
|
| 28 |
+
stderr_logfile=/app/fastapi.err
|
| 29 |
+
|
| 30 |
+
[program:worker]
|
| 31 |
+
command=python worker.py
|
| 32 |
+
directory=/app
|
| 33 |
+
autostart=true
|
| 34 |
+
autorestart=true
|
| 35 |
+
stdout_logfile=/app/worker.log
|
| 36 |
+
stderr_logfile=/app/worker.err
|
worker.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import json
|
| 4 |
+
import requests
|
| 5 |
+
from redis import Redis
|
| 6 |
+
from rq import Worker, Queue, Connection
|
| 7 |
+
from supabase import create_client, Client
|
| 8 |
+
|
| 9 |
+
# --- Configuration ---
|
| 10 |
+
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
|
| 11 |
+
SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
|
| 12 |
+
# Piston internal URL (assuming it runs on localhost via the Docker setup)
|
| 13 |
+
PISTON_URL = os.getenv("PISTON_URL", "http://localhost:2000/api/v2/execute")
|
| 14 |
+
|
| 15 |
+
# Initialize Supabase Admin Client
|
| 16 |
+
supabase: Client = create_client(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
|
| 17 |
+
|
| 18 |
+
def process_job(job_data):
|
| 19 |
+
"""
|
| 20 |
+
The core logic executed by the background worker.
|
| 21 |
+
"""
|
| 22 |
+
job_id = job_data['job_id']
|
| 23 |
+
problem_id = job_data['problem_id']
|
| 24 |
+
source_code = job_data['source_code']
|
| 25 |
+
language = job_data['language']
|
| 26 |
+
is_submit = job_data['is_submit']
|
| 27 |
+
user_id = job_data['user_id']
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
# 1. Fetch Test Cases from Supabase
|
| 31 |
+
# We fetch from 'problems' table where problem_id matches
|
| 32 |
+
# Expecting a JSON column 'test_cases' like [{"input": "...", "expected": "..."}]
|
| 33 |
+
response = supabase.table("problems").select("test_cases").eq("id", problem_id).single().execute()
|
| 34 |
+
if not response.data:
|
| 35 |
+
raise Exception(f"Problem {problem_id} not found")
|
| 36 |
+
|
| 37 |
+
test_cases = response.data.get("test_cases", [])
|
| 38 |
+
results = []
|
| 39 |
+
all_passed = True
|
| 40 |
+
|
| 41 |
+
# 2. Iterate through Test Cases
|
| 42 |
+
for index, tc in enumerate(test_cases):
|
| 43 |
+
# Security: Custom Piston Payload with our 10MB / 1s limits
|
| 44 |
+
payload = {
|
| 45 |
+
"language": language,
|
| 46 |
+
"version": "*", # Use latest available
|
| 47 |
+
"files": [{"content": source_code}],
|
| 48 |
+
"stdin": tc.get("input", ""),
|
| 49 |
+
"run_timeout": 1000, # 1 second limit
|
| 50 |
+
"compile_timeout": 10000, # 10s for compilation
|
| 51 |
+
"memory_limit": 10485760, # 10MB limit (10 * 1024 * 1024)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# Call Piston Engine
|
| 55 |
+
piston_res = requests.post(PISTON_URL, json=payload, timeout=5)
|
| 56 |
+
piston_data = piston_res.json()
|
| 57 |
+
|
| 58 |
+
run_info = piston_data.get("run", {})
|
| 59 |
+
actual_output = run_info.get("stdout", "").strip()
|
| 60 |
+
error_output = run_info.get("stderr", "")
|
| 61 |
+
|
| 62 |
+
passed = actual_output == tc.get("expected", "").strip()
|
| 63 |
+
if not passed:
|
| 64 |
+
all_passed = False
|
| 65 |
+
|
| 66 |
+
results.append({
|
| 67 |
+
"test_case": index + 1,
|
| 68 |
+
"passed": passed,
|
| 69 |
+
"stdout": actual_output if not is_submit else "Hidden",
|
| 70 |
+
"stderr": error_output,
|
| 71 |
+
"exit_code": run_info.get("code")
|
| 72 |
+
})
|
| 73 |
+
|
| 74 |
+
# Optimization: If it's a 'Run' and one fails, we can stop early
|
| 75 |
+
if not passed and not is_submit:
|
| 76 |
+
break
|
| 77 |
+
|
| 78 |
+
# 3. Handle Persistence & Real-time Update
|
| 79 |
+
final_payload = {
|
| 80 |
+
"job_id": job_id,
|
| 81 |
+
"user_id": user_id,
|
| 82 |
+
"problem_id": problem_id,
|
| 83 |
+
"status": "completed",
|
| 84 |
+
"results": results,
|
| 85 |
+
"all_passed": all_passed,
|
| 86 |
+
"created_at": "now()"
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
if is_submit:
|
| 90 |
+
# Save to 'submissions' table
|
| 91 |
+
supabase.table("submissions").insert(final_payload).execute()
|
| 92 |
+
|
| 93 |
+
# Broadcast via a 'results' table or a dedicated Realtime channel
|
| 94 |
+
# Here we update a 'job_status' table that the frontend is listening to via Supabase Realtime
|
| 95 |
+
supabase.table("job_status").insert({
|
| 96 |
+
"job_id": job_id,
|
| 97 |
+
"user_id": user_id,
|
| 98 |
+
"data": final_payload
|
| 99 |
+
}).execute()
|
| 100 |
+
|
| 101 |
+
except Exception as e:
|
| 102 |
+
# Update status to failed
|
| 103 |
+
supabase.table("job_status").insert({
|
| 104 |
+
"job_id": job_id,
|
| 105 |
+
"user_id": user_id,
|
| 106 |
+
"data": {"status": "error", "message": str(e)}
|
| 107 |
+
}).execute()
|
| 108 |
+
|
| 109 |
+
if __name__ == '__main__':
|
| 110 |
+
# Connect to the local Redis instance defined in supervisord.conf
|
| 111 |
+
redis_conn = Redis(host='localhost', port=6379)
|
| 112 |
+
with Connection(redis_conn):
|
| 113 |
+
worker = Worker([Queue('executions')])
|
| 114 |
+
worker.work()
|