0Pricing
DevOps Bootcamp · Lezione

Script idempotenti e logica di retry con backoff

Progetti operazioni sicure da rieseguire e aggiunga un backoff esponenziale per le chiamate esterne instabili.

Script idempotenti e logica di retry con backoff è una lezione DevOps Bootcamp gratuita su CoddyKit. Questa è la lezione 4 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento DevOps Bootcamp, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso DevOps Bootcamp include 4 lezioni in totale.

Che cos’è l’idempotenza e perché è importante

Idempotenza significa che l’esecuzione ripetuta della stessa operazione produce lo stesso risultato di una singola esecuzione. Negli script Bash è fondamentale perché gli script possono arrestarsi, le reti possono interrompersi e gli utenti possono ripetere accidentalmente un’operazione.

  • Uno script non idempotente che crea due volte un utente può fallire o duplicare i dati.
  • Uno script idempotente esegue prima un controllo: esiste già?
  • Gli script idempotenti possono essere utilizzati senza rischi nei cron job, nelle pipeline CI e nei cicli di retry.

La regola d’oro è: controlli prima di agire. Ogni operazione distruttiva o di creazione deve essere protetta da un controllo della precondizione.

Proteggere la creazione di file e directory

Il pattern di idempotenza più comune consiste nel verificare se una risorsa esiste già prima di crearla. Bash offre comandi concisi per farlo.

  • [ -d dir ] — vero se la directory esiste
  • [ -f file ] — vero se il file normale esiste
  • mkdir -p — crea la directory solo se non esiste (idempotenza integrata)

Quando disponibili, preferisca i flag integrati come -p e --no-clobber ai controlli manuali: sono atomici e sicuri rispetto alle race condition.

#!/usr/bin/env bash
set -euo pipefail

CONFIG_DIR="$HOME/.myapp"
CONFIG_FILE="$CONFIG_DIR/config.ini"

# Idempotent: mkdir -p never fails if dir already exists
mkdir -p "$CONFIG_DIR"

# Idempotent: only write config if it doesn't exist yet
if [ ! -f "$CONFIG_FILE" ]; then
  echo '[defaults]' > "$CONFIG_FILE"
  echo 'timeout=30' >> "$CONFIG_FILE"
  echo "Created $CONFIG_FILE"
else
  echo "Config already exists, skipping."
fi

Gestione idempotente di utenti e gruppi

Le attività di amministrazione del sistema, come l’aggiunta di utenti o gruppi, devono essere idempotenti: rieseguire lo script sulla stessa macchina non deve generare errori né creare duplicati.

  • id username restituisce 0 se l’utente esiste
  • getent group groupname verifica l’esistenza di un gruppo
  • Racchiuda ogni operazione in un controllo, così lo script può essere rieseguito senza rischi

Questo pattern è alla base di strumenti di gestione della configurazione come Ansible: ogni attività è un’operazione protetta e idempotente.

#!/usr/bin/env bash
set -euo pipefail

APP_USER="apprunner"
APP_GROUP="appgroup"

# Idempotent group creation
if ! getent group "$APP_GROUP" &>/dev/null; then
  groupadd "$APP_GROUP"
  echo "Group '$APP_GROUP' created."
else
  echo "Group '$APP_GROUP' already exists."
fi

# Idempotent user creation
if ! id "$APP_USER" &>/dev/null; then
  useradd -m -g "$APP_GROUP" -s /bin/bash "$APP_USER"
  echo "User '$APP_USER' created."
else
  echo "User '$APP_USER' already exists."
fi

Utilizzare file di lock per impedire le esecuzioni simultanee

Anche uno script idempotente può causare problemi se due istanze vengono eseguite contemporaneamente. Un file di lock garantisce che venga eseguita una sola istanza alla volta.

  • Crei un file di lock all’avvio e lo rimuova alla terminazione.
  • Utilizzi un trap per pulire il lock anche se lo script viene interrotto.
  • mkdir su un singolo percorso è atomico nella maggior parte dei filesystem Linux: è più sicuro di touch per il locking.

Senza un lock, un cron job lento e una riesecuzione manuale possono entrare in conflitto e corrompere lo stato condiviso.

#!/usr/bin/env bash
set -euo pipefail

LOCKFILE="/tmp/myapp_deploy.lock"

# Atomic lock acquisition using mkdir
if ! mkdir "$LOCKFILE" 2>/dev/null; then
  echo "ERROR: Another instance is running (lock: $LOCKFILE). Exiting." >&2
  exit 1
