CodeForge has four parts: Kotlin Multiplatform clients, a Ktor API, judge workers and a sandbox harness. PostgreSQL stores everything, including the queue; Redis is an optional wake-up signal.
| Component | Responsibility |
|---|---|
Clients (app/) |
One Compose Multiplatform UI for Web (WebAssembly), Android and Desktop. The web build embeds the Monaco editor. The client imports a problem, submits code and polls the submission until it has a final verdict. |
API (backend/routes) |
Validates and stores submissions, imports Codeforces problems, serves results and history. Rate-limited per client. |
Judge workers (backend/worker) |
Claim queued submissions, keep a lease while judging, write the result. |
Sandbox (backend/sandbox, backend/judge-sandbox) |
Runs the harness in a fresh Docker container per submission, or as a child process on hosts without Docker. |
Harness (judge-sandbox/harness/judge.py) |
Compiles once, runs every test with limits, measures time and memory, decides the per-test status. |
sequenceDiagram
participant C as Client
participant A as Ktor API
participant P as PostgreSQL
participant R as Redis
participant W as Judge worker
participant S as Sandbox
C->>A: POST /submit {problemId, language, code}
A->>A: rate limit, size, language, queue depth
A->>P: INSERT submission (QUEUED)
A-->>R: LPUSH id (best effort)
A->>C: {submissionId, "QUEUED"}
R-->>W: BRPOP wakes a worker (or the worker polls)
W->>P: UPDATE ... SET RUNNING, claimed_at, attempts+1<br/>WHERE id = (SELECT ... FOR UPDATE SKIP LOCKED)
W->>S: job: language, source, tests, limits
S->>S: compile once, run each test, stop at first failure
S->>W: compile result + per-test status, time, memory
W->>P: verdict, output, per-test results, peak time and memory
loop every second
C->>A: GET /submission/{id}
A->>C: verdict, metrics, test results
end
While the worker judges, a background task refreshes claimed_at every 10 seconds for every submission it holds.
| Failure | What happens |
|---|---|
Redis is down, or LPUSH fails after the insert |
The row is already QUEUED; workers find it on their next database poll (15 s with Redis, 3 s without). |
| A worker or the whole service dies mid-judge | The lease stops being refreshed. The reaper (every 30 s) re-queues RUNNING rows whose lease is older than 60 s. |
| A submission keeps crashing the judge | After 2 attempts the reaper marks it INTERNAL_ERROR instead of re-queueing it again. |
| An exception while judging (missing problem, sandbox failure) | The worker writes INTERNAL_ERROR with a short message. No row stays in RUNNING. |
| The harness hangs | The backend kills it after a deadline computed from the compile timeout and the number of tests; in Docker mode it also removes the container. |
| Two workers race for one row | FOR UPDATE SKIP LOCKED gives the row to one of them; the other takes the next one. |
The harness reads one JSON job and writes one JSON result. Per test it:
- writes the input to a file and starts the program with stdin, stdout and stderr redirected to files;
- applies limits in the child before
exec:RLIMIT_CPU,RLIMIT_AS(not for Java, which gets-Xmx),RLIMIT_FSIZE(16 MB output),RLIMIT_CORE = 0, a new session and, when running as root, a drop to uid 10002; - waits with
wait4()and a wall-clock cap, which yields the exit status, the signal, CPU time and peak RSS; - kills anything the program left running and cleans its working directory;
- decides the status:
TLE(CPU over the limit,SIGXCPUor wall clock),OLE(SIGXFSZ),OK,MLE(allocation failure or peak RSS at the limit) orRE; then compares output forWA, ignoring trailing spaces and blank lines.
The backend starts codeforge-sandbox once per submission:
docker run --rm -i --network none --read-only
--tmpfs /work:rw,exec,size=256m --tmpfs /tmp:rw,size=16m
--memory <limit+512>m --memory-swap <same> --cpus 1 --pids-limit 128
--cap-drop ALL --cap-add SETUID --cap-add SETGID --cap-add KILL
--security-opt no-new-privileges --ulimit core=0 --init --user 0
codeforge-sandbox
The harness runs as root inside the container only to separate itself from the program: the compiled binary and the recorded output belong to root, and the program runs as uid 10002 in the only writable directory.
Used by the free Render deployment, where no Docker daemon is available. The same harness and limits run as a child process in a private temporary directory with an empty environment. There is no network isolation and the program runs as the same user as the backend, so the start script moves the database and Redis credentials out of the environment into a private file that the backend reads and deletes before it judges anything. Use it for a public demo; prefer Docker mode anywhere else.
