Ctrl K
ComunidadGuías…

dns-watch.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# Renova o DNS do container n8n a CADA troca de rede, em vez de num horário fixo.
#
# O Docker congela os servidores de DNS do host no momento em que o container sobe
# (ExtServers em /etc/resolv.conf). Este notebook troca de rede o tempo todo, e o n8n
# segue consultando o DNS da rede anterior — sintoma: getaddrinfo EAI_AGAIN em
# www.googleapis.com, dias inteiros sem uma execução bem-sucedida.
#
# O antecessor (triagem-dns-refresh.timer, horário fixo pela manhã) não cobria troca de
# rede DEPOIS do horário: mudar de rede no meio da manhã fazia o dia inteiro falhar.
#
# NÃO troque isso por um `dns:` fixo no compose: na rede da PUC, 1.1.1.1 e 8.8.8.8 dão
# ETIMEDOUT (DNS externo bloqueado) — só o 172.16.0.3/.4 local responde.
#
# ── Por que o script é mais chato do que parece necessário ──────────────────────────
# A 1ª versão (18/08) usava só o exit code do `docker exec` para decidir. Mas ele dá 1
# tanto para "o DNS não resolve" quanto para "o container ainda está subindo" — então
# um restart derrubava o probe seguinte, que pedia outro restart, e assim por diante:
# 39 restarts em dois dias, e a rodada das 09:00 de 20/08 morreu numa dessas cascatas
# (o n8n levou SIGTERM 250ms depois de "Initializing n8n process"). Daí as três
# proteções: probe que distingue os casos, cooldown, e não reiniciar durante uma rodada.

set -uo pipefail

CONTAINER=n8n
PROBE_HOST=www.googleapis.com
DB="$HOME/.n8n/database.sqlite"
# nmcli emite uma rajada de linhas por troca de rede (link, IP, DNS, connectivity).
DEBOUNCE_SECS=8
# Piso entre dois restarts. Só a troca de rede justifica reiniciar, e ela não acontece
# de minuto em minuto — se o pedido vier antes disso, é sintoma de cascata.
COOLDOWN_SECS=300
# Quanto esperar o container ficar sondável depois de subir, antes de desistir da volta.
READY_TIMEOUT_SECS=90
# Confirmações seguidas de falha antes de reiniciar: uma falha isolada costuma ser
# a rede ainda assentando depois do evento, não o resolv.conf congelado.
FAIL_CONFIRMATIONS=3
FAIL_INTERVAL_SECS=5

LAST_RESTART=0

log() { printf '%s %s\n' "$(date '+%F %T')" "$*"; }

# 0 = DNS ok | 1 = DNS quebrado | 2 = indeterminado (não deu para sondar)
probe() {
    local out
    out=$(docker exec "$CONTAINER" node \
        -e "require('dns').lookup('$PROBE_HOST',e=>console.log(e?'FAIL:'+e.code:'OK'))" 2>/dev/null)
    case "$out" in
        OK)     return 0 ;;
        FAIL:*) return 1 ;;
        *)      return 2 ;;   # container parado, subindo, ou exec recusado
    esac
}

container_running() {
    [[ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" == "true" ]]
}

# Espera o container responder ao probe (qualquer veredito que não seja "indeterminado").
wait_probeable() {
    local deadline=$((SECONDS + READY_TIMEOUT_SECS))
    while (( SECONDS < deadline )); do
        probe; [[ $? -ne 2 ]] && return 0
        sleep 3
    done
    return 1
}

# Uma rodada de triagem leva ~30 min. Reiniciar no meio joga o lote fora e ainda deixa a
# execução marcada como "crashed" com uma mensagem enganosa de falta de memória.
execution_running() {
    [[ -f "$DB" ]] || return 1
    local n
    n=$(python3 -c "
import sqlite3,sys
try:
    con=sqlite3.connect('file:$DB?mode=ro',uri=True)
    print(con.execute(\"select count(*) from execution_entity where status in ('running','new')\").fetchone()[0])
    con.close()
except Exception:
    print(0)" 2>/dev/null)
    [[ "${n:-0}" -gt 0 ]]
}

refresh_if_broken() {
    if ! container_running; then
        log "container '$CONTAINER' não está rodando — nada a fazer"
        return 0
    fi

    probe; local st=$?
    if [[ $st -eq 2 ]]; then
        log "probe indeterminado (container subindo?) — aguardando, sem restart"
        wait_probeable || { log "container não ficou sondável em ${READY_TIMEOUT_SECS}s — desisto desta rodada"; return 0; }
        probe; st=$?
    fi
    [[ $st -eq 0 ]] && { log "DNS ok — sem restart"; return 0; }

    # Confirma antes de agir: só falha persistente é resolv.conf congelado.
    local i
    for ((i = 2; i <= FAIL_CONFIRMATIONS; i++)); do
        sleep "$FAIL_INTERVAL_SECS"
        probe; st=$?
        [[ $st -eq 0 ]] && { log "DNS voltou sozinho na tentativa $i — sem restart"; return 0; }
        [[ $st -eq 2 ]] && { log "probe ficou indeterminado — sem restart"; return 0; }
    done

    local since=$(( $(date +%s) - LAST_RESTART ))
    if (( since < COOLDOWN_SECS )); then
        log "DNS quebrado, mas último restart foi há ${since}s (cooldown ${COOLDOWN_SECS}s) — adiando"
        return 0
    fi

    if execution_running; then
        log "DNS quebrado, mas há execução em andamento — adiando para não matar o lote"
        return 0
    fi

    log "DNS quebrado ($PROBE_HOST não resolve, ${FAIL_CONFIRMATIONS}x) -> docker restart $CONTAINER"
    if docker restart "$CONTAINER" >/dev/null; then
        LAST_RESTART=$(date +%s)
        wait_probeable && log "restart concluído, container sondável" || log "restart feito, mas container não respondeu ao probe"
    else
        log "docker restart falhou"
    fi
}

# Confere no start também: cobre o container ter subido no boot com o DNS de outra rede.
refresh_if_broken

nmcli monitor | while IFS= read -r _; do
    while read -r -t "$DEBOUNCE_SECS" _; do :; done   # drena a rajada
    refresh_if_broken
done