fi

# Guarantee lock removal on any exit
trap 'rmdir "$LOCKFILE"; echo "Lock released."' EXIT

echo "Lock acquired. Running deployment..."
sleep 2   # simulate work
echo "Deployment complete."

Tracciare i passaggi completati con un file di stato

Per gli script composti da più passaggi, come migrazioni, installazioni e provisioning, è possibile tenere traccia dei passaggi già completati utilizzando un file di stato. Ogni passaggio controlla il file di stato prima dell’esecuzione e vi scrive al termine.

  • È economico e portabile: non richiede un database.
  • Consente a uno script fallito di riprendere dal punto in cui si era interrotto.
  • Memorizzi lo stato in un percorso prevedibile, come /var/lib/myapp/ o ~/.myapp/state/.

Questo pattern è utilizzato da strumenti importanti come apt, cloud-init e i framework per le migrazioni dei database.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/myapp_state"
mkdir -p "$STATE_DIR"

run_step() {
  local step_name="$1"
  local step_cmd="$2"
  local marker="$STATE_DIR/${step_name}.done"

  if [ -f "$marker" ]; then
    echo "[SKIP] $step_name already completed."
    return 0
  fi

  echo "[RUN]  $step_name ..."
  eval "$step_cmd"
  touch "$marker"
  echo "[DONE] $step_name"
}

run_step "install_deps"   "echo 'Installing dependencies...'"
run_step "configure_db"   "echo 'Configuring database...'"
run_step "start_service"  "echo 'Starting service...'"

Introduzione alla logica di retry

Le chiamate esterne, come richieste HTTP, ricerche DNS e chiamate alle API cloud, sono intrinsecamente inaffidabili. Un singolo errore non dovrebbe interrompere l’intero script. La logica di retry riprova automaticamente un comando fallito.

  • Retry ingenuo: ripetere il comando per N volte finché non riesce.
  • Imposti sempre un numero massimo di tentativi per evitare cicli infiniti.
  • Registri ogni tentativo, così gli errori possono essere diagnosticati.

La funzione di retry più semplice avvolge un comando qualsiasi e lo riprova fino a un numero fisso di volte, con un ritardo costante. È sufficiente per molti casi d’uso, ma presenta un difetto critico sotto carico, che verrà illustrato nel prossimo argomento.

#!/usr/bin/env bash
set -euo pipefail

# Simple fixed-delay retry (3 attempts, 2s apart)
retry() {
  local max_attempts=3
  local delay=2
  local attempt=1

  until "$@"; do
    if (( attempt >= max_attempts )); then
      echo "ERROR: Command failed after $max_attempts attempts: $*" >&2
      return 1
    fi
    echo "Attempt $attempt failed. Retrying in ${delay}s..." >&2
    sleep "$delay"
    (( attempt++ ))
  done
}

# Example: retry a curl call
retry curl --silent --fail --max-time 5 https://httpbin.org/get -o /dev/null
echo "Request succeeded."

Il problema del thundering herd

Quando molti client effettuano contemporaneamente un retry dopo un errore, creano un thundering herd: eseguono tutti il retry allo stesso intervallo fisso, sovraccaricando il server esattamente nello stesso momento e rendendo impossibile il recupero.

  • 100 script eseguono il retry ogni 5 secondi → 100 richieste simultanee ogni 5 secondi.
  • Il server è già in difficoltà; il carico sincronizzato peggiora la situazione.
  • La soluzione è il backoff esponenziale: raddoppiare il tempo di attesa dopo ogni errore.
  • Aggiunga il jitter (una variazione casuale) per desincronizzare i retry tra i client.

Il backoff esponenziale con jitter è lo standard del settore, utilizzato dagli SDK AWS, dai client Google Cloud e dai principali sistemi distribuiti.

Implementazione del backoff esponenziale

Il backoff esponenziale aumenta esponenzialmente il tempo di attesa dopo ogni errore: 1 s, 2 s, 4 s, 8 s, 16 s... In questo modo il sistema remoto ha il tempo di riprendersi, riducendo al contempo il carico complessivo.

La formula: delay = base * (2 ^ attempt)

  • Impostate un limite massimo (ritardo massimo), in modo che i tempi di attesa non crescano senza limiti.
  • Parametri da regolare: base_delay, max_delay, max_attempts.

