Fontes de dados externos — ANATEL e OpenCelliD¶
Este documento descreve, de forma auditável e honesta, de onde vêm os dados de torres usados pela plataforma, quais são as limitações de cada fonte, e quais controles compensatórios foram adotados.
Matriz de fontes¶
| Fonte | Tipo | Origem real | SLA upstream | Cadência | Resiliência |
|---|---|---|---|---|---|
| ANATEL ERBs | externa | Planilha XLSX obtida via LAI, redistribuída por LuSrodri/ERBs_per_city_per_operators_brazil | nenhum (curadoria manual a montante) | cron 4x/dia (00:05/06:05/12:05/18:05 UTC) via .github/workflows/pull-anatel.yml; re-ingere apenas se SHA do XLSX upstream mudar — o ciclo sem mudança é uma única chamada à Contents API, sem download nem escrita |
geocodificação com cache local; dedupe 443k→105k; sync-state em S3; Slack |
| OpenCelliD MCC-724 | externa | https://opencellid.org/ (API com token) | nenhum (serviço gratuito, rate-limited) | cron 4x/dia (00:25/06:25/12:25/18:25 UTC) via .github/workflows/pull-opencellid.yml; sem skip por mudança de conteúdo — todo ciclo tenta baixar (~1,2 MB gz) e reexecuta o COPY idempotente. A quota do vendor está medida em 2 downloads por ficheiro por dia, portanto só os dois primeiros ciclos de cada dia UTC ingerem; os outros dois publicam skipped_rate_limited (ciclo verde, sem ingestão) |
4 tentativas totais (3 retries) com backoff exponencial (30s/60s/120s); o fallback de cache local vale só para execução de operador — o runner de CI é efêmero, então um ciclo estrangulado pela quota publica skipped_rate_limited e preserva o last_ingest_at e o towers_in_db_after anteriores em vez de zerar o relógio ou a contagem — um download recusado não verificou nada, logo não pode creditar o relógio nem apagar a última medição; sync-state em S3; Slack |
| AWS RDS → Railway | replicação interna (APOSENTADA) | — | — | — | sync-towers.yml foi desativado (#501): o Postgres do Railway é a DB primária e os crons de ingestão escrevem nela diretamente |
ANATEL — o que é e o que não é¶
O que é: snapshot manual de uma planilha Excel publicada via LAI (Lei
de Acesso à Informação), redistribuída por um repositório público de terceiro.
Cada execução de load_anatel.py --file ERBs_com_equipamentos_v2.xlsx carrega
443.396 registros brutos, deduplica para 105.240 estações únicas, geocodifica
5.570 municípios brasileiros via Nominatim (com cache em
geocode_cache_br.json) e popula a tabela towers.
O que não é: não é integração em tempo real com sistemas.anatel.gov.br.
Não existe API pública oficial da ANATEL para ERBs com coordenadas
geográficas. Cabe ao operador da plataforma atualizar a planilha periodicamente.
Limitação honesta: o repositório upstream depende de curadoria manual
de um terceiro. A plataforma detecta novas versões automaticamente via
GitHub Contents API (compara o SHA do blob com o último ingerido, persistido
em s3://.../snapshots/anatel-pull/_state.json) e re-ingere apenas quando o
upstream muda. O endpoint /status/data-freshness expõe age_hours desta
última ingestão e flag stale=true quando excede 240h (10 dias).
OpenCelliD — limitações conhecidas¶
O que é: API gratuita de uma base colaborativa de células rádio mantida pela comunidade. Token de autenticação obrigatório (registro gratuito em opencellid.org).
Limitações:
- Sem SLA. Serviço já passou por períodos de instabilidade e mudanças de política de acesso historicamente.
- Rate-limited: a resposta
RATE_LIMITEDindica cota excedida. A cota está medida em 2 downloads por ficheiro por dia ("we only allow 2 downloads per file, per day"), o que com o cron 4x/dia torna os ciclos 3 e 4 uma recusa determinística — umskipped_rate_limitednomeado, não uma falha intermitente. - Cobertura é colaborativa — qualidade varia por região.
Controles compensatórios (load_opencellid.py::download_brazil_csv):
- Timeout de 300s no
requests.get. - Retry com backoff exponencial: 4 tentativas totais (3 retries),
esperas 30s/60s/120s entre tentativas,
apenas em erros transitórios (
ConnectionError,Timeout,HTTPError5xx/429). Erros 4xx (exceto 429) falham imediatamente para não mascarar token inválido. - Cache local persistente em
.opencellid_cache/724.csv.gz. Acionado automaticamente em:- resposta
RATE_LIMITED, - exaustão das 3 tentativas com erro de rede.
- resposta
- Validação de magic bytes do gzip — qualquer arquivo que não seja gzip válido aborta a ingestão (defesa contra HTML/JSON disfarçado).
Replicação AWS RDS → Railway (aposentada)¶
O cron .github/workflows/sync-towers.yml foi desativado em #501:
com a migração AWS→Railway concluída, o Postgres do Railway é a base
primária de torres e os crons de ingestão (ANATEL/OpenCelliD) escrevem nela
diretamente — não existe mais lag de replicação a monitorar. O endpoint
/status/data-freshness mantém a chave tower_sync com status: "retired"
por compatibilidade de shape da API.
Endpoint /status/data-freshness¶
Exposto pela API. Lê os sync-state JSONs das duas fontes ativas
(anatel-pull e opencellid-pull, ambas 4x ao dia) do storage S3-compatível
(MinIO do Railway em produção, via S3_ENDPOINT_URL) com cache em
memória (TTL 5 min). Devolve por fonte: status, idade, threshold de
staleness e flag stale. Campo any_stale resume as fontes ativas e é o
gatilho do gate 409 (Fase C).
Thresholds:
anatel_pull: > 18h → stale (cron 4x/dia; 3x o período de 6 h). O ciclo no-op publicaskipped_unchanged, cuja base de idade é o instante da verificação, então a idade em regime é ≤ 6 h e 18 h ainda absorve dois ciclos perdidos seguidos.opencellid_pull: > 36h → stale (cron 4x/dia; 6x o período). Só um ciclo bem-sucedido (ingested) move o relógio: o skip de quota (skipped_rate_limited) e uma falha (failed) preservam olast_ingest_atanterior, porque um download recusado não verificou nada — ao contrário doskipped_unchangedda ANATEL, não pode pedir emprestada a hora da verificação. Com a quota medida em 2 downloads/ficheiro/dia, o regime são dois ingests reais por dia (00:25 e 06:25 UTC), ou seja ≤ 18 h entre ingests, e as 36 h absorvem um dia inteiro perdido — ainda um aperto de 6,7x em relação aos 240 h antigos.
Os dois thresholds são acoplados mecanicamente ao cron por
tests/test_ingest_cadence_slo_copy_invariant.py: mover um sem o outro
reprova o CI.
Exemplo de resposta abreviada:
{
"sources": [ { "name": "ANATEL ERBs (LAI XLSX via LuSrodri)", "...": "..." } ],
"anatel_pull": {
"status": "ok",
"last_completed_at": "2026-07-13T08:07:42Z",
"age_hours": 112.4,
"stale": false
},
"tower_sync": { "status": "retired", "stale": false },
"towers_in_db": 105240
}
Gaps reconhecidos¶
- ANATEL upstream é curadoria manual de terceiro — não há mecanismo contratual para garantir cadência. Mitigação: cron 4x/dia automatizado detecta novas versões via SHA do GitHub upstream; endpoint expõe idade da última ingestão; alerta operacional via Slack em falhas.
- OpenCelliD é serviço gratuito sem SLA. Mitigação: cron 4x/dia + cache local + retry/backoff. Em caso de descontinuação prolongada, será necessária outra fonte (avaliação aberta).
- A ingestão é agendada e auditável via workflows
.github/workflows/ pull-anatel.ymle.github/workflows/pull-opencellid.yml, com sync-state JSON no MinIO do Railway (S3-compatível) e Slack notify. A publicação do sync-state é fatal em falha — uma publicação silenciosamente quebrada já deixou o gate de staleness desarmado por semanas (bucket AWS da conta encerrada).
Referências de código¶
- load_anatel.py
- load_opencellid.py
- .github/workflows/pull-anatel.yml
- .github/workflows/pull-opencellid.yml
- Endpoint:
telecom_tower_power_api.py—/status/data-freshness