Idempotência: garantindo execução segura múltiplas vezes

1. Fundamentos da Idempotência em Scripts Shell

1.1 O que é idempotência e por que é crítica em automação

Idempotência é a propriedade de uma operação produzir o mesmo resultado independentemente de quantas vezes seja executada. Em automação com Bash/Shell Script, isso significa que executar um script uma ou cem vezes deve levar o sistema ao mesmo estado final.

Exemplo cotidiano: criar um diretório com mkdir -p é idempotente — se o diretório já existe, o comando simplesmente não faz nada. Já um script que adiciona uma linha ao final de um arquivo sem verificar se ela já existe é não-idempotente.

# Idempotente
mkdir -p /var/log/meuapp

# Não-idempotente
echo "config=valor" >> /etc/meuapp.conf

1.2 Riscos de scripts não-idempotentes

Scripts não-idempotentes causam problemas graves em produção:
- Sobrescrita de dados: usar > sem verificação apaga conteúdo existente
- Duplicação de entradas: cron jobs que adicionam linhas repetidamente em arquivos de configuração
- Estados inconsistentes: deploys que falham no meio e deixam o sistema em estado parcial

Cenários comuns incluem scripts de inicialização de containers Docker, jobs de backup e automação de infraestrutura.

1.3 Princípios de design para idempotência

Dois princípios fundamentais guiam o design idempotente:
- Verificar antes de agir (check-then-act): testar condições antes de modificar o sistema
- Estado desejado vs. estado atual: definir o resultado esperado e aplicar apenas as mudanças necessárias

# Princípio check-then-act
if [[ ! -f "/etc/meuapp.conf" ]]; then
    cat > /etc/meuapp.conf <<EOF
config=valor
EOF
fi

2. Técnicas de Verificação de Estado

2.1 Testes de existência com operadores de arquivo

O Bash oferece operadores poderosos para verificar estado antes de agir:

# Verificar se diretório existe antes de criar
[[ -d "/dados" ]] || mkdir -p "/dados"

# Verificar se arquivo é executável antes de tentar executar
if [[ -x "/usr/local/bin/meuapp" ]]; then
    /usr/local/bin/meuapp
fi

# Verificar permissão de escrita antes de modificar
if [[ -w "/etc/meuapp.conf" ]]; then
    echo "alteracao=segura" >> /etc/meuapp.conf
fi

2.2 Comandos idempotentes nativos do Linux

Diversos comandos Linux já são idempotentes por design:

# mkdir -p: não falha se diretório existe
mkdir -p /var/log/meuapp

# touch -a: atualiza timestamp apenas se arquivo existe
touch -a /var/log/meuapp/access.log

# cp -n: não sobrescreve arquivo existente
cp -n template.conf /etc/meuapp.conf

# ln -f: substitui link existente atomicamente
ln -f /usr/local/bin/meuapp /usr/bin/meuapp

# install -m: copia e define permissões, sem sobrescrever se igual
install -m 644 config.template /etc/meuapp.conf

2.3 Verificação de conteúdo e checksums

Para evitar escrita desnecessária em arquivos de configuração:

# Comparação com checksum
if ! echo "$novo_conteudo" | md5sum -c <(md5sum < /etc/meuapp.conf) 2>/dev/null; then
    echo "$novo_conteudo" > /etc/meuapp.conf
    echo "Arquivo atualizado"
else
    echo "Arquivo já está atualizado"
fi

# Comparação binária com cmp
if ! cmp -s /tmp/novo_config /etc/meuapp.conf; then
    cp /tmp/novo_config /etc/meuapp.conf
fi

3. Estruturas de Controle para Execução Condicional

3.1 Guard clauses e early exit

Padrões que evitam execução desnecessária:

# Guard clause: verifica e sai cedo
meu_servico() {
    systemctl is-active --quiet meuapp && return 0
    systemctl start meuapp
}

# Uso de trap para cleanup garantido
cleanup() {
    rm -f /tmp/meuapp.lock
}
trap cleanup EXIT

# Early exit com verificação
[[ -f "/var/run/meuapp.pid" ]] && { echo "Já em execução"; exit 0; }

3.2 Flags de estado e arquivos de lock

Arquivos semáforo previnem execução simultânea:

# Lock atômico com mkdir
if mkdir /tmp/meuapp.lock 2>/dev/null; then
    echo "Executando..."
    # ... processamento ...
    rmdir /tmp/meuapp.lock
