Error recovery: rollback e compensação em scripts
1. Fundamentos do Tratamento de Erros em Shell Script
1.1. Códigos de retorno e configurações globais
Em Bash, todo comando retorna um código de saída entre 0 e 255. O valor 0 indica sucesso; qualquer outro valor indica erro. A variável $? captura o último código retornado:
#!/bin/bash
rm /tmp/arquivo_inexistente.txt
echo "Código de retorno: $?" # Saída: 1 (arquivo não encontrado)
Para interromper o script automaticamente ao primeiro erro, usamos set -e. Combinado com set -o pipefail, garantimos que falhas em pipelines sejam detectadas:
#!/bin/bash
set -e
set -o pipefail
# Se qualquer comando do pipeline falhar, o script aborta
cat /etc/hosts | grep "localhost" | wc -l
echo "Este comando só executa se o pipeline acima for bem-sucedido"
1.2. Armadilhas com trap
O comando trap permite capturar sinais e executar ações de limpeza antes da saída:
#!/bin/bash
cleanup() {
echo "Executando limpeza..."
rm -rf /tmp/meu_diretorio_temporario
}
trap cleanup EXIT # Executa cleanup ao final do script (sucesso ou erro)
trap cleanup ERR # Executa cleanup apenas em caso de erro
1.3. Classificação de erros
- Erro fatal: interrompe o script imediatamente (ex: falta de permissão)
- Erro recuperável: pode ser contornado (ex: arquivo já existe)
- Estado inconsistente: dados parciais ou corrompidos exigem rollback
#!/bin/bash
# Exemplo de tratamento diferenciado
if ! mkdir /dados/backup 2>/dev/null; then
if [ -d /dados/backup ]; then
echo "Erro recuperável: diretório já existe"
else
echo "Erro fatal: sem permissão para criar diretório"
exit 1
fi
fi
2. Estratégias de Rollback em Operações com Arquivos
2.1. Snapshots temporários
Antes de modificar arquivos críticos, crie cópias de segurança:
#!/bin/bash
backup_file() {
local src="$1"
local backup="${src}.bak.$(date +%Y%m%d%H%M%S)"
cp -p "$src" "$backup" || { echo "Falha ao criar backup"; exit 1; }
echo "$backup" # Retorna o caminho do backup
}
2.2. Função de rollback
#!/bin/bash
rollback_file() {
local backup="$1"
local original="${backup%.bak.*}"
if [ -f "$backup" ]; then
cp "$backup" "$original"
echo "Rollback realizado: $original restaurado de $backup"
else
echo "Backup $backup não encontrado"
return 1
fi
}
2.3. Reversão em lote
Mantenha uma lista de arquivos modificados para reversão coletiva:
#!/bin/bash
declare -a MODIFIED_FILES=()
declare -a BACKUP_FILES=()
track_change() {
local file="$1"
local backup=$(backup_file "$file")
MODIFIED_FILES+=("$file")
BACKUP_FILES+=("$backup")
}
rollback_all() {
for i in "${!BACKUP_FILES[@]}"; do
rollback_file "${BACKUP_FILES[$i]}"
done
}
3. Gerenciamento de Estado com Transações Simuladas
3.1. Flag files para etapas
#!/bin/bash
STATE_DIR="/tmp/meu_script_state"
mkdir -p "$STATE_DIR"
mark_step() {
touch "$STATE_DIR/step_$1"
}
is_step_done() {
[ -f "$STATE_DIR/step_$1" ]
}
clear_steps() {
rm -f "$STATE_DIR/step_"*
}
3.2. Funções de transação
#!/bin/bash
TRANSACTION_DIR=""
begin_transaction() {
TRANSACTION_DIR=$(mktemp -d /tmp/transacao.XXXXXX)
echo "Transação iniciada em $TRANSACTION_DIR"
}
commit_transaction() {
if [ -d "$TRANSACTION_DIR" ]; then
mv "$TRANSACTION_DIR"/* ./ 2>/dev/null || true
rm -rf "$TRANSACTION_DIR"
echo "Transação confirmada"
fi
}
rollback_transaction() {
if [ -d "$TRANSACTION_DIR" ]; then
rm -rf "$TRANSACTION_DIR"
echo "Transação revertida"
fi
}
3.3. Limpeza automática
#!/bin/bash
trap 'rollback_transaction; exit 1' ERR
trap 'commit_transaction; exit 0' EXIT
4. Compensação em Operações com Efeitos Colaterais
4.1. Compensação para criação de usuários
#!/bin/bash
create_user_with_rollback() {
local username="$1"
useradd "$username" || return 1
echo "userdel $username" >> /tmp/compensacoes.txt
}
# Em caso de erro, executar:
# while read -r line; do eval "$line"; done < /tmp/compensacoes.txt
4.2. Compensação para banco de dados
#!/bin/bash
execute_sql_with_rollback() {
local sql="$1"
local reverso="$2"
mysql -e "$sql" || return 1
echo "$reverso" >> /tmp/sql_rollback.sql
}
4.3. Log de auditoria estruturado
#!/bin/bash
log_action() {
local action="$1"
local status="$2"
local timestamp=$(date -Iseconds)
echo "{\"timestamp\": \"$timestamp\", \"action\": \"$action\", \"status\": \"$status\"}" >> /var/log/script_audit.json
}
5. Padrão de Script com Rollback Automático
5.1. Estrutura centralizada
#!/bin/bash
set -e
set -o pipefail
ROLLBACK_STACK=()
push_rollback() {
ROLLBACK_STACK+=("$1")
}
pop_rollback() {
unset 'ROLLBACK_STACK[${#ROLLBACK_STACK[@]}-1]'
}
execute_rollbacks() {
for ((i=${#ROLLBACK_STACK[@]}-1; i>=0; i--)); do
eval "${ROLLBACK_STACK[$i]}"
done
}
cleanup() {
if [ $? -ne 0 ]; then
echo "Erro detectado. Executando rollbacks..."
execute_rollbacks
fi
}
trap cleanup EXIT
5.2. Exemplo completo: deploy com reversão
#!/bin/bash
set -e
set -o pipefail
source rollback_lib.sh # Script anterior
deploy() {
local app_dir="/opt/minha_app"
local backup_dir="/tmp/deploy_backup"
begin_transaction
# Backup do diretório atual
cp -r "$app_dir" "$backup_dir"
push_rollback "rm -rf $app_dir && cp -r $backup_dir $app_dir"
# Copiar novos arquivos
rsync -a ./dist/ "$app_dir/"
push_rollback "rsync -a $backup_dir/ $app_dir/"
# Reiniciar serviço
systemctl restart minha_app
push_rollback "systemctl restart minha_app"
commit_transaction
echo "Deploy concluído com sucesso"
}
deploy
6. Tratamento de Erros em Pipelines e Subshells
6.1. Propagação em pipelines
#!/bin/bash
set -o pipefail
# Se "comando_que_pode_falhar" falhar, o pipeline inteiro falha
comando_que_pode_falhar | tee /tmp/log.txt || {
echo "Pipeline falhou. Iniciando rollback..."
execute_rollbacks
exit 1
}
6.2. Captura em subshells
#!/bin/bash
(
set -e
comando1
comando2
) || {
echo "Subshell falhou com código $?"
}
6.3. Verificação de processos filhos
#!/bin/bash
comando1 &
pid1=$!
comando2 &
pid2=$!
wait $pid1 || { echo "Processo 1 falhou"; kill $pid2 2>/dev/null; }
wait $pid2 || { echo "Processo 2 falhou"; kill $pid1 2>/dev/null; }
7. Boas Práticas e Anti-Padrões
7.1. Evitar set -e cego
#!/bin/bash
# Ruim: set -e pode abortar em situações aceitáveis
set -e
grep "padrão" /arquivo/inexistente.txt # Script aborta aqui
# Bom: tratamento explícito
grep "padrão" /arquivo/inexistente.txt || true # Ignora erro esperado
7.2. Idempotência nas funções de compensação
#!/bin/bash
# Função idempotente: pode ser executada múltiplas vezes sem efeitos colaterais
remove_user_safe() {
if id "$1" &>/dev/null; then
userdel "$1"
else
echo "Usuário $1 já removido ou não existe"
fi
}
7.3. Logging estruturado para rastreabilidade
#!/bin/bash
log_json() {
local level="$1"
local message="$2"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
echo "{\"level\": \"$level\", \"timestamp\": \"$timestamp\", \"message\": \"$message\"}"
}
# Uso
log_json "INFO" "Iniciando deploy"
log_json "ERROR" "Falha ao copiar arquivos"
log_json "ROLLBACK" "Revertendo alterações"
Referências
- Bash Reference Manual: Exit Status — Documentação oficial sobre códigos de retorno e tratamento de erros em Bash
- Advanced Bash-Scripting Guide: Error Handling — Guia avançado com exemplos práticos de tratamento de erros e traps
- Google Shell Style Guide: Error Handling — Padrões recomendados pelo Google para scripts shell robustos
- Bash Hackers Wiki: The trap command — Explicação detalhada do comando trap para captura de sinais e limpeza
- ShellCheck: Common Pitfalls — Ferramenta de análise estática que identifica problemas comuns em scripts shell
- Linux Journal: Error Recovery in Shell Scripts — Artigo técnico sobre estratégias de rollback e compensação em scripts
- Red Hat Developer: Writing Robust Bash Scripts — Guia da Red Hat para scripts bash confiáveis com tratamento de erros