Como uma submissão viaja do browser até virar veredicto no placar, e
como as peças conversam entre si. Tudo é bash +
arquivos (sem DB, sem broker); a comunicação assíncrona usa
spool de arquivos + inotifywait e, no
julgamento, o modelo pull (os juízes puxam o job no
heartbeat — sem master, sem push de entrada).
Browser (web/) API (server/api/v1, via nginx+fcgiwrap)
────────────── ─────────────────────────────────────
POST /api/v1/submit ───────────▶ handlers/submit.sh
1. valida + gera id (md5)
2. escreve JSON no SPOOL ───────────┐
3. grava linha provisória │
"Not Answered Yet" no history │
4. responde {submission_id,queued} │
(front faz polling de /contest/history até sair de "Not Answered Yet") │
▼
run/spool/submissions/<arquivo>
│ inotifywait (push)
▼
server/daemons/judged.sh (consumidor do spool, systemd: moj-judged)
para cada arquivo novo:
1. lê o JSON {contest,login,problem_id,code_b64,lang,...}
2. verdict = judge_run(...) ──────────────▶ server/judge-gw/judge.sh
3. troca a linha "Not Answered Yet :<id>" pelo veredicto real (mv atômico)
4. recomputa users/<login>/metrics.json (fonte do placar)
5. arquiva o fonte em users/<login>/submissions/<id>.<ext>
6. roda server/score/build.sh <c> ──▶ reescreve var/placar.txt
7. move o arquivo de spool p/ run/spool/submissions-done/
│
Placar: GET /api/v1/contest/score ◀── lê var/placar.txt (1ª linha = modo)
O ponto-chave: a CGI nunca bloqueia.
submit.sh enfileira e retorna na hora; o
judged.sh é o único que fala com o juiz. O front descobre o
veredicto por polling do history (HTTP), não segurando a
conexão.
handlers/submit.sh (e
handlers/contest/rejudge.sh, set-verdict.sh)
escrevem um arquivo no spool
run/spool/submissions/. O nome carrega o
roteamento e o conteúdo é o JSON com os dados:
nome: <contest>:<epoch>:<id>:<login>:<comando>:<arg>[:<FILETYPE>]
comando ∈ { submit, rejulgar, setverdict, synctreino, newcontest, … }
submit: <c>:<ts>:<id>:<login>:submit:<problemid>:<LANG>
rejulgar: <c>:<ts>:<id>:<login>:rejulgar:<subid>
setverdict: <c>:<ts>:<id>:<login>:setverdict:<problemid>
Conteúdo (submit):
{contest,login,problem_id,filename,code_b64,lang,time,id}.
A escrita é atômica (.in.<id> →
mv), então o daemon só vê o arquivo pronto.
Logo após enfileirar, submit.sh anexa ao
contests/<c>/users/<login>/history uma linha
provisória terminada em :<id> com veredicto
Not Answered Yet (e recomputa o metrics.json
do usuário, p/ o PENDING aparecer no placar) — é o que o front mostra
como "julgando" enquanto faz polling.
Formato do history por-usuário (6 campos, login implícito no
diretório):
tempo:problemid:lang:verdict:epoch:subid. Os leitores
agregados usam
emit_user_history/emit_history_stream
(lib/users.sh), que reinjetam o login e entregam o formato global de 7
campos tempo:login:problemid:lang:verdict:epoch:subid.
server/daemons/judged.shServiço systemd moj-judged. Observa o
spool com inotifywait -m -e create -e moved_to (fallback:
poll de 1s). Para cada arquivo: lê o JSON, chama judge_run,
e aplica o veredicto reescrevendo só a linha com sufixo
:<id> no history do usuário
(casamento seguro, reescrita atômica via mv), recomputa
users/<login>/metrics.json, arquiva o fonte
decodificado, e dispara
server/score/build.sh <contest> para recalcular o
placar. Por fim move o arquivo para
run/spool/submissions-done/.
No modo fila/pull (INTAKE_MODE=queue,
ver server/judge-gw/PULL.md), em vez de julgar na hora o
daemon enfileira um job JSON
{id,contest,problem_id,login,lang,filename, code_b64,priority,enqueued_at,allowed_hosts?}
na banda do CONTEST_PRIORITY; um juiz o reivindica no
heartbeat. allowed_hosts é o pool de
juízes efetivo (override do problema em
problem-judges.json → CONTEST_JUDGES do conf;
ausente = qualquer juiz) — o claim é estrito: com o
pool offline o job espera na fila (preflight/dashboard avisam).
O resultado do juiz carrega, além do verdict de display
(com o score embutido, ex. Accepted,100p), os campos
estruturados verdict_canon (canônico
sem score), score/score_max/score_kind,
correct/total_tests e groups
(subtarefas [{earned,max},…], quando o problema pontua por
grupos) — persistidos em results/<id>.json e servidos
pelo /submission/summary. Ao competidor o veredicto
servido é SEMPRE o canônico (todos os modos;
lib/verdict.sh canoniza os endpoints de history na leitura
— o history em disco não muda) e o summary é redigido por
modo: treino/lista = tudo; obi/heurístico/outro =
score/grupos/heur; icpc/ausente = só o canônico. No modo
veredicto manual, o casamento da matriz de auto-veredicto usa o
verdict_canon (não a string com score), e
os erros de juiz
(Judge Error/No_Servers) também são
segurados p/ revisão — o competidor vê só
Not Answered Yet até um veredicto sair.
server/judge-gw/judge.sh (dev/legado)Expõe
judge_run <contest> <problemid> <lang> <code_b64> <filename>
→ ecoa o veredicto. Não é usado no modo pull de
produção (lá o daemon enfileira antes de chamar
judge_run — ver §2 e PULL.md). Fica p/ dev e
p/ o intake legado (INTAKE_MODE=legacy), com dois backends
por $JUDGE_BACKEND:
mock (default em dev) — heurística
local, sem juiz; bom para testar a malha assíncrona.local — compila/roda na própria
máquina via mojtools (bubblewrap sandbox).Em produção (INTAKE_MODE=queue JUDGE_BACKEND=queue) o
julgamento é pull, e o lado servidor é a biblioteca
server/judge-gw/sched-lib.sh + os handlers
handlers/judge/*. Não há master, worker nem porta de
entrada p/ os juízes:
daemon enfileira ─▶ run/queue/<banda>/<id>.json (bandas 000-super … 080-…)
juiz (repo judge/, agente moj-agent@)
POST /judge/heartbeat ─▶ reivindica um job (claim atômico flock+mv → run/assigned/<host>/)
GET /judge/package ─▶ baixa o pacote sob demanda p/ o cache local (calibra na 1ª vez)
...julga (mojtools/build-and-test.sh, sandbox bwrap)...
POST /judge/result ─▶ sched-lib grava run/results/<id>.json (o daemon consome via consumer)
POST /judge/tl-report ─▶ reporta o TL calibrado (run/tl/<id>.json)
allowed_hosts (pool de juízes do problema/contest) é
respeitado no claim: com o pool offline o job espera na
fila (o preflight/dashboard avisam). Protocolo completo em
server/judge-gw/PULL.md.
sched-lib.sh +
handlers/judge/*Cada juiz se registra (POST /judge/register) e manda
heartbeat (POST /judge/heartbeat), gravando um registro
JSON por host:
run/registry/<host>.json (cpu, langs, slots, last_seen; vivo = last_seen recente)
last_seen < agora-REG_TTL (30s) = juiz morto (sai do free-set)
O escalonador in-daemon (sched-lib.sh) casa job×juiz por
capacidade e por allowed_hosts, com claim
atômico (flock+mv p/
run/assigned/); job reivindicado sem novo beat em
ASSIGN_TTL (120s) volta p/ a fila.
judge/ (máquinas separadas, modelo pull)Uma máquina de juiz clona só judge/ +
mojtools/ (não o cdmoj) e sobe o agente
moj-agent@<cap>
(pos/gpu/cm/hu). O
agente: registra a capacidade, puxa job no heartbeat, baixa o pacote sob
demanda p/ um cache local, calibra na 1ª vez e
reporta o TL, e roda a solução em sandbox
bubblewrap (mojtools/cage-run.sh +
lang/<lang>/{compile,run}.sh, tipicamente sobre um
rootfs moj-sysroot). Ver judge/README.md.
A API expõe o estado dos juízes por
handlers/treino/admin/judges.sh (painel admin,
model:"pull") e a página pública /status/
(handlers/index/status.sh) agrega fila + juízes + liveness
do daemon.
server/score/build.shChamado pelo judged.sh após cada veredicto. Lê
contests/<c>/conf (CONTEST_TYPE), gera o
placar a partir de users/*/metrics.json (uma passada; ver
SCOREBOARD.md) e grava
contests/<c>/var/placar.txt — 1ª linha =
modo
(icpc/obi/treino/heuristic/outro),
as demais já ordenadas. O front (web/contest/score/) busca
GET /contest/score, despacha pelo modo e renderiza (com
bandeiras locais de /shared/flags/, regras
teams-meta, modo anônimo, etc.).
O mesmo mecanismo (API → spool → daemon) serve comandos
administrativos vindos da web e do mojinho-bot (cliente
da API): synctreino, newcontest,
adduser, passwd,
alteravigenciacontest, rejulgar. O
jplag roda à parte:
handlers/contest/admin/jplag-run.sh dispara
server/score/jplag-run.sh em background, que junta as
soluções aceitas, roda o jar e grava os pares de similaridade em
contests/<c>/jplag/.
server/etc/systemd/)| Unit | Papel |
|---|---|
moj-fcgiwrap.socket/.service |
socket + fcgiwrap que roda o router.sh (a API) |
moj-judged.service |
daemon que consome o spool e enfileira p/ o pull |
moj-agent@.service |
agente do juiz (repo judge/, nas máquinas de
julgamento; @pos/@gpu/…) |
moj-bot.service |
mojinho-bot (cliente da API) |
moj-contest-backup@.service/.timer |
snapshot rotacionado do contest %i a cada 5 min durante
a prova (server/bin/contest-backup.sh: tar de
contests/<c>/ + spool pendente; ligar o timer no
dia) |
run/registry/<host>.json).inotifywait + flock, fiel ao bash-nativo do
MOJ.