else
    echo "Script já em execução"
    exit 1
fi

# Lock com flock
exec 200>/tmp/meuapp.lock
flock -n 200 || { echo "Já em execução"; exit 1; }

3.3 Loops com verificação incremental

Processamento baseado em timestamps para evitar trabalho repetido:

# Processar apenas arquivos modificados desde última execução
ultima_execucao=$(cat /tmp/ultimo_timestamp 2>/dev/null || echo 0)
for arquivo in /dados/entrada/*.csv; do
    if [[ $(stat -c %Y "$arquivo") -gt $ultima_execucao ]]; then
        processar_arquivo "$arquivo"
    fi
done
date +%s > /tmp/ultimo_timestamp

4. Idempotência em Manipulação de Arquivos e Diretórios

4.1 Escrita segura de arquivos de configuração

Padrão de escrita atômica para evitar arquivos corrompidos:

# Escrita segura: temporário + renomeação
escrever_config() {
    local tmpfile=$(mktemp /tmp/meuapp.XXXXXX)
    cat > "$tmpfile" <<EOF
config=valor
EOF
    mv -f "$tmpfile" /etc/meuapp.conf
}

# sed -i com backup e restauração condicional
sed -i.bak 's/old/new/' /etc/meuapp.conf
if ! grep -q "new" /etc/meuapp.conf; then
    mv /etc/meuapp.conf.bak /etc/meuapp.conf
fi

4.2 Gerenciamento de permissões e ownership

Verificar antes de alterar permissões:

# Verificar permissões atuais antes de modificar
perm_atual=$(stat -c %a /etc/meuapp.conf)
if [[ "$perm_atual" != "644" ]]; then
    chmod 644 /etc/meuapp.conf
fi

# Alterar ownership apenas se necessário
if [[ $(stat -c %U:%G /etc/meuapp.conf) != "app:app" ]]; then
    chown app:app /etc/meuapp.conf
fi

4.3 Operações em árvores de diretórios

Sincronização seletiva com rsync:

# Sincronizar apenas arquivos novos ou modificados
rsync -a --ignore-existing /origem/ /destino/

# Limpeza condicional de arquivos antigos
find /var/log/meuapp -type f -mtime +30 -delete

5. Tratamento de Processos e Serviços

5.1 Verificação de processos em execução

Padrões seguros para gerenciamento de processos:

# Iniciar apenas se não estiver rodando
if ! pgrep -x meuapp > /dev/null; then
    /usr/local/bin/meuapp &
fi

# Parar apenas se estiver rodando
if pidof meuapp > /dev/null; then
    kill $(pidof meuapp)
fi

5.2 Gerenciamento de serviços systemd

Verificações condicionais para serviços:

# Habilitar apenas se não estiver habilitado
if ! systemctl is-enabled --quiet meuapp; then
    systemctl enable meuapp
fi

# Iniciar apenas se não estiver ativo
if ! systemctl is-active --quiet meuapp; then
    systemctl start meuapp
fi

# Restart condicional baseado em mudanças
if [[ /etc/meuapp.conf -nt /var/run/meuapp.pid ]]; then
    systemctl restart meuapp
fi

5.3 Controle de jobs em background

Evitar duplicação de processos:

# Arquivo PID para controle
PID_FILE="/var/run/meuapp.pid"
if [[ -f "$PID_FILE" ]] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
    echo "Processo já em execução"
    exit 0
fi
echo $$ > "$PID_FILE"
trap "rm -f $PID_FILE" EXIT

6. Integração com Bancos de Dados e APIs

6.1 Inserções e atualizações idempotentes

# PostgreSQL: INSERT com ON CONFLICT
psql -c "INSERT INTO usuarios (id, nome) VALUES (1, 'João')
         ON CONFLICT (id) DO NOTHING;"

# MySQL: INSERT IGNORE
mysql -e "INSERT IGNORE INTO usuarios (id, nome) VALUES (1, 'João');"

6.2 Chamadas de API com verificação de estado

# Usar If-None-Match para evitar downloads repetidos
curl -H "If-None-Match: \"$(cat /tmp/etag.txt 2>/dev/null)\"" \
     -o /tmp/dados.json \
     https://api.exemplo.com/dados

# Tratar resposta 304 (Not Modified)
if [[ $? -eq 0 ]]; then
    echo "Dados atualizados"
else
    echo "Dados não modificados"
fi

6.3 Sincronização de dados com checksums

# Verificar checksum antes de transferir
hash_local=$(md5sum /dados/arquivo.bin | cut -d' ' -f1)
hash_remoto=$(curl -s https://api.exemplo.com/checksum/arquivo.bin)

if [[ "$hash_local" != "$hash_remoto" ]]; then
    curl -o /dados/arquivo.bin https://api.exemplo.com/download/arquivo.bin
fi

7. Logging e Monitoramento para Debug

7.1 Logs de decisão para auditoria

# Log estruturado de decisões
log_action() {
    local action=$1
    local reason=$2
    local resource=$3
    echo "{\"action\": \"$action\", \"reason\": \"$reason\", \"resource\": \"$resource\", \"timestamp\": \"$(date -Iseconds)\"}" >> /var/log/meuapp.log
}

# Uso
if [[ -f "/etc/meuapp.conf" ]]; then
    log_action "skip" "already_exists" "/etc/meuapp.conf"
else
    cp template.conf /etc/meuapp.conf
    log_action "create" "file_missing" "/etc/meuapp.conf"
fi

7.2 Modo dry-run para validação

# Implementação de flag --dry-run
DRY_RUN=false
[[ "$1" == "--dry-run" ]] && DRY_RUN=true

executar_ou_simular() {
    local comando="$1"
    if $DRY_RUN; then
        echo "[DRY-RUN] $comando"
    else
        eval "$comando"
    fi
}

# Uso
executar_ou_simular "cp template.conf /etc/meuapp.conf"

7.3 Testes de idempotência automatizados

# Script de teste de idempotência
testar_idempotencia() {
    local script=$1
    local estado_inicial=$(cat /etc/meuapp.conf | md5sum)

    # Executar script duas vezes
    bash "$script"
    bash "$script"

    local estado_final=$(cat /etc/meuapp.conf | md5sum)

    if [[ "$estado_inicial" == "$estado_final" ]]; then
        echo "TESTE PASSou: script é idempotente"
    else
        echo "TESTE FALHOU: script não é idempotente"
        return 1
    fi
}

8. Padrões Avançados e Antipadrões

8.1 Antipadrões comuns em scripts Shell

# ANTIPADRÃO: rm -rf sem verificação
rm -rf /dados/importantes  # Perigoso!

# CORRETO: verificar antes de remover
if [[ -d "/dados/importantes" ]] && [[ "$CONFIRMACAO" == "sim" ]]; then
    rm -rf /dados/importantes
fi

# ANTIPADRÃO: redirecionamento sem proteção
echo "dados" > /etc/arquivo_importante  # Sobrescreve sem aviso

# CORRETO: usar set -o noclobber
set -o noclobber
echo "dados" > /etc/arquivo_importante

8.2 Padrão "State File" para scripts longos

# Persistência de progresso em arquivo de estado
STATE_FILE="/tmp/meuapp.state"

salvar_progresso() {
    local etapa=$1
    echo "{\"etapa\": \"$etapa\", \"timestamp\": \"$(date -Iseconds)\"}" > "$STATE_FILE"
}

carregar_progresso() {
    if [[ -f "$STATE_FILE" ]]; then
        grep -o '"etapa": "[^"]*"' "$STATE_FILE" | cut -d'"' -f4
    fi
}

# Uso para retomada
ultima_etapa=$(carregar_progresso)
case "$ultima_etapa" in
    "download")
        processar_dados
        salvar_progresso "processamento"
        ;&
    "processamento")
        enviar_resultados
        salvar_progresso "completo"
        ;;
    *)
        baixar_dados
        salvar_progresso "download"
        processar_dados
        salvar_progresso "processamento"
        enviar_resultados
        salvar_progresso "completo"
        ;;
esac

8.3 Combinação com outras técnicas

# Idempotência + rollback: desfazer ações parciais
executar_com_rollback() {
    local backup_dir=$(mktemp -d)
    cp -r /etc/meuapp/* "$backup_dir/"

    if ! aplicar_mudancas; then
        echo "Falha! Restaurando backup..."
        cp -r "$backup_dir/"* /etc/meuapp/
        exit 1
    fi

    rm -rf "$backup_dir"
}

# Idempotência + logging estruturado
log_acao_idempotente() {
    local acao=$1
    local recurso=$2
    local resultado=$3
    logger -t meuapp "{\"acao\": \"$acao\", \"recurso\": \"$recurso\", \"resultado\": \"$resultado\"}"
}

Referências