# Alta disponibilidade e failover automático

Requisito definido pelo responsável em 17/08/2026:

> A nova versão precisa conter um mecanismo automático e que não precise de
> intervenção humana.

Origem: em 2025 a operação dependia de um datacenter único que ficou **36 horas
fora do ar**, com prejuízo para unidades, franqueados, profissionais e
franqueadora.

Este documento registra o que esse requisito exige de verdade, o que hoje
impediria o failover de funcionar, e as decisões que faltam.

---

## Estado atual

Não existe procedimento de failover escrito nem ensaiado. Na prática, hoje:

> Em caso de falha do primário, uma das réplicas é aberta **somente leitura**,
> para acompanhamento da agenda das 32 unidades.

Isso significa que, durante uma queda, a operação degrada para **consulta de
agenda**. Não se vende, não se abre caixa, não se agenda, não se fecha comanda.
É melhor que nada — e é uma decisão consciente, não um acidente —, mas é o
oposto de "sem intervenção humana": depende de alguém disponível, que saiba os
passos, com acesso, na hora em que acontecer.

---

## O que "automático, sem intervenção humana" exige

Failover não é uma peça, são quatro camadas. Falhar em qualquer uma faz o
conjunto não funcionar — e as três últimas costumam ser esquecidas porque a
primeira é a única que aparece nos tutoriais.

### 1. Dados — promoção automática do banco

A camada resolvida. No PostgreSQL o padrão maduro é **Patroni** com um repositório
de consenso (etcd ou Consul): ele monitora os nós, elege o novo primário e
reconfigura os demais para seguirem o eleito, sem ninguém digitar nada.

É motivo forte para **não** construir HA no MySQL agora: seria montar uma
solução (Orchestrator, InnoDB Cluster) para jogar fora na Fase 1. O desenho de
alta disponibilidade nasce junto com o PostgreSQL.

### 2. Decisão — quórum e isolamento do nó falho

Recurso a implementar: quórum (maioria decide, nó isolado não promove) e
*fencing* (o nó suspeito perde a escrita antes da promoção). Exige três votantes
com conectividade confiável entre si.

O responsável já tem o desenho dessa parte e avalia o risco como pequeno; fica
registrado como funcionalidade necessária, não como questão em aberto.

### 3. Tráfego — como as 32 unidades chegam ao novo primário

Banco promovido não adianta se as requisições continuam indo para a máquina
morta. As opções realistas nesta escala:

| Mecanismo | Observação |
|---|---|
| DNS com verificação de saúde (Cloudflare) | Mais simples; troca em ~30 s com TTL baixo |
| IP flutuante do provedor | Depende de a Hostinger oferecer, e só vale dentro dela |
| Proxy reverso na frente | Vira ele mesmo um ponto único de falha |

DNS com health check é provavelmente a resposta para o tamanho de vocês. Vale
lembrar que o **aplicativo móvel** também resolve por DNS, e há versões antigas
em campo (D5) — o mecanismo precisa funcionar para elas sem atualização de app.

### 4. Arquivos — o furo que ninguém vê até acontecer

**Este é o problema mais concreto hoje.** O sistema grava arquivos enviados pelo
usuário no disco local, em pelo menos sete diretórios:

`public/arquivos` · `public/contratos` · `public/banners` · `public/assistentes` ·
`public/vip-class/{capas,materiais,certificados}` · `public/layout/img/avatar`

A replicação do banco **não replica esses arquivos**. Numa promoção, o sistema
sobe e funciona — mas contratos, certificados do VIP Class, materiais, banners
do aplicativo e as fotos dos colaboradores simplesmente não existem no nó novo.
Referência no banco apontando para arquivo inexistente, em silêncio.

Solução: tirar os uploads do disco local. Armazenamento de objetos compatível
com S3 resolve — MinIO auto-hospedado, ou um serviço gerenciado. É mudança de
código, e por isso precisa entrar no planejamento da migração, não ser descoberta
no primeiro failover.

### 5. Sessões (menor, mas do mesmo tipo)

`SessaoAcl` usa `Laminas\Session\Container`, ou seja, o manipulador padrão do
PHP: **arquivos no disco local**. Numa promoção todo mundo é deslogado.

Incômodo aceitável, e barato de resolver junto — sessão em banco ou Redis — já
que o mesmo problema de estado local está sendo tratado para os uploads.

---

## O que impediria o failover hoje

Dois itens, e nenhum é sobre o banco:

1. **Os uploads não são replicados.** Camada 4 acima. Failover "bem-sucedido"
   entregaria um sistema com arquivos faltando. É o único item desta lista que
   exige mudança de código.
2. **O Orange Pi está atrás de CGNAT.** Não recebe conexão de entrada, logo não
   pode assumir tráfego como primário nem votar. Uma VPN em malha (WireGuard ou
   Tailscale) resolve e é barata — mas precisa existir antes de ele contar como
   votante.

E um terceiro, já registrado como **D27**: o servidor 1 roda MySQL 8.4 contra
8.0.4x dos demais. Hoje ele é réplica de leitura, não nó de contingência.

---

## A decisão FOI TOMADA em 17/08/2026

O que esta seção chamava de decisão pendente está fechado, e o registro do
raciocínio ficou em `f1-2-metas-rpo-rto.md`:

| | |
|---|---|
| **RPO** | **0** para falha de máquina; **≤ 30 s** para perda de datacenter |
| **RTO** | **≤ 5 minutos**, automático |

O que sustenta os 5 minutos, na palavra do responsável: *"se cai, o caixa deixa
de conseguir fazer lançamento, mas a venda em si é realizada e o cliente é
atendido, tudo presencialmente."* O faturamento não evapora numa parada curta —
fica represado e entra depois.

E tem um limite estrutural, o **D28**: o sistema não sabe registrar uma venda na
data em que ela ocorreu. Por isso o lançamento posterior só é inócuo enquanto a
recuperação couber dentro do mesmo caixa — o que é a razão de a meta ser 5
minutos e não uma hora.

Traduzindo para arquitetura: **par síncrono no datacenter principal, nós remotos
assíncronos.**

---

## Ensaiado em 27/08/2026

O desenho acima deixou de ser proposta. Um agrupamento Patroni + etcd foi montado
localmente e o primário foi morto com SIGKILL, três vezes:

| Cenário | Tempo até aceitar escrita |
|---|---:|
| Assíncrono | **30 a 32 s** |
| Síncrono | **36 s** |

Contra a meta de 300 segundos. E o nó derrubado **voltou sozinho como réplica**.

Detalhe, e principalmente **o que o ensaio não prova**, em
`f1-7-ensaio-failover.md`. O laboratório é versionado e repetível:

```bash
docker compose -p franquiabv-ha -f docker-compose.ha.yml up -d
sh scripts/ensaiar-failover.sh
```

---

## Enquanto isso

A Fase 1 não é amanhã, e o risco de repetir 2025 existe **hoje**. Duas coisas
baratas, que não dependem de nada do que está acima:

1. **Escrever o procedimento atual.** O modo degradado que você já usa — abrir
   uma réplica em somente leitura para a agenda — funciona, mas só existe na sua
   cabeça. Escrito, ele pode ser executado por outra pessoa, às 3h, sem você.
2. **Ensaiar uma vez, cronometrando.** Um failover nunca ensaiado costuma falhar
   na primeira tentativa. O ensaio revela o que falta antes de custar dinheiro, e
   o cronômetro dá o número real para comparar com a meta de recuperação.

Isso vale mais, agora, que qualquer item ainda aberto na Fase 0 — inclusive a
W6.
