Integração
Fila de tarefas
Jobs, estados, executores, artefatos e o catálogo de tarefas.
Ponto único para pedir downloads, raspagens e execuções de crawlers. Quem pede recebe um id na hora (HTTP 202) e acompanha o job até o fim; a execução acontece num subprocesso isolado, com timeout, cancelamento, novas tentativas e artefatos guardados por job.
cliente ──POST /jobs──▶ API ──▶ PostgreSQL, schema jobs
▲ │ proxima() (advisory lock + SKIP LOCKED)
│ ▼
finalizar()/heartbeat │ executor (core/jobs/runner.py, 1 por máquina,
│ │ N máquinas por fila)
│ │ python -m core.jobs.worker <id>
└────────┴──▶ tarefa registrada (core/jobs/tarefas/)
└─▶ JOBS_DIR/execucoes/<id>/ (disco do executor)
Banco de dados
Tudo fica no schema jobs do PostgreSQL, isolado do resto do banco:
| Tabela | Conteúdo |
|---|---|
jobs.jobs | Um pedido: tarefa, parâmetros (JSONB), status, prioridade, recurso, chave, tentativas, lease do executor, resultado da última tentativa |
jobs.tentativas | Uma linha por tentativa (executor, início, fim, status, resultado): o histórico que jobs.jobs sobrescreve |
jobs.artefatos | Manifesto dos arquivos (caminho, bytes, sha256, executor, uri para armazenamento compartilhado) |
jobs.alembic_version | Versão das migrations deste schema |
As regras ficam no banco, não só no código: CHECK de status, nome de tarefa,
faixa de prioridade e tentativas, caminho de artefato sem .., e o índice
único parcial que garante uma chave ativa por vez.
Conexão: JOBS_DATABASE_URL (postgresql://user:senha@host:5432/banco) ou,
na falta dela, o banco principal (DATABASE_URL), o mesmo do resto do
repositório (ver core/database/config.py). Sem nenhuma das duas a API sobe, mas o
executor fica desligado (aviso no log).
Migrations
Alembic com ambiente próprio em core/jobs/migrations/. A tabela de versão
mora em jobs.alembic_version, então não interfere em nada fora do schema.
uv run python -m core.jobs migrar # upgrade head
uv run alembic -c core/jobs/alembic.ini current
uv run alembic -c core/jobs/alembic.ini upgrade head --sql # só gera o SQL, para revisão
uv run alembic -c core/jobs/alembic.ini downgrade -1
uv run alembic -c core/jobs/alembic.ini revision -m "descricao" # nova migration
Migrations em SQL explícito (op.execute), sem autogenerate: não há modelos
SQLAlchemy da fila, e o SQL é o que se revisa.
Subir
uv run python -m core.jobs migrar
# API + executor no mesmo processo (default)
uv run uvicorn api.main:app --port 8000
# ou o executor à parte (ex.: no poly-lab, onde ficam o token A3 e o X11)
JOBS_RUNNER_NA_API=0 uv run uvicorn api.main:app --port 8000
JOBS_TAREFAS='inss.*,govbr.*' uv run python -m core.jobs
uv run python -m core.jobs --tarefas # catálogo em JSON
Vários executores
Qualquer número de máquinas pode consumir a mesma fila. Cada uma escolhe o
que aceita com JOBS_TAREFAS (padrões com *; vazio = tudo). Na mesma
máquina só um processo executa (flock em JOBS_DIR/runner.lock).
Cada job rodando tem executor (host:pid) e heartbeat_em, renovado a cada
5 s. Se um executor morrer, o lease vence em JOBS_LEASE_S e qualquer outro
executor dá o job como interrompido (e ele volta à fila se houver
tentativas). Um executor que reinicia recolhe na hora o que o processo antigo
do mesmo host deixou rodando.
Artefatos no MinIO
Com o MinIO configurado (MINIO_ENDPOINT, MINIO_ACCESS_KEY,
MINIO_SECRET_KEY), o executor envia cada artefato ao fim do job para
s3://<JOBS_BUCKET>/<JOBS_BUCKET_PREFIXO>/<job>/<caminho> (default
s3://crawlers-jobs/jobs/<job>/...; o bucket é criado se não existir) e grava
o uri em jobs.artefatos. A API serve o arquivo do disco local quando ele
está lá e, senão, faz streaming do MinIO — de qualquer máquina.
Sem MinIO (ou com JOBS_BUCKET= vazio), os artefatos ficam só no disco de
quem executou; nas outras máquinas o download responde 409. Falha no envio não
derruba o job: o artefato fica sem uri e o erro vai para o log.
API
Todas as rotas exigem API key (Authorization: Bearer crw_…) ou a sessão do
painel; cada usuário vê só os próprios jobs. Ver docs/contas-e-cofre.md.
Uma tarefa que usa credencial do cofre recebe credencial_id no pedido.
| Método | Rota | |
|---|---|---|
| GET | /jobs/tarefas | Catálogo: nome, descrição, timeout, tentativas, recurso e JSON Schema dos parâmetros |
| POST | /jobs | Enfileira um job → 202 |
| POST | /jobs/lote | Enfileira até 1000 jobs; tudo ou nada |
| GET | /jobs?status=&tarefa=&limite= | Lista + contagem por status |
| GET | /jobs/{id} | Estado, etapa atual, tentativa e resultado |
| GET | /jobs/{id}/tentativas | Histórico de tentativas |
| POST | /jobs/{id}/cancelar | Cancela na fila ou em execução (mata o grupo de processos) |
| GET | /jobs/{id}/artefatos | Manifesto dos arquivos produzidos |
| GET | /jobs/{id}/artefatos/{caminho} | Download (disco local ou MinIO; 409 se só existe no disco de outro executor) |
curl -X POST localhost:8000/jobs -H "Authorization: $TOKEN" -H 'content-type: application/json' -d '{
"tarefa": "download.http",
"parametros": {"url": "https://exemplo.jus.br/relatorio.pdf"},
"prioridade": 5,
"chave_idempotencia": "relatorio-2026-10",
"origem": "n8n"
}'
- prioridade (-10..10): maior roda antes; empate por ordem de chegada.
- chave_idempotencia: se já houver job ativo com a chave, ele é devolvido
(
"duplicado": true) em vez de criar outro. - max_tentativas: sobrescreve o default da tarefa.
Estados
na_fila → rodando → um destes:
| Status | Significado | Repete? |
|---|---|---|
sucesso / parcial | Terminou; parcial = dados incompletos | não |
sem_dados | Rodou, nada a extrair | não |
bloqueado | Pré-requisito ausente (credencial, token, destino barrado) | não |
falha / timeout / interrompido | Erro, prazo excedido, serviço reiniciado | sim, se houver tentativas (espera 30s, 60s, 120s…) |
cancelado | Pedido pelo usuário | não |
O traceback fica em erro.txt e a saída do processo em execucao.log, ambos
só no servidor — a API não os expõe, porque podem conter URLs com sessão.
Tarefas disponíveis
| Tarefa | Recurso | Faz |
|---|---|---|
diagnostico.eco | — | Testa a fila de ponta a ponta |
download.http | — | Baixa URL pública para os artefatos (sha256, limite de tamanho, SSRF barrado) |
govbr.verificar_sessao | — | Confere a sessão gov.br salva, sem navegador |
govbr.login | govbr_navegador | Login gov.br no Chromium e salva a sessão |
inss.gerid.verificar_extensao | token_a3 | Extensão BRy + certificados válidos no token, sem PIN |
inss.gerid.login | token_a3 | Login completo no PAT (certificado, PIN, MFA, consentimento) |
Recurso é um nome de exclusão mútua: jobs com o mesmo recurso nunca rodam
juntos, mesmo com JOBS_CONCORRENCIA > 1. Tarefas que gastam tentativa de PIN
têm max_tentativas=1: repetir sozinho pode bloquear o token.
Adicionar uma tarefa
# core/jobs/tarefas/tjxx.py
from pydantic import BaseModel, ConfigDict
from core.jobs.registry import tarefa
class Params(BaseModel):
model_config = ConfigDict(extra="forbid") # parâmetro desconhecido = 422
processo: str
@tarefa("tjxx.extrair_processo", parametros=Params, timeout_s=600, max_tentativas=2)
async def extrair(ctx, params: Params) -> dict:
"""Primeira linha da docstring vira a descrição no catálogo."""
ctx.etapa("Abrindo processo") # aparece em GET /jobs/{id}
(ctx.pasta / "processo.json").write_text("...") # vira artefato
if ctx.cancelado():
return {"status": "falha", "detalhe": "cancelado"}
return {"status": "sucesso", "arquivo": "processo.json"}
Depois, importe o módulo em core/jobs/tarefas/__init__.py. Imports pesados
(Playwright, crawlers) ficam dentro da função, para a API listar o catálogo sem
carregá-los. Segredos nunca são parâmetros: a tarefa lê do ambiente.
Variáveis
| Variável | Default | |
|---|---|---|
JOBS_DATABASE_URL | — | DSN do Postgres da fila |
JOBS_DATABASE | principal | Sem JOBS_DATABASE_URL: nome do banco em core/database/config.py |
JOBS_DB_POOL | 5 | Conexões por processo |
JOBS_DIR | cache/jobs | Lock e artefatos locais do executor (modo 700) |
JOBS_CONCORRENCIA | 2 | Jobs simultâneos por máquina |
JOBS_TAREFAS | (todas) | Padrões das tarefas que este executor aceita |
JOBS_LEASE_S | 120 | Sem heartbeat por esse tempo, o job é dado como interrompido |
JOBS_RUNNER_NA_API | 1 | 0 quando o executor roda com python -m core.jobs |
JOBS_BUCKET | crawlers-jobs | Bucket dos artefatos no MinIO; vazio = só disco local |
JOBS_BUCKET_PREFIXO | jobs | Prefixo das chaves no bucket |
JOBS_PERMITIR_REDE_PRIVADA | 0 | 1 libera download.http para IPs internos |
Relação com as filas existentes
processo.fila_downloads (usada pelo extractor) é outra coisa: fila de
documentos amarrada a processo.processos, particionada por tribunal. Não foi
alterada; um crawler de processo pode virar tarefa daqui e continuar gravando lá.
core/precatorios (painel /precatorios/monitor/ui) e core/health têm filas
próprias em SQLite local. Elas seguem funcionando como
estão; migrar as coletas LOA para tarefas precatorios.* desta fila é o
próximo passo natural.