Automatisation

flock : éviter les doublons dans vos scripts Bash Linux

Empêchez l’exécution simultanée de vos scripts Bash avec flock : verrouillage fiable, gestion des erreurs et exemples pour cron et systemd.

Par Julien Martel 7 min de lecture
flock : éviter les doublons dans vos scripts Bash Linux

Pourquoi les lancements simultanés créent des incidents

Une tâche planifiée n’est pas forcément terminée lorsque son prochain déclenchement arrive. C’est fréquent avec un script de sauvegarde, une synchronisation rsync, un export de base de données, un traitement de fichiers ou un appel d’API distante. Un ralentissement réseau, un volume de données inhabituel, une API indisponible ou une opération lancée manuellement peuvent suffire à créer un chevauchement.

Le résultat n’est pas seulement un peu de charge supplémentaire. Deux instances d’un même script peuvent produire des effets concrets et difficiles à diagnostiquer :

  • deux synchronisations modifient le même répertoire en parallèle ;
  • un fichier temporaire est écrasé ou supprimé par l’autre instance ;
  • un export est envoyé deux fois à un service externe ;
  • une tâche de purge supprime des fichiers encore manipulés ;
  • les journaux deviennent confus, car les sorties des deux processus s’entremêlent ;
  • la machine consomme inutilement du CPU, de l’I/O disque ou de la bande passante.

cron ne garantit pas qu’une exécution précédente soit terminée. Il déclenche simplement la commande à l’heure prévue. Le même principe s’applique lorsqu’un opérateur lance le script à la main pendant qu’une exécution automatique est en cours.

La commande Linux flock fournit une réponse simple à ce problème : elle place un verrou consultatif sur un fichier. Tant que ce verrou est détenu, une autre instance qui tente de prendre le même verrou peut attendre, ou échouer immédiatement selon l’option choisie.

Le mot important est consultatif. flock ne bloque pas magiquement toute écriture sur un fichier ni toute exécution d’un programme. Il coordonne les processus qui utilisent volontairement le même verrou. C’est exactement ce dont un script Bash planifié a besoin : chaque chemin d’exécution doit passer par le même point de contrôle avant de travailler.

Vérifier la présence de flock et choisir un emplacement de verrou

Sur la plupart des distributions Linux, flock est fourni par le paquet util-linux. Avant de l’ajouter à un script de production, vérifiez sa disponibilité :

command -v flock
flock --help

La commande manipule un fichier de verrou, par exemple /var/lib/report-job/report-job.lock. Ce fichier sert de support au verrou ; il ne doit pas être confondu avec une preuve qu’un processus tourne réellement.

Le choix du répertoire compte. Évitez de créer un fichier de verrou prévisible dans un répertoire accessible en écriture à tous les utilisateurs, tel que /tmp, sans précaution particulière. Pour un script exécuté par root, utilisez plutôt un répertoire applicatif possédé par root, sous /var/lib ou /run selon le besoin.

  • /run convient aux verrous liés à l’exécution courante de la machine. Son contenu est généralement perdu au redémarrage.
  • /var/lib/nom-du-script convient lorsque le script possède déjà un répertoire d’état durable et correctement protégé.
  • Le répertoire parent doit exister avant l’ouverture du fichier de verrou.

Un fichier .lock peut rester présent après la fin d’un processus, y compris après un arrêt brutal ou un redémarrage. Ce n’est pas un problème : le verrou est associé au descripteur de fichier détenu par le processus, pas à la simple existence du fichier. Lorsqu’un processus se termine, le système libère ses descripteurs et donc le verrou. Supprimer systématiquement le fichier à la fin est inutile et peut introduire des courses critiques.

Ne testez pas l’existence d’un fichier .lock avec test -f pour décider si votre script peut démarrer. Ce test et la création du fichier ne forment pas une opération atomique. Utilisez flock, qui réalise l’acquisition du verrou au bon niveau.

Utiliser flock simplement dans un script Bash

La méthode la plus robuste dans un script Bash consiste à ouvrir le fichier de verrou sur un descripteur, puis à verrouiller ce descripteur. Le verrou reste actif aussi longtemps que ce descripteur est ouvert.

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

lock_file="/var/lib/report-job/report-job.lock"

exec {lock_fd}>"$lock_file"

if ! flock -n "$lock_fd"; then
  echo "Une autre exécution de report-job est déjà en cours." >&2
  exit 0
fi

echo "Début du traitement"
# Commandes réelles du script
sleep 10
echo "Fin du traitement"

Dans cet exemple, exec {lock_fd}>"$lock_file" demande à Bash d’ouvrir le fichier et d’attribuer un descripteur disponible à la variable lock_fd. L’option -n, pour non-blocking, demande à flock d’échouer immédiatement si le verrou est déjà détenu.

