# F1.7 — O ensaio de failover

Feito em 27/08/2026, num laboratório local, antes de tocar no parque real.

O portão da frente pede **failover disparado de propósito, com o primário
derrubado sem aviso, promovendo sozinho, e cronometrado**. Este documento traz o
ensaio, os números, e — mais importante — o que eles **não** dizem.

Para repetir:

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

---

## O que foi montado

Três nós PostgreSQL 16 sob **Patroni 3.2**, com **etcd** como repositório de
consenso. É o desenho descrito em `alta-disponibilidade.md`, montado de verdade
em vez de descrito.

O primário é morto com **`docker kill`**, não `docker stop`. A diferença é o
ensaio inteiro: `stop` manda SIGTERM e o PostgreSQL desliga com ordem — fecha
conexões, grava o que falta, avisa o Patroni. Isso é manutenção programada. O
`kill` manda SIGKILL, o processo morre no meio da frase, e o agrupamento só
descobre pela ausência. É o que acontece quando um datacenter cai.

**O critério do cronômetro é escrita aceita**, não eleição anunciada. Eleger é
rápido; o que interessa ao caixa é poder lançar a venda de novo.

---

## Os números

| Cenário | Tempo até aceitar escrita | Perda |
|---|---:|---|
| Assíncrono, com 2 s de folga para replicar | **32 s** | nenhuma |
| Assíncrono, matando logo após a escrita | **30 s** | nenhuma |
| **Síncrono**, matando logo após a escrita | **36 s** | nenhuma |

**Meta da F1.2: 300 segundos.** A margem é de cerca de oito vezes.

E uma coisa que não estava no roteiro e vale mais que o cronômetro: **o nó
derrubado voltou sozinho como réplica** ao ser religado. Sem `pg_rewind` manual,
sem reconstruir base, sem ninguém digitar nada.

### O que os tempos dependem

Não é mágica, é configuração:

```
PATRONI_TTL: 30          # quanto tempo a liderança vale sem renovação
PATRONI_LOOP_WAIT: 10    # de quanto em quanto o nó verifica
PATRONI_RETRY_TIMEOUT: 10
```

Os 30 segundos são essencialmente o `TTL`: o agrupamento espera a liderança
expirar antes de eleger. Reduzir o `TTL` reduz o tempo — e aumenta a chance de
promover por soluço de rede, que é o pior tipo de failover: o que não precisava
acontecer.

**Com meta de 5 minutos, não há por que apertar.** A folga é boa.

---

## Replicação síncrona: configurável, e barata

`synchronous_mode` foi ligado pela API do Patroni e o agrupamento reorganizou
sozinho os papéis:

```
sync_standby   streaming
replica        streaming
leader         running
```

Um nó vira **`sync_standby`** e o outro segue assíncrono. É exatamente o desenho
que a F1.2 pediu: **par síncrono para RPO 0 na falha de máquina, nó remoto
assíncrono** para não pagar latência de datacenter em toda venda.

O custo medido foi **6 segundos a mais** na promoção. Contra 300 de meta, é ruído.

---

## O que este ensaio NÃO prova

Esta seção importa mais que os números, porque um número lido como promessa é
pior que número nenhum.

**Os tempos são um piso.** Tudo roda no mesmo host: latência de rede é
praticamente zero. No parque real, com um nó em outro datacenter, o tempo será
maior. Quanto maior, só medindo lá.

**A perda zero não foi testada de verdade.** Nos três ensaios a marca sobreviveu,
inclusive no modo assíncrono sem folga — mas isso é artefato do laboratório: sem
latência, a replicação alcança antes de o `kill` chegar. **A janela de perda da
replicação assíncrona não pode ser demonstrada aqui.** É por isso que o RPO 0
depende de configuração síncrona, e não de o teste ter passado.

**Não houve partição de rede.** Derrubar um contêiner é falha limpa. O caso
difícil é a rede cair entre nós que continuam vivos — cada lado achando que o
outro morreu. É onde quórum e *fencing* existem para valer, e é o que este
laboratório não reproduz.

**O etcd tem um nó só.** De propósito, para medir promoção. Um etcd de um nó não
tem quórum nenhum e **nunca deve ir para produção assim** — em produção são três.

**O tráfego não foi trocado.** O ensaio mede o banco aceitar escrita. Falta a
aplicação descobrir o novo primário, e as 32 unidades chegarem nele. Com DNS,
soma-se o TTL do registro; e o aplicativo móvel também resolve por DNS, com
versões antigas em campo.

**Os arquivos continuam de fora.** Uploads moram no disco local do primário
(**D33**). Uma promoção bem-sucedida entregaria o sistema **sem contrato, sem
certificado, sem banner e sem foto de colaborador**. O ensaio de banco não
revela isso porque não é problema de banco.

---

## O que falta para o portão fechar de verdade

O portão pede failover no parque real, não em laboratório. O que este ensaio
entrega é o desenho **validado e cronometrado**, e a certeza de que a primeira
tentativa lá não será a primeira tentativa de todas.

Em ordem de dependência:

| | O que falta | Por quê |
|---|---|---|
| 1 | **VPN em malha** | o Orange Pi está atrás de CGNAT: não recebe conexão, logo não vota nem assume |
| 2 | **Três nós de etcd** | um só não tem quórum; sem maioria, um nó isolado pode promover indevidamente |
| 3 | **Arquivos fora do disco local** | D33 — a abstração está pronta desde a F1.5, falta ligar o driver S3 |
| 4 | **Troca de tráfego** | DNS com verificação de saúde, contando o TTL no tempo total |
| 5 | **Ensaio no parque** | com o primário real derrubado sem aviso, cronometrado |

Os itens 1 e 2 são de infraestrutura e baratos. O 3 é decisão de contratação,
não de engenharia — o código já está pronto. O 4 e o 5 fecham a frente.

---

## Situação do portão

**Ensaiado em laboratório, não no parque.**

| | |
|---|---|
| Promoção automática, sem intervenção | **provada**, três vezes |
| Cronometrada contra a meta | **30 a 36 s** contra 300 |
| Reintegração do nó caído | **automática** |
| RPO 0 configurável | **provado** — `sync_standby` designado |
| No parque real | **não** — faltam os cinco itens acima |

O laboratório fica versionado e repetível. Quando os cinco itens estiverem
prontos, o mesmo roteiro roda lá, e a comparação entre os dois números dirá
quanto a rede real custa.
