MOJ — Fluxo de comunicação (API, daemons e juízes pull) — MOJ docs

MOJ — Fluxo de comunicação (API, daemons e juízes pull)

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).

Visão geral

  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.

1. API → spool (enfileiramento)

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.

2. Daemon de julgamento — server/daemons/judged.sh

Serviç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.jsonCONTEST_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.

3. Gateway de juiz — 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:

4. Julgamento pull — os juízes puxam o job

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.

5. Registro + heartbeat — 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.

6. Juízes — 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.

7. Placar — server/score/build.sh

Chamado 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.txt1ª 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.).

8. Outros caminhos pelo mesmo spool

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/.

Serviços systemd (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)

Resumo do "porquê"