La branche if est importante. Elle rend le comportement intentionnel et lisible : lorsqu’une instance est déjà active, la nouvelle exécution ne fait rien et termine avec le code 0. Pour une tâche dont une exécution manquée est acceptable, c’est souvent le meilleur choix.

Le verrou sera automatiquement libéré lorsque le script se termine, y compris en cas d’erreur si le shell quitte. Vous n’avez donc pas besoin d’ajouter une suppression du fichier dans un trap.

Cette structure s’accorde avec les bonnes pratiques de scripts fiables. Si votre script n’utilise pas encore les options de sécurité Bash, consultez le guide sur le mode strict Bash. Il faut cependant distinguer les erreurs métier d’un conflit de verrou : une seconde instance bloquée n’est pas nécessairement une panne.

Choisir entre échec immédiat et attente limitée

L’option -n est adaptée aux tâches fréquentes dont le prochain passage peut rattraper le travail. Par exemple, un script lancé toutes les quelques minutes pour relever des métriques peut abandonner une occurrence si la précédente prend plus de temps que prévu.

Dans d’autres cas, il est préférable d’attendre un peu. L’option -w fixe un délai maximal d’attente, exprimé en secondes :

if ! flock -w 30 "$lock_fd"; then
  echo "Verrou toujours indisponible après 30 secondes." >&2
  exit 1
fi

Ici, l’instance attend jusqu’à 30 secondes. Si le travail précédent se termine pendant ce délai, elle acquiert le verrou et continue. Dans le cas contraire, elle signale une erreur avec le code de sortie 1.

Le bon choix dépend du contrat de votre tâche :

  • Ignorer l’occurrence avec -n si le prochain déclenchement suffit et que deux traitements simultanés seraient dangereux.
  • Attendre un temps borné avec -w si le traitement ne doit pas être écarté trop vite.
  • Échouer explicitement si un verrou occupé révèle une situation anormale qui doit alerter l’équipe.

Évitez l’attente infinie sans raison. Si une instance est réellement bloquée — sur un montage réseau, une requête distante ou un processus enfant — les futures exécutions pourraient s’accumuler en attente. Une durée maximale rend la situation observable.

Vous pouvez également conserver un code de sortie distinct pour un conflit de verrou. Les versions de flock qui proposent l’option -E permettent de le définir :

if ! flock -n -E 75 "$lock_fd"; then
  exit 75
fi

Avant de dépendre de -E, vérifiez les options disponibles avec flock --help sur les machines ciblées. Le code 75 est couramment associé à une indisponibilité temporaire dans les conventions sysexits, mais votre supervision doit surtout interpréter ce code de façon cohérente.

Verrouiller la totalité de la zone critique

Un verrou ne protège que la partie du script exécutée après son acquisition et avant sa libération. Dans la plupart des tâches planifiées, prenez le verrou au tout début, avant de créer des fichiers temporaires, de nettoyer un répertoire ou de lancer un transfert.

Vous pouvez relâcher explicitement le verrou si une longue phase finale n’a plus besoin d’exclusion mutuelle :

flock -n "$lock_fd" || exit 0

generer_export
publier_export

flock -u "$lock_fd"

envoyer_notification

Cette approche n’est utile que si vous avez clairement identifié les opérations concurrentes. Par défaut, gardez le verrou jusqu’à la fin : le script est plus simple à relire et donc moins sujet aux erreurs.

Attention également aux processus lancés en arrière-plan. Un processus enfant peut hériter des descripteurs ouverts par le shell. S’il conserve le descripteur du verrou, celui-ci peut rester actif après la fin apparente du script parent. Lorsque vous utilisez &, les pipelines asynchrones ou des sous-processus persistants, vérifiez précisément qui garde le descripteur ouvert.

Pour des travaux de fichiers, le verrou complète les autres protections, il ne les remplace pas. Utilisez par exemple des fichiers temporaires uniques, puis un renommage atomique lorsque vous publiez un résultat. Les manipulations de redirections et de flux doivent aussi rester maîtrisées ; le sujet est détaillé dans ce guide sur stdin, stdout, stderr et les redirections Bash.

Intégrer flock directement dans une crontab

Pour une commande courte, il n’est pas obligatoire de modifier le script. Vous pouvez placer flock directement dans la crontab. Cet exemple lance un script toutes les 15 minutes, sans attendre si une instance détient déjà le verrou :

*/15 * * * * /usr/bin/flock -n /var/lib/report-job/report-job.lock /usr/local/sbin/report-job

Utilisez des chemins absolus. L’environnement de cron est volontairement minimal : ne supposez pas que PATH, le répertoire courant ou les variables d’une session interactive sont disponibles.

