Timeout e retry logic em automações
1. Por que Timeout e Retry são Essenciais em Automações
Automações em Bash frequentemente interagem com recursos externos: APIs web, servidores remotos, bancos de dados ou sistemas de arquivos em rede. Esses componentes estão sujeitos a falhas temporárias — rede instável, serviços momentaneamente indisponíveis, comandos que demoram mais que o esperado.
Sem proteções adequadas, um script pode:
- Entrar em loop infinito aguardando resposta de um servidor morto
- Consumir CPU e memória desnecessariamente em tentativas repetidas
- Travar completamente a pipeline de automação
A abordagem defensiva assume que falhas ocorrerão e prepara mecanismos de contenção (timeout + retry limitado). Já a abordagem otimista executa sem verificações, adequada apenas para ambientes controlados com garantia de disponibilidade.
2. Implementando Timeout com timeout e alarm
Usando o comando timeout (coreutils)
O comando timeout é a forma mais direta de limitar a execução de um processo:
#!/bin/bash
# Executa com timeout de 5 segundos
timeout 5 ping -c 1 google.com
# Verifica se houve timeout (código 124)
if [ $? -eq 124 ]; then
echo "Comando excedeu o tempo limite"
fi
Alternativa com trap e SIGALRM
Em ambientes sem timeout (ex: macOS mais antigo ou shells minimalistas), podemos implementar timeout manual:
#!/bin/bash
exec_com_timeout() {
local timeout=$1
shift
(
trap 'exit 124' SIGALRM
(sleep "$timeout" && kill -ALRM $$) &
"$@"
kill $! 2>/dev/null
)
}
exec_com_timeout 5 ping -c 1 google.com
Capturando saída parcial
Para preservar a saída gerada antes do timeout:
#!/bin/bash
saida=$(timeout 3 curl -s http://exemplo.com 2>&1)
codigo=$?
if [ $codigo -eq 124 ]; then
echo "Timeout - saída parcial capturada:"
echo "$saida"
fi
3. Estratégias de Retry: Loops Simples e Backoff
Retry fixo com intervalo constante
#!/bin/bash
MAX_TENTATIVAS=3
INTERVALO=2
COMANDO="curl -s http://api.exemplo.com/status"
for tentativa in $(seq 1 $MAX_TENTATIVAS); do
if $COMANDO; then
echo "Sucesso na tentativa $tentativa"
break
fi
echo "Tentativa $tentativa falhou. Aguardando ${INTERVALO}s..."
sleep $INTERVALO
done
Backoff exponencial com teto máximo
#!/bin/bash
MAX_TENTATIVAS=5
TETO_BACKOFF=30
for tentativa in $(seq 0 $((MAX_TENTATIVAS - 1))); do
if curl -s http://api.exemplo.com; then
echo "Concluído na tentativa $((tentativa + 1))"
exit 0
fi
espera=$((2 ** tentativa))
[ $espera -gt $TETO_BACKOFF ] && espera=$TETO_BACKOFF
echo "Aguardando ${espera}s antes da próxima tentativa..."
sleep $espera
done
echo "Falha após $MAX_TENTATIVAS tentativas"
exit 1
Jitter para evitar thundering herd
#!/bin/bash
BASE=5
MAX_JITTER=5
for tentativa in $(seq 1 3); do
if curl -s http://api.exemplo.com; then
exit 0
fi
espera=$((BASE + RANDOM % MAX_JITTER))
sleep $espera
done
4. Combinando Timeout e Retry com Funções Reutilizáveis
Função genérica que encapsula toda a lógica:
#!/bin/bash
exec_with_retry() {
local comando="$1"
local max_tentativas=${2:-3}
local timeout_cmd=${3:-10}
local intervalo=${4:-2}
local tentativa=0
while [ $tentativa -lt $max_tentativas ]; do
tentativa=$((tentativa + 1))
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tentativa $tentativa de $max_tentativas"
timeout $timeout_cmd bash -c "$comando"
local codigo=$?
if [ $codigo -eq 0 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Comando executado com sucesso"
return 0
fi
if [ $codigo -eq 124 ]; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Timeout na tentativa $tentativa"
else
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Erro (código $codigo) na tentativa $tentativa"
fi
[ $tentativa -lt $max_tentativas ] && sleep $intervalo
done
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Todas as $max_tentativas tentativas falharam"
return 1
}
# Uso:
exec_with_retry "curl -s http://api.exemplo.com/health" 5 10 3
5. Tratamento de Saída e Logging em Falhas
Diferenciando timeout de erro do comando
#!/bin/bash
timeout 5 ping -c 1 servidor.local
case $? in
0) echo "Sucesso" ;;
124) echo "Timeout - servidor não respondeu em 5s" ;;
*) echo "Erro no comando ping" ;;
esac
Log estruturado com redirecionamento
#!/bin/bash
LOG_FILE="/var/log/automacao.log"
exec 2>> "$LOG_FILE"
exec_with_retry_logged() {
local comando="$1"
local saida
saida=$(timeout 10 bash -c "$comando" 2>&1)
local codigo=$?
echo "$(date '+%Y-%m-%d %H:%M:%S') | Código: $codigo | Saída: $saida" >> "$LOG_FILE"
return $codigo
}
6. Casos de Uso Reais: APIs, Downloads e Comandos Remotos
Retry em requisições HTTP com curl
O curl possui retry nativo, mas combiná-lo com nossa função oferece mais controle:
#!/bin/bash
# Usando retry nativo do curl
curl --retry 3 --retry-delay 5 --retry-max-time 30 http://api.exemplo.com
# Implementação manual com mais controle
exec_with_retry "curl --connect-timeout 5 --max-time 10 -s http://api.exemplo.com" 3 15 2
Timeout em conexões SSH
#!/bin/bash
# Evita conexão SSH pendurada
timeout 15 ssh -o ConnectTimeout=5 usuario@servidor "uptime"
if [ $? -eq 124 ]; then
echo "Conexão SSH excedeu o tempo limite"
fi
Script completo de monitoramento com ping
#!/bin/bash
monitorar_host() {
local host=$1
local max_tentativas=3
for tentativa in $(seq 1 $max_tentativas); do
if timeout 3 ping -c 1 "$host" &>/dev/null; then
echo "$(date) - $host: OK"
return 0
fi
local espera=$((2 ** tentativa))
echo "$(date) - $host: Tentativa $tentativa falhou. Aguardando ${espera}s"
sleep $espera
done
echo "$(date) - $host: FORA DO AR após $max_tentativas tentativas"
return 1
}
monitorar_host "8.8.8.8"
monitorar_host "servidor.interno"
7. Boas Práticas e Armadilhas Comuns
Evitando retry infinito
Sempre defina um limite máximo explícito:
# ERRADO - loop infinito se comando sempre falhar
while ! comando; do sleep 1; done
# CORRETO - limite de tentativas
for i in $(seq 1 5); do comando && break; done
Cuidado com subshells e variáveis
#!/bin/bash
# Variáveis em subshell não afetam o shell pai
contador=0
for i in 1 2 3; do
(contador=$((contador + 1))) # Não funciona!
done
# CORRETO - alteração direta
contador=0
for i in 1 2 3; do
contador=$((contador + 1))
done
Testando a lógica com mocks
Para testar sua lógica de retry, crie comandos mock que simulam falhas:
#!/bin/bash
# Mock de comando que falha nas primeiras tentativas
mock_comando() {
local arquivo_tentativa="/tmp/tentativa_$$"
if [ ! -f "$arquivo_tentativa" ]; then
echo 1 > "$arquivo_tentativa"
return 1 # Falha na primeira tentativa
fi
local tentativa=$(cat "$arquivo_tentativa")
if [ "$tentativa" -lt 2 ]; then
echo $((tentativa + 1)) > "$arquivo_tentativa"
return 1 # Falha na segunda tentativa
fi
rm -f "$arquivo_tentativa"
return 0 # Sucesso na terceira tentativa
}
# Teste
exec_with_retry "mock_comando" 5 5 1
echo "Resultado: $?"
Referências
- GNU Coreutils - timeout invocation — Documentação oficial do comando
timeoutcom todas as opções e exemplos de uso. - Bash Hackers Wiki - Signal handling with trap — Guia detalhado sobre tratamento de sinais e implementação de alarmes com
trap. - Advanced Bash-Scripting Guide - Loops and Control — Capítulo sobre estruturas de repetição, útil para implementar loops de retry com controle de fluxo.
- Curl man page - retry options — Documentação do parâmetro
--retrydo curl, incluindo--retry-delaye--retry-max-time. - Wikipedia - Exponential backoff — Explicação detalhada do algoritmo de backoff exponencial e suas aplicações em redes e automações.
- Bats - Bash Automated Testing System — Framework para testar scripts Bash, útil para validar lógicas de timeout e retry com mocks.
- ShellCheck - SC2046 e boas práticas — Ferramenta de análise estática para scripts Bash que ajuda a evitar armadilhas comuns em loops e subshells.