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