Pour écrire les sorties dans un journal dédié tout en évitant les doublons :

*/15 * * * * /usr/bin/flock -n /var/lib/report-job/report-job.lock /usr/local/sbin/report-job >>/var/log/report-job.log 2>&1

Dans cette forme, un conflit de verrou produit le code de sortie standard de flock. Si vous souhaitez simplement considérer le chevauchement comme une occurrence ignorée, encapsulez la logique dans le script, comme dans l’exemple précédent. C’est plus clair que de masquer indistinctement tous les échecs dans la crontab.

La crontab doit aussi être pensée comme une interface d’exploitation : redirection des logs, utilisateur d’exécution, droits sur le fichier de verrou et comportement en cas d’erreur doivent être explicites. Pour aller plus loin sur la planification, lisez notre guide pour fiabiliser les tâches cron.

Utiliser flock avec un timer systemd

Les timers systemd sont une alternative moderne à cron pour les systèmes qui utilisent systemd. Un service systemd n’est généralement pas lancé une seconde fois lorsqu’il est déjà actif par le gestionnaire lui-même. Néanmoins, flock reste pertinent si le même script peut être démarré par plusieurs voies : timer, cron historique, exécution manuelle ou autre service.

Voici un service /etc/systemd/system/report-job.service qui utilise un verrou sous /run :

[Unit]
Description=Génération du rapport

[Service]
Type=oneshot
RuntimeDirectory=report-job
ExecStart=/usr/bin/flock -n -E 75 /run/report-job/report-job.lock /usr/local/sbin/report-job
SuccessExitStatus=75

RuntimeDirectory=report-job demande à systemd de créer le répertoire /run/report-job pour la durée de vie du service. Cela évite de supposer que le répertoire existe déjà. L’instruction SuccessExitStatus=75 indique à systemd qu’une fin avec ce code est acceptable : le verrou occupé devient alors une exécution volontairement sautée, et non un échec du service.

Le timer associé, enregistré dans /etc/systemd/system/report-job.timer, peut ressembler à ceci :

[Unit]
Description=Planification du rapport

[Timer]
OnCalendar=*-*-* *:0/15:00
Persistent=true
Unit=report-job.service

[Install]
WantedBy=timers.target

Persistent=true demande à systemd de tenir compte d’un déclenchement calendrier manqué lorsque le timer était inactif, par exemple pendant un arrêt de la machine. Cette exécution de rattrapage est précisément l’un des cas où une stratégie de verrou explicite peut sécuriser une tâche également lancée ailleurs.

Après création ou modification des unités :

sudo systemctl daemon-reload
sudo systemctl enable --now report-job.timer
systemctl list-timers --all
journalctl -u report-job.service

Le suivi avec journalctl permet de vérifier les exécutions et les éventuels conflits. Retrouvez les filtres utiles dans le guide consacré à journalctl. Pour une présentation plus complète des timers, consultez aussi notre article sur le remplacement de cron par les timers systemd.

Tester le comportement avant la mise en production

Un verrou mérite un test réel. Ouvrez deux terminaux et démarrez une première instance volontairement lente :

/usr/bin/flock /var/lib/report-job/report-job.lock sleep 60

Dans le second terminal, tentez de prendre le même verrou sans attendre :

/usr/bin/flock -n /var/lib/report-job/report-job.lock echo "Deuxième instance"

La seconde commande ne doit pas afficher le texte tant que le premier processus détient le verrou. Testez ensuite votre script réel, sa journalisation et son code de sortie. Vérifiez aussi les droits du répertoire avec l’utilisateur effectivement employé par cron ou systemd.

Enfin, soyez prudent sur les systèmes de fichiers réseau : les garanties de verrouillage peuvent dépendre du type de montage et de sa configuration. Pour un script exécuté localement sur un serveur Linux, avec un fichier de verrou placé sur un système de fichiers local, flock constitue une solution directe et fiable.

Conclusion : une protection simple pour des automatisations plus fiables

flock répond à un problème courant sans démon supplémentaire ni dépendance applicative : une seule instance de votre tâche peut travailler à la fois. En choisissant un répertoire de verrou contrôlé, un comportement clair en cas de conflit et une intégration cohérente avec cron ou systemd, vous évitez de nombreux doublons silencieux.

Ajoutez ce garde-fou en priorité aux scripts qui écrivent des fichiers, lancent des sauvegardes, appellent des services externes ou manipulent des données partagées. C’est une petite modification qui rend une automatisation nettement plus prévisible en production.

Commentaires· Aucun commentaire pour l'instant

Soyez le premier à réagir.

Laisser un commentaire