Skip to content

Checar HEAD/ETag antes do GET no extrator do ComprasGov - #88

Open
maia-andre wants to merge 5 commits into
mainfrom
feat/extrator-head-etag
Open

Checar HEAD/ETag antes do GET no extrator do ComprasGov#88
maia-andre wants to merge 5 commits into
mainfrom
feat/extrator-head-etag

Conversation

@maia-andre

Copy link
Copy Markdown
Collaborator

Resolve #87

O que muda com esse PR

Tudo contido em ingestion/pncp_comprasgov/extract.py; nada muda no pipeline.py nem no dbt.

  • Checagem barata antes do download. O manifesto ganha as colunas etag e last_modified. Para cada arquivo com ETag conhecido, um HEAD na fonte decide se o GET é necessário: ETag igual e parquet ainda no bucket → ignorado sem baixar. Qualquer dúvida (sem ETag, HEAD falhou, objeto sumiu do bucket, ETag divergente) cai no GET de sempre, com conferência de hash — o atalho nunca substitui a verificação, só a antecipa. HEAD 404 numa entrada conhecida vira indisponivel, o mesmo veredito que o GET 404 sempre deu.
  • Modo paranoico. COLIBRI_VERIFICACAO_COMPLETA=1 ignora os ETags e baixa tudo para conferir hash, como hoje.
  • Cliente S3 único por execução (utils.criar_cliente), em vez de recriar boto3.resource e reler o segredo a cada um dos milhares de HeadObject da varredura.
  • Contagem de linhas em streaming em registrar_entrada (csv.reader sobre TextIOWrapper(BytesIO(...))): o StringIO de hoje custa ~4× o tamanho do arquivo em RAM — ~19 GB extras para o anual de itens de 2025 (3,86 GB). Problema pré-existente, independente do ETag; sem ele a rodada completa não cabe em 15 GB.
  • 13 testes unitários da decisão HEAD × GET (tests/test_pncp_comprasgov_extract.py, sessão HTTP e bucket falsos, sem rede) e da contagem em streaming.
  • Documentação: docs/nota-extrator-comprasgov-head-etag.md (+ .pdf, mesmo conteúdo) com método, números e evidência. Se preferirem não versionar o PDF, tiro e fica só o .md.

Compatibilidade: manifestos antigos carregam normalmente (restval="" preenche as colunas novas). A primeira rodada após o merge custa o mesmo de hoje — baixa tudo e aprende os ETags pelo caminho atual; da segunda em diante a varredura fica barata.

Evidência

Bancada em MinIO local contra a fonte real, escopo de produção inteiro (2021-12-01 → hoje; 1.212 arquivos existentes + 4.176 períodos inexistentes). O censo é um HEAD por candidato: como o main faz GET incondicional em todo arquivo existente, a soma dos Content-Length é o custo exato de uma rodada dele.

rodada cenário GET c/ corpo HEAD MB baixados segundos
censo (= main, qualquer rodada) varredura completa 1.212 0 22.749,2
patch r1 primeira carga, aprende os ETags 1.212 0 22.749,2 3.684
patch r2 nada mudou 0 1.212 0,0 420
patch r3 ETag adulterado no anual COMPRA 2022 1 1.212 40,2 460
  • r1 baixou byte a byte o que o censo previu (1.212/1.212) e aprendeu 1.212/1.212 ETags iguais aos do censo.
  • r2 deixou o manifesto idêntico e alteracoes.csv vazio — nada para o dbt.
  • r3: a sabotagem causou exatamente 1 re-download, o hash confirmou conteúdo igual e o ETag correto foi regravado sozinho.
  • Recorte anterior (28/08, 18 arquivos, dois braços): statuses, alteracoes.csv, manifesto e parquets idênticos entre main e patch.

Veredito automático contra o censo: 20/20 checagens OK. Detalhe completo na nota em docs/.

Como testar

pytest tests/test_pncp_comprasgov_extract.py -v       # 13 testes, sem rede

# Contra um bucket de desenvolvimento (MinIO ou bucket próprio), duas rodadas:
colibri pipeline run --apenas pncp-comprasgov         # 1ª: baixa e aprende ETags (log "baixado")
colibri pipeline run --apenas pncp-comprasgov         # 2ª: log "ETag bate com manifesto, pulando sem baixar"; ~0 bytes

COLIBRI_VERIFICACAO_COMPLETA=1 colibri pipeline run --apenas pncp-comprasgov   # força GET + hash em tudo

Para reproduzir a bancada inteira (recorte em ~30–40 min; escopo completo em ~1h15, ~23 GB de banda) tenho o pacote "reproduzir" — mando na hora.

Checklist antes de mesclar na main

  • Formatar todos os arquivos python usando ruff format . (branch rebaseada sobre a main pós-Feat/ci GitHub actions #69; pre-commit run --all-files passa)
  • Executar testes unitários usando pytest — 51 passam (38 smoke + 13 deste PR)
  • N/A yamlfix (nenhum YAML tocado)
  • N/A dbt test (nenhum modelo dbt tocado)

Cliente S3 criado uma vez por execução (criar_cliente) em vez de um boto3.resource e uma leitura do segredo a cada arquivo — são centenas de checagens por varredura.

FalhaHead no lugar da sentinela False em obter_cabecalhos: qualquer falha do HEAD cai no GET de sempre, agora de forma explícita.

HEAD 404 numa entrada conhecida documentado: o arquivo sumiu da fonte, mesmo veredito do GET 404.

COLIBRI_VERIFICACAO_COMPLETA=1 desliga o atalho e força GET + hash em tudo (modo paranoico).

Docstring do módulo descreve a checagem barata.
12 casos com sessão HTTP e bucket simulados: primeira carga, manifesto sem ETag, ETag igual/diferente, objeto sumido do bucket, HEAD com falha de rede, HEAD 503, HEAD 404, ETag renovado sem mudança de conteúdo, modo de verificação completa e compatibilidade com o manifesto anterior (sem as colunas etag/last_modified).

tests/__init__.py vazio, como no #69.
registrar_entrada decodificava o CSV inteiro para str e o embrulhava num StringIO para contar as linhas — o StringIO custa 4x o tamanho do arquivo em memória (buffer UCS-4), além da str e do conteúdo. Para o anual de itens de 2025 (3,86 GB) são ~19 GB extras: a carga não cabe em 15 GB de RAM.

Ler com csv.reader sobre TextIOWrapper(BytesIO(conteudo)) custa +0 MB e dá a mesma contagem (medido com 100 MB: +395 MB antes, +0 MB depois). Foi o que permitiu rodar a bancada do HEAD/ETag no escopo completo (22,75 GB) nesta máquina.
Nota técnica (Markdown + PDF) com o experimento que mede a troca do GET incondicional pela checagem HEAD/ETag: censo HEAD do escopo de produção (1.212 arquivos, 22,75 GB por rodada), rodadas de escopo completo (22,75 GB -> 0 bytes na varredura sem mudanças; sabotagem de ETag custa exatamente 1 download) e os achados de bancada, incluindo a memória do extrator e as reescritas em lote dos anuais 2021-2024.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pncp_comprasgov: a varredura re-baixa 22,75 GB por rodada para conferir hash — checar ETag via HEAD antes do GET

1 participant