Dans mon article d'il y a deux jours, j'ai exposé une manière de synchroniser des dossiers au moyen de rsync et des événements inotify intercepté par incrond. Le script proposé comme solution finale... heu.. comme seconde méthode disons plutôt, contient un bug qui empêche de garantir une exécution sans processus concurrents. Comme l'objectif premier du script était justement d'empêcher plusieurs instances du script lancées simultanément de se marcher dessus, on peut raisonnablement considérer ce bug comme critique.
Lorsque je l'ai testé, c'était surtout (mais pas uniquement) depuis la ligne de commande, en lançant plusieurs processus en background à la suite. Bien que dans ces conditions le script fonctionne comme prévu (même en allant très très vite pour lancer les processus), c'est hélas bien loin des conditions "réelles", observées lorsque c'est incrond qui exécute les commandes dans la table.
Dans ce cas, les processus sont alors lancés quasiment en simultané, et le système de lock par fichier se révèle alors totalement inopérant. La vérification de l'existence du fichier et sa création sont en effet 2 opérations bien distinctes, et il arrive fréquemment que deux instances (ou plus) du script concurrentes obtiennent le même résultat pour la vérification (le fichier n'existe pas) et considèrent alors qu'elles peuvent continuer. On obtient donc deux processus distincts qui se partagent un lock qui devrait être exclusif.
La solution consiste à utiliser non pas un fichier mais un dossier. L'opération de création de répertoire étant atomique sous UNIX. On reprend donc le principe original, en utilisant en plus un second élément – un fichier cette fois – pour stocker le PID du dernier processus lancé (qu'on stockait auparavant directement dans le fichier lock).
Accessoirement, le répertoire de travail était positionné à la racine "/" lorsqu'exécuté par incrond. C'est aussi corrigé dans cette version. Des commentaires et des logs supplémentaires ont finalement été ajoutés.
Le script corrigé et intensivement testé est disponible sur PasteBin (http://pastebin.com/kcsv9vPR) ainsi que ci-après :
#!/bin/bash
# rsync-now.sh
# Configuration
METHOD=ssh
ARGS="--delete --progress"
SRC=/dossier/source/
DEST=utilisateur@hote:/dossier/cible/
OUTPUT_MAIL=root@localhost
#OUTPUT_MAIL=0
# Constants
SCRIPTNAME=$(basename "$0")
CWDIR=$(dirname "$0")
LOG=$SCRIPTNAME.log
LOCK=$SCRIPTNAME.lock
PID_FILE=$SCRIPTNAME.tmp
RSYNC_CMD="rsync -e $METHOD -av $ARGS $SRC $DEST"
## Process start ##
cd $CWDIR
#echo "($$) Process started | CWD: $PWD | UID: $UID | ARGS: $@" >> $LOG
pid=$$
# Check lock
if ! mkdir $LOCK; then
#echo "($$) Lock already exists, updating PID & exiting" >> $LOG
echo $pid > $PID_FILE
exit 1
fi;
#echo "($$) Lock does not exist, creating" >> $LOG
echo $pid > $PID_FILE
#echo "($$) Lock PID: `cat $PID_FILE`" >> $LOG
# Loop / Wait for no more concurrent processes
runCmd=0
while [ $runCmd == 0 ]; do
#echo "($$) Waiting 2s more..." >> $LOG
sleep 2
lastPid=`cat $PID_FILE`
#echo "($$) $lastPid // $pid" >> $LOG
if [ "$lastPid" == "$pid" ]; then
runCmd=1
else
pid=$lastPid
fi
done
#echo "($$) Lock PID unchanged, executing commands" >> $LOG
# Loop until command is out of job
out="`date` Starting sync...\n"
while [ $runCmd == 1 ]; do
out="$out`$RSYNC_CMD` \n"
lastPid=`cat $PID_FILE`
#echo "($$) $lastPid // $pid" >> $LOG
if [ "$lastPid" == "$pid" ]; then
runCmd=0
rmdir $LOCK
rm $PID_FILE
else
pid=$lastPid
fi
done
out="$out`date` Done.\n"
#echo "($$) Finished, flushing output:" >> $LOG
#echo -e "$out" >> $LOG
# Output
if [ $OUTPUT_MAIL != 0 ]; then
echo -e "$out" | mail -s "$SCRIPTNAME" $OUTPUT_MAIL
else
echo -e "$out"
fi;
#echo "($$) Process finished | CWD: $PWD | UID: $UID | ARGS: $@" >> $LOG