#!/bin/sh
#
# Ensaio de failover: derruba o primário sem aviso e cronometra a promoção.
#
#   docker compose -p franquiabv-ha -f docker-compose.ha.yml up -d
#   sh scripts/ensaiar-failover.sh
#
# O portão da F1.7 pede exatamente isto: primário derrubado de propósito, sem
# aviso, promovendo sozinho, e cronometrado para comparar com a meta de RTO de 5
# minutos decidida na F1.2.
#
# POR QUE `docker kill` E NÃO `docker stop`. O `stop` manda SIGTERM e o
# PostgreSQL desliga com ordem: fecha conexões, grava o que falta, avisa o
# Patroni. Isso é manutenção programada, não é falha. O `kill` manda SIGKILL, o
# processo morre no meio da frase, e o resto do agrupamento só descobre pela
# ausência — que é o que acontece quando um datacenter cai.
#
# O QUE O NÚMERO SIGNIFICA. É o tempo entre a morte do primário e o momento em
# que uma ESCRITA é aceita por outro nó. Não é o tempo de eleição: eleger é
# rápido, e o que interessa ao caixa é poder lançar a venda de novo.
#
# O QUE ELE NÃO SIGNIFICA. Aqui tudo está no mesmo host, então o valor é um PISO.
# No parque real há latência entre datacenters, e o tráfego ainda precisa
# encontrar o novo primário — o que este ensaio não mede.
set -e

PROJETO="franquiabv-ha"
ARQUIVO="docker-compose.ha.yml"

# --sem-folga mata o primário IMEDIATAMENTE depois da escrita, sem dar tempo à
# replicação. É o cenário que mede o RPO em vez do RTO: com replicação
# assíncrona, a última venda pode não ter chegado à réplica.
FOLGA=2
[ "$1" = "--sem-folga" ] && FOLGA=0

executar() {
    docker compose -p "$PROJETO" -f "$ARQUIVO" exec -T "$1" "$2" "$3" "$4" "$5" "$6" 2>/dev/null
}

# O líder responde "role": "master" na própria API. Perguntar a cada nó evita
# depender de ferramenta externa.
achar_lider() {
    for no in no1 no2 no3; do
        papel=$(executar "$no" curl -s localhost:8008/patroni | sed -n 's/.*"role": "\([a-z_]*\)".*/\1/p')

        if [ "$papel" = "master" ] || [ "$papel" = "primary" ]; then
            echo "$no"
            return 0
        fi
    done

    return 1
}

sql() {
    executar "$1" psql -U postgres -tAc "$2"
}

echo "== Estado antes =="

for no in no1 no2 no3; do
    printf '   %-4s %s\n' "$no" "$(executar "$no" curl -s localhost:8008/patroni \
        | sed -n 's/.*"state": "\([a-z ]*\)".*"role": "\([a-z_]*\)".*/estado=\1 papel=\2/p')"
done

LIDER=$(achar_lider) || { echo "ABORTADO: nenhum líder. O agrupamento ainda está subindo?"; exit 1; }
echo
echo "   líder atual: $LIDER"

# --------------------------------------------------------------------------
# A marca. Serve para responder a outra metade da pergunta: além de voltar,
# voltou COM O DADO? Uma promoção rápida que perde a última venda não atende a
# meta de RPO 0 para falha de máquina.
# --------------------------------------------------------------------------
echo
echo "== Gravando a marca no primário =="
sql "$LIDER" "CREATE TABLE IF NOT EXISTS ensaio_failover (id serial primary key, gravado_em timestamptz default now());" >/dev/null
sql "$LIDER" "INSERT INTO ensaio_failover DEFAULT VALUES;" >/dev/null
ANTES=$(sql "$LIDER" "SELECT count(*) FROM ensaio_failover;" | tr -d ' \r')
echo "   linhas gravadas: $ANTES"

# Um instante para a marca chegar às réplicas. Em replicação assíncrona esta
# espera é justamente a janela de perda: com ela, mede-se o RTO; sem ela, o RPO.
if [ "$FOLGA" -gt 0 ]; then
    echo "   aguardando ${FOLGA}s para a replicação alcançar (use --sem-folga para o outro cenário)"
    sleep "$FOLGA"
else
    echo "   SEM FOLGA: matando imediatamente após a escrita"
fi

echo
echo "== Derrubando $LIDER sem aviso (SIGKILL) =="
INICIO=$(date +%s)
docker kill "${PROJETO}-${LIDER}-1" >/dev/null
echo "   morto em $(date '+%H:%M:%S')"

# --------------------------------------------------------------------------
# A espera. O critério é ESCRITA ACEITA, não eleição anunciada.
# --------------------------------------------------------------------------
echo
echo "== Aguardando alguém aceitar escrita =="
NOVO=""

while [ -z "$NOVO" ]; do
    AGORA=$(date +%s)

    if [ $((AGORA - INICIO)) -gt 300 ]; then
        echo "   ABORTADO: 5 minutos sem promoção — a meta de RTO da F1.2 estourou."
        exit 1
    fi

    for no in no1 no2 no3; do
        [ "$no" = "$LIDER" ] && continue

        if sql "$no" "INSERT INTO ensaio_failover DEFAULT VALUES;" >/dev/null 2>&1; then
            NOVO="$no"
            break
        fi
    done

    [ -z "$NOVO" ] && sleep 1
done

FIM=$(date +%s)
DEPOIS=$(sql "$NOVO" "SELECT count(*) FROM ensaio_failover;" | tr -d ' \r')

echo
echo "== Resultado =="
printf '   novo primário ....... %s\n' "$NOVO"
printf '   tempo até escrever .. %s segundos\n' "$((FIM - INICIO))"
printf '   meta da F1.2 ........ 300 segundos\n'
printf '   linhas antes ........ %s\n' "$ANTES"
printf '   linhas depois ....... %s   (esperado: %s, sendo a marca nova)\n' "$DEPOIS" "$((ANTES + 1))"

if [ "$DEPOIS" -lt "$((ANTES + 1))" ]; then
    echo
    echo "   *** PERDA DE DADO: a marca gravada antes da queda não sobreviveu."
    echo "   Isto é replicação assíncrona fazendo o que ela faz. Com RPO 0 para"
    echo "   falha de máquina, o par principal precisa ser síncrono."
fi

echo
echo "Para restaurar o nó derrubado:"
echo "  docker compose -p $PROJETO -f $ARQUIVO start $LIDER"
