Aller au contenu
Sylvain Joffraud l4w
Retour

`set -e` et rsync exit 23 : la rétention ne tournait pas

Un set -e combiné à rsync exit 23 a empêché la rétention de mon backup de tourner pendant des semaines : le 1er juillet 2026, je trouve 37 snapshots du dossier de données Nextcloud sur le NAS, pour une rétention configurée à 3 jours. La cause : rsync sortait régulièrement avec le code 23 parce que des fichiers Nextcloud changeaient pendant la copie, et set -euo pipefail tuait le script avant la purge. Le backup « marchait », la purge jamais.

Le NAS récupère chaque nuit les sauvegardes du serveur srv-a en mode pull, avec une clé SSH restreinte comme décrit dans l’article sur le backup pull et la clé SSH from= + no-pty. Pour les données Nextcloud (environ 100 Go), le script pull-from-srv-a.sh créait un snapshot daté par jour, dédupliqué par --link-dest : --link-dest est l’option de rsync qui crée un hardlink vers le snapshot précédent pour chaque fichier inchangé, au lieu de le recopier.

Version simplifiée de l’ancien script :

set -euo pipefail
RETENTION_DAYS_CLOUD="${RETENTION_DAYS_CLOUD:-3}"

log "[2/2] Rsync cloud-data -> $SNAP_DIR (link-dest=current)"
rsync -az --delete -e "$SSH_CMD" \
  --exclude='data/updater-*/' \
  "${LINK_DEST_OPT[@]}" \
  "$SRC:/srv/nextcloud-data/" "$SNAP_DIR/"

# Mise a jour symlink current
ln -snf "$DATE" "$CURRENT_LINK"

# ---------- Retention ----------
CUTOFF_CLOUD=$(date -d "$RETENTION_DAYS_CLOUD days ago" +%F)
for snap in "$DEST/nextcloud/data"/20[0-9][0-9]-[01][0-9]-[0-3][0-9]; do
  [ -d "$snap" ] || continue
  name=$(basename "$snap")
  if [[ "$name" < "$CUTOFF_CLOUD" ]]; then
    log "  rm $name"
    rm -rf "$snap"
  fi
done

Tout est séquentiel : copie, lien current, purge. Si la copie « échoue », le reste n’existe pas.

Pourquoi rsync sort en exit 23 sur un dossier Nextcloud ?

Le code 23 de rsync signifie, d’après sa page de manuel, Partial transfer due to error ; le code 24, Partial transfer due to vanished source files. Ce sont des transferts partiels : presque tout est copié, quelques fichiers posent problème.

Sur les données d’une instance Nextcloud en service, certains fichiers sont réécrits en permanence. Dans mon cas, ceux qui revenaient étaient les appstore/*.json, nextcloud.log et des CACHEDIR.TAG : des fichiers lus par rsync pendant qu’ils étaient modifiés. Rien de grave pour la sauvegarde, mais rsync le signale avec un code non nul.

Et set -e ne fait pas de nuance : pour bash, un code différent de 0 est un échec, et le script s’arrête sur la ligne de rsync. Le lien current n’est pas mis à jour, la boucle de rétention n’est jamais atteinte.

Avec set -e, un code de sortie « partiellement réussi » vaut un crash : tout ce qui suit la commande, purge comprise, n’est jamais exécuté.

Le correctif : un wrapper rsync_tol() qui tolère 23 et 24

Je n’ai pas retiré set -e, qui reste un bon garde-fou. J’ai encapsulé rsync dans une fonction qui accepte explicitement 0, 23 et 24, les journalise, et renvoie le vrai code pour tout le reste :

# rsync tolerant aux codes 23/24 (fichiers volatils) ; renvoie le vrai code sinon
rsync_tol() {
  local rc=0
  rsync "$@" || rc=$?
  if [ "$rc" -eq 0 ] || [ "$rc" -eq 23 ] || [ "$rc" -eq 24 ]; then
    [ "$rc" -ne 0 ] && log "  (rsync rc=$rc tolere : fichiers volatils)"
    return 0
  fi
  log "  ERREUR rsync rc=$rc"
  return "$rc"
}

Le || rc=$? est le détail qui compte : une commande suivie de || ne déclenche pas set -e, ce qui permet de capturer le code au lieu de mourir dessus. Une connexion SSH coupée ou un disque plein sortent avec un autre code et arrêtent toujours le script.

⚠️ Le code 23 couvre aussi de vraies erreurs, comme un fichier illisible pour cause de permissions. Le tolérer sans le journaliser reviendrait à cacher le problème suivant : d’où la ligne de log à chaque occurrence.

Le même jour, j’ai simplifié le modèle : les données Nextcloud passent d’un snapshot daté par jour à un miroir unique (--delete), et seuls les dumps de bases gardent 7 jours glissants. Moins de snapshots, moins de choses à purger.

Ce que je surveillais, et ce que j’aurais dû surveiller

Le script tournait chaque nuit et les données arrivaient : vu de loin, le backup était sain. Ce qui ne marchait pas, c’était l’effet attendu de la fin du script, la purge. 37 dossiers au lieu de 3, c’est visible en une commande, à condition de la lancer.

Une vérification utile compte le résultat plutôt que le code de retour : le nombre de snapshots présents, la date du plus ancien, l’espace occupé. Si la rétention est de 3 jours et qu’il y a plus de 4 dossiers, quelque chose ne tourne pas.

Lessons learned

Questions fréquentes

Que signifie le code de sortie 23 de rsync ?
Le code de sortie 23 de rsync signifie « Partial transfer due to error » : la copie s’est déroulée, mais certains fichiers n’ont pas pu être transférés correctement. Sur un dossier vivant comme les données Nextcloud, c’est typiquement un fichier modifié pendant sa lecture. Le code 24 (« Partial transfer due to vanished source files ») couvre les fichiers disparus entre le listage et la copie.
Pourquoi set -e arrête-t-il mon script de backup alors que rsync a presque tout copié ?
Avec set -e, bash quitte le script dès qu’une commande renvoie un code non nul hors d’un if, d’un || ou d’un &&. Pour bash, un rsync qui sort en 23 a échoué, même si 99 % des fichiers sont arrivés. Tout ce qui suit dans le script, purge et mise à jour du lien current comprises, est sauté.
Faut-il ignorer tous les codes d’erreur de rsync dans un backup ?
Non. Il faut tolérer explicitement les codes attendus, ici 23 et 24 pour un dossier modifié en continu, les journaliser, et laisser remonter tous les autres (connexion SSH perdue, disque plein, protocole). Un rsync … || true global masquerait une vraie panne. Le code 23 peut aussi cacher un problème de permissions : d’où l’intérêt de le loguer à chaque run.
Comment vérifier qu’une rétention de snapshots fonctionne vraiment ?
En contrôlant son effet, pas le code de sortie du script : compter les snapshots présents et alerter s’il y en a plus que la rétention prévue. Un script de backup peut échouer avant la purge tous les jours sans que personne ne le voie si la seule surveillance est « la copie a eu lieu ».

Partager cet article :

Article précédent
LLM local : préchauffer le prompt au déploiement
Article suivant
Postgres `permission denied for table` : GRANT, pas RLS