Depuis que je suis arrivé chez GPDis, je me rends compte que je ressors de temps à autres ce que j'ai fait dans les autres boites. Notamment des petits bouts de code qui font ce qui est pour moi évident. Comme cela faisait trop longtemps que je n'avais pas écrit de billet techniques très fouillés, je reprends pour cette série ces petits snipets de code que je reproduis souvent pour en expliquer la stratégie, et accessoirement augmenter mon klout sur les réseaux sociaux en tant que dev, parce qu'écrire des billets sur la balise <img> ou la série des Dirty Hacky m'ont pris parfois plus de trois mois. Et si vous croyez que j'ai que ça à faire, imaginez que je ponds aussi mon émission radio hebdomadaire CPU, qui demande son quota sacrificiel en nuits blanches.

Occasion de commencer une petite série, qui est doublé d'un projet git.

Le principe (de la série)

Écrire des petites lignes de code, pour explorer des petites astuces autour d'un langage, des petits bouts de code qu'on peut étendre à loisir pour son usage, en utilisant le langage dans sa version la plus pure/simple/indépendante/vanilla possible et faisant appel aux standards normaux.

L'énoncé du problème

Supposons que tu aies un script bash sur un serveur, qui est lancé toutes les minutes via crontab (supposons !), sauf qu'il ne doit pas être lancé concurrentiellement (une et une seule fois à la fois), et que parfois, il dure plus d'une minute.
Problèmes :

  • Bash n'est pas super lisible,
  • mais il est l'intepréteur de commande le plus présent sur un serveur unix alors que zsh doit être explicitement installé,
  • il existe très peu de libs qui propose ce genre de snipets
  • et on veut un petit bout de code qu'on puisse facilement étendre à sa propre convenance (d'où la licence GPL)

TL;DR Le corrigé

Soit vous clonez le repo git, soit vous copiez-collez sans comprendre les lignes suivantes (et bonne chance) :

LOCK_DIR=~/locks
LOCK_FILE=${LOCK_DIR}/cron.lock

function lock_script() {
    mkdir -p ${LOCK_DIR}
    if [ -f "${LOCK_FILE}" ]; then 
        LOCKING_PID=`cat ${LOCK_FILE}`
        >&2  echo "ERROR : Script ${CALLED_FUNCTION} already locked on PID ${LOCKING_PID}. Cannot run PID $$ . Still running ?"
        exit 1
    fi
    echo $$ > ${LOCK_FILE}
}

function unlock_script() {
    rm ${LOCK_FILE}
}

Usage

Commencez votre script par lock_script et concluez avant de quitter par unlock_script.

Préliminaires

Pour des facilités de refactoring, je définis toujours mes variables en début de fichier. Je me suis mis dans le répertoire utilisateur, mais en cas de redistribution, il vaut mieux créer son arborescence dans /usr/var/lock si on suit le Filesystem Hierarchy Standard.

En bash, les définition de fonctions peuvent être écrites soit :

function lock_script {

ou sous cette forme :

lock_script() {

Pour aider mes petits camarades à la lecture, je mixe (sûrement très improprement) les notations de déclarations de fonctions bash, ce qui ressemble plus à la convention C/JS/PHP/etc… On notera que la parenthèse n'est pas interprétée

lock_script() ligne à ligne

Je crée inconditionnellement le sous-répertoire qui va accueillir mes fichiers locks

    mkdir -p ${LOCK_DIR}

Est-ce qu'un fichier lock est déjà présent ?

    if [ -f "${LOCK_FILE}" ]; then 

Je lis son contenu, lequel contient le PID du script l'ayant lancé. La notation `command` est plus compacte que $(command) et surtout plus lisible car déjà imbriquée.

        LOCKING_PID=`cat ${LOCK_FILE}`

J'envoie un message expliquant la situation vers STDERR, via la très cabalistique notation >&2 qui préfixe une commande dont la sortie normale est redirigée vers le canal d'erreur. La variable $$ donne le PID du script courant.

        >&2  echo "ERROR : Script ${CALLED_FUNCTION} already locked on PID ${LOCKING_PID}. Cannot run PID $$ . Still running ?"

Évolutions possibles :

  • En profiter pour vérifier si ledit script tourne toujours.
  • Internationaliser le message

Sortir avec un code d'erreur

        exit 1

Fin de la conditionnelle.

    fi

Sinon implicite : Enregistrer dans le LOCK_FILE le PID du script courant.
J'aurais pu utiliser un touch qui crée un fichier vide. Mais stocker le PID dans le lock d'un script permet, par exemple, de vérifier qui le script est toujours en cours d'exécution ou a brusquement planté. Si vous avez des outils de monitoring, cela vous laisse de quoi vous amuser.

    echo $$ > ${LOCK_FILE}

unlock_script() ligne à… ligne

On supprime le lock.

    rm ${LOCK_FILE}

Et voilà.

Alors certes…

Y'avait mieux et plus propre. Pour celà, je vous invite à râler dans ⇓ les commentaires ⇓ ou sinon à commiter sur mon github.