Aman Githala commited on
Commit
e121f2e
·
1 Parent(s): 7ea29fa

Add runtime, FastAPI main, and worker

Browse files
Files changed (6) hide show
  1. Dockerfile +40 -0
  2. install_runtime.sh +21 -0
  3. main.py +85 -0
  4. requirements.txt +12 -0
  5. supervisord.conf +36 -0
  6. 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()