Pular para conteúdo

ADR-001: Estratégia de Fallback para Google Gemini API

Status

🟢 ACEITO — Opção A (decidido pela equipe em 2026-06-17)

Contexto

O sistema utiliza Google Gemini para gerar resumos concisos de ocorrências criminais. A API Gemini pode falhar por: - Timeout (30s limite) - Rate limiting (quota excedida) - Indisponibilidade (erro 5xx) - Erro permanente (invalid input)

Pergunta: Como o sistema deve se comportar quando Gemini falha?

Opções Consideradas

Opção A: Retorna Notícia sem Resumo Imediatamente

  • Implementação: Card mostra titulo, data, localização, mas resumo aparece vazio/"Indisponível"
  • Prós: UX rápida; usuário vê informação básica; sem delay
  • Contras: Informação incompleta; necessário campo visual "resumo pending" no DB
  • Impacto: Baixo custo; UX degradada

Opção B: Enfileira para Retry Assíncrono

  • Implementação: Ocorrência persistida sem resumo; worker assíncrono tenta novamente a cada 1h
  • Prós: Eventualmente consistente; resumo aparece após alguns minutos
  • Contras: Card inicialmente vazio; requer job queue (ex: Celery, RQ); lógica mais complexa
  • Impacto: Médio custo; UX melhor eventualmente; operacional mais complexo

Opção C: Usa Resumo Genérico Padrão

  • Implementação: Se Gemini falha, usa template: "Crime de [TIPO] registrado em [LOCAL] em [DATA]"
  • Prós: Card sempre completo; sem delay; simples
  • Contras: Menos informativo; perde valor da IA; false sense of completeness
  • Impacto: Baixo custo; UX consistente; valor de IA perdido

Opção D: Circuit Breaker + Fallback Híbrido

  • Implementação:
  • Gemini fails < 3x: retry com backoff exponencial
  • Gemini fails > 3x ou timeout > 5min: circuit breaker abre
  • Circuit aberto: Opção C (resumo genérico) ou Opção A (vazio)
  • Prós: Resiliência máxima; comportamento gradual; protege backend
  • Contras: Implementação mais complexa; requer monitoring
  • Impacto: Alto custo inicial; melhor em produção; mais observabilidade

Recomendação (Proposta)

⚠️ Histórico: esta era a proposta inicial. A equipe optou pela Opção A (ver Decisão Final). A Opção D fica registrada como possível evolução futura quando o volume/escala justificar o custo de circuit breaker.

Opção D: Circuit Breaker + Fallback Híbrido

  • Justificativa: Sistema crítico de segurança pública merece máxima resiliência
  • Implementação:
  • Primeira falha: retry com backoff 1s → 2s → 4s
  • Após 3 falhas ou timeout > 5min: circuit breaker abre
  • Enquanto aberto: resumo genérico (Opção C) + log warning
  • Quando circuit fecha: tenta Gemini novamente

Decisão Final

Opção A — Retorna Notícia sem Resumo Imediatamente (equipe, 2026-06-17)

Quando o Gemini falhar (timeout, rate limit, 5xx ou input inválido), o sistema não bloqueia a entrega da ocorrência: persiste e exibe o card com título, data e localização, e o resumo aparece como indisponível.

Justificativa

  • Simplicidade e custo: não exige job queue (Celery/RQ) nem circuit breaker — escopo adequado ao estágio atual do projeto.
  • UX sem delay: o usuário vê a informação básica imediatamente; o sistema nunca trava esperando a IA.
  • A resiliência maior (Opção D) fica como evolução futura, sem reescrever o fluxo — basta trocar o comportamento de fallback.

Consequências para a implementação

  • O campo resumo_status registra o estado: COMPLETO quando o Gemini responde; ERRO quando falha (resumo fica nulo/"Indisponível").
  • O campo resumo (resumo_gemini) é nullable — pode ficar vazio.
  • Sem retry assíncrono e sem resumo genérico: a falha é refletida no status, não mascarada.
  • Frontend deve tratar resumo_status = "ERRO" exibindo "Resumo indisponível".

Critério de Aceitação

  • [x] Equipe concordou com o padrão (2026-06-17)
  • [ ] Implementação em models/ + services/ (camada de integração Gemini)
  • [ ] Testes Pytest cobrindo os caminhos: sucesso (COMPLETO) e falha (ERRO)
  • [ ] Frontend trata resumo_status = "ERRO"

Referências

Adendo (2026-06-30): retry síncrono para erro transitório (5xx)

Em produção, observamos que o Gemini retorna 503 UNAVAILABLE ("model is currently experiencing high demand") de forma intermitente — em torno de 40% das chamadas em um teste com 8 notícias seguidas. Sem retry, cada uma dessas falhas transitórias virava status="ERRO" permanente (a ingestão não reprocessa notícia já persistida, por dedup de URL).

Isso não muda a decisão da Opção A: o sistema continua sem fila de retry assíncrona (Opção B) e sem circuit breaker (Opção D). O que foi adicionado é só a parte de retry com backoff que já estava na proposta original da Opção D (1s → 2s → 4s), aplicada dentro da mesma chamada síncrona em GeminiClient._gerar_real, e só para ServerError (5xx) — erros permanentes (ClientError: chave inválida, input rejeitado) continuam falhando na primeira tentativa, sem retry. Esgotados os retries, o comportamento volta a ser exatamente o da Opção A: resumo=None, status="ERRO".