Questa funzione è riutilizzabile: passatele qualsiasi comando come argomento.

#!/usr/bin/env bash
set -euo pipefail

retry_with_backoff() {
  local max_attempts="${RETRY_MAX_ATTEMPTS:-5}"
  local base_delay="${RETRY_BASE_DELAY:-1}"
  local max_delay="${RETRY_MAX_DELAY:-30}"
  local attempt=0
  local delay="$base_delay"

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: '$*' failed after $max_attempts attempts." >&2
      return 1
    fi
    echo "Attempt $attempt failed. Backing off for ${delay}s..." >&2
    sleep "$delay"
    # Double the delay, but cap it
    delay=$(( delay * 2 ))
    (( delay > max_delay )) && delay=$max_delay
  done

  echo "Command succeeded on attempt $(( attempt + 1 ))."
}

retry_with_backoff echo "Simulated success"

Aggiunta del jitter al backoff

Il jitter aggiunge casualità al ritardo del backoff. Anche con il backoff esponenziale, se tutti i client iniziano nello stesso momento, continueranno a riprovare sincronizzati. Il jitter interrompe questa sincronizzazione.

Due strategie comuni per il jitter:

  • Jitter completo: sleep random(0, cap) — distribuzione massima, picco di carico minimo.
  • Jitter uniforme: sleep cap/2 + random(0, cap/2) — garantisce un'attesa minima ed evita di sovraccaricare immediatamente il sistema.

In Bash, usate $RANDOM (0–32767) per generare numeri casuali. Ridimensionateli in base all'intervallo del ritardo usando l'aritmetica modulare.

#!/usr/bin/env bash
set -euo pipefail

retry_with_jitter() {
  local max_attempts="${1:-5}"; shift
  local base_delay=1
  local max_delay=32
  local attempt=0
  local cap=$base_delay

  until "$@"; do
    (( attempt++ ))
    if (( attempt >= max_attempts )); then
      echo "ERROR: Giving up after $max_attempts attempts." >&2
      return 1
    fi

    # Full jitter: random value in [0, cap]
    local jitter=$(( RANDOM % (cap + 1) ))
    echo "Attempt $attempt failed. Sleeping ${jitter}s (cap=${cap}s)..." >&2
    sleep "$jitter"

    # Grow cap exponentially, bounded by max_delay
    cap=$(( cap * 2 ))
    (( cap > max_delay )) && cap=$max_delay
  done
}

retry_with_jitter 4 curl --silent --fail --max-time 3 https://httpbin.org/get -o /dev/null
echo "Done."

Combinazione di idempotenza e retry in un flusso di lavoro reale

Negli script di produzione, l'idempotenza e la logica di retry lavorano insieme. Un tipico flusso di distribuzione potrebbe:

  1. Acquisire un lock (per impedire esecuzioni simultanee)
  2. Controllare i file di stato (per saltare i passaggi completati)
  3. Usare il retry con backoff per le chiamate esterne (download, API, DNS)
  4. Contrassegnare i passaggi come completati solo dopo averne confermato il successo
  5. Rilasciare il lock tramite trap

Questa combinazione rende gli script sicuri da eseguire nuovamente in qualsiasi momento, dopo un arresto anomalo, un timeout o un'interruzione manuale, senza lasciare il sistema in uno stato non funzionante.

#!/usr/bin/env bash
set -euo pipefail

STATE_DIR="/tmp/deploy_state" && mkdir -p "$STATE_DIR"
LOCK="/tmp/deploy.lock"

mkdir "$LOCK" 2>/dev/null || { echo "Already running."; exit 1; }
trap 'rmdir "$LOCK"' EXIT

step_done() { [ -f "$STATE_DIR/$1.done" ]; }
mark_done() { touch "$STATE_DIR/$1.done"; }

retry_backoff() {
  local attempt=0 delay=1
  until "${@:2}"; do
    (( ++attempt >= $1 )) && { echo "Failed after $1 attempts."; return 1; }
    echo "Retry $attempt in ${delay}s..."; sleep $delay; delay=$(( delay * 2 ))
  done
}

if ! step_done "download_artifact"; then
  retry_backoff 4 curl -fsSL https://httpbin.org/get -o /tmp/artifact.json
  mark_done "download_artifact"
  echo "[DONE] download_artifact"
else
  echo "[SKIP] download_artifact"
fi

echo "Deployment finished successfully."

Gestione degli errori non ripetibili

Non tutti gli errori devono essere sottoposti a retry. Riprovare in caso di 404 Not Found o 403 Forbidden è inutile: senza l'intervento umano, queste richieste non avranno mai successo. La logica di retry deve distinguere tra:

  • Errori transitori — timeout di rete, 503 Service Unavailable, errore DNS → riprovare
  • Errori permanenti — 401 Unauthorized, 404 Not Found, input non valido → fallire immediatamente

Con curl, controllate il codice di stato HTTP e ignorate i retry per le risposte 4xx. Usate --write-out '%{http_code}' per acquisire lo stato separatamente dal corpo della risposta.

#!/usr/bin/env bash
set -euo pipefail

fetch_with_retry() {
  local url="$1"
  local max_attempts=4
  local delay=1
  local attempt=0
  local http_code

  until http_code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "$url"); do
    : # curl itself failed (network error)
    (( ++attempt >= max_attempts )) && { echo "Network error, giving up."; return 1; }
    sleep $delay; delay=$(( delay * 2 ))
  done

  # Permanent client errors — do not retry
  if [[ "$http_code" =~ ^4 ]]; then
    echo "ERROR: HTTP $http_code for $url — not retrying." >&2
    return 1
  fi

  # Transient server errors — retry
  if [[ "$http_code" =~ ^5 ]]; then
    (( ++attempt >= max_attempts )) && { echo "Server error, giving up."; return 1; }
    echo "HTTP $http_code — backing off ${delay}s..."
    sleep $delay; delay=$(( delay * 2 ))
    fetch_with_retry "$url"
    return
  fi

  echo "HTTP $http_code — success."
}

fetch_with_retry "https://httpbin.org/status/200"

Verifica delle conoscenze: scelta della strategia di backoff

Uno script di distribuzione scarica un artefatto di release da un bucket S3. Durante un incidente recente, 80 istanze della pipeline hanno avuto un errore simultaneamente a causa di una breve interruzione di S3. Quando S3 si è ripristinato dopo 10 secondi, tutte le 80 istanze hanno riprovato nello stesso momento, causando un altro sovraccarico e prolungando l'interruzione di 3 minuti.

Quale strategia di retry sarebbe più efficace per evitare questa cascata da effetto gregge in incidenti futuri?

Riepilogo: idempotenza e retry con backoff

In questa lezione avete imparato a progettare script Bash sicuri da eseguire nuovamente e resilienti agli errori transitori.

Pattern di idempotenza:

  • Proteggete ogni operazione con un controllo di esistenza ([ -f ], [ -d ], id, getent).
  • Usate mkdir -p e gli altri flag idempotenti integrati, quando disponibili.
  • Usate file di lock (mkdir atomico) per impedire esecuzioni simultanee.
  • Usate file di stato (file indicatore per ogni passaggio) per consentire la ripresa dopo un errore.

Pattern di retry con backoff:

  • Impostate sempre un numero massimo di tentativi: non riprovate mai all'infinito.
  • Usate il backoff esponenziale: raddoppiate il ritardo dopo ogni errore.
  • Aggiungete il jitter (casualità) per impedire la sincronizzazione da effetto gregge.
  • Distinguete gli errori transitori (riprovare) da quelli permanenti (fallire rapidamente).

La combinazione di questi pattern produce script pronti per la produzione: sicuri, osservabili e capaci di autoripararsi.

Domande Frequenti

La lezione «Script idempotenti e logica di retry con backoff» è gratuita?

Sì — il testo completo di «Script idempotenti e logica di retry con backoff» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso DevOps Bootcamp, passa a CoddyKit PRO. Il corso DevOps Bootcamp include 4 lezioni in totale.

Cosa imparerò in «Script idempotenti e logica di retry con backoff»?

Progetti operazioni sicure da rieseguire e aggiunga un backoff esponenziale per le chiamate esterne instabili. Eserciti DevOps Bootcamp con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare DevOps Bootcamp?

Non è richiesta alcuna esperienza precedente. DevOps Bootcamp su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 4 di 4.

Quanto tempo richiede la lezione «Script idempotenti e logica di retry con backoff»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione DevOps Bootcamp?

Sì. Ogni lezione DevOps Bootcamp include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Modalità rigorosa con set -euo pipefail
  2. Gestori trap per pulizia e segnali
  3. File temporanei sicuri e directory di lock
  4. Script idempotenti e logica di retry con backoff
← Torna a DevOps Bootcamp