Les bases du contrôle d'exclusion pour l'intégration par fichiers - bonnes pratiques de verrouillage de fichiers et de claim atomique
· Mis à jour le: · Go Komura · Intégration par fichiers, Contrôle d'exclusion, Conception, Développement Windows
Le contrôle d’exclusion de l’intégration par fichiers pose presque toujours problème dès qu’on utilise un dossier partagé, un traitement batch de nuit, ou une intégration entre processus distincts. Les recherches les plus fréquentes portent sur des points précis : un verrouillage de fichiers suffit-il à lui seul, comment empêcher plusieurs workers de récupérer le même fichier, comment éviter de lire un fichier encore en cours d’écriture.
Dans cet article, nous examinons le contrôle d’exclusion de l’intégration par fichiers en nous appuyant sur le verrouillage de fichiers, le claim atomique, temp -> rename et l’idempotence.
Table des matières
- La conclusion d’abord (en une phrase)
- Schémas de conflit qui surviennent dans l’intégration par fichiers (diagrammes)
- 2.1. Lire un fichier encore en cours d’écriture
- 2.2. Plusieurs workers récupèrent le même fichier simultanément
- 2.3. Un stale lock bloque tout le monde
- Anti-patterns
- 3.1. La vérification en deux temps
Exists -> Create - 3.2. Écrire directement dans le nom de fichier final
- 3.3. Considérer que la taille qui n’évolue plus signifie que c’est terminé
- 3.4. Faire mettre à jour un fichier partagé par tout le monde
- 3.5. Croire qu’une API de verrouillage est toute-puissante
- 3.1. La vérification en deux temps
- Bonnes pratiques
- 4.1. Publier via
temp -> close -> rename / replace - 4.2. Rendre l’intégralité explicite avec un fichier
done/ manifest - 4.3. Le récepteur prend le claim de façon atomique
- 4.4. Si l’on s’appuie sur un lock file, en faire un lease
- 4.5. Partir du principe de l’idempotence
- 4.1. Publier via
- Pseudocode (extraits)
- Comment choisir, à grands traits
- Conclusion
- Références
L’intégration par fichiers est un domaine où c’est moins le code lui-même que « l’accord de remise en main propre » qui se casse facilement. Les tests unitaires passent, mais l’incident ne se produit de temps en temps que sur le dossier partagé de production ou lors du batch de nuit - et il est de surcroît difficile à reproduire. C’est un cas assez courant.
Dans la plupart des cas, la cause n’est pas tant l’API d’E/S sur fichiers elle-même que l’ambiguïté sur ces trois points.
- À partir de quand a-t-on le droit de lire ?
- Qui détient le droit de traiter ?
- Comment récupère-t-on en cas d’échec ?
Dans cet article, nous ne réduisons pas le contrôle d’exclusion de l’intégration par fichiers aux seuls verrous du système d’exploitation, mais nous l’organisons comme un véritable protocole de remise en main propre.
Le code présenté dans cet article est publié sur GitHub sous la forme d’un ensemble complet, compilable et exécutable (une bibliothèque, une démo qui met en scène une contention de claim entre deux workers ainsi qu’une reprise de lease, et des tests unitaires qui reproduisent les conflits, la corruption et les stale locks).
file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)
1. La conclusion d’abord (en une phrase)
- Ce qui compte le plus dans l’intégration par fichiers, c’est de faire en sorte que dès que le nom de fichier final devient visible, l’état soit déjà « prêt à être lu »
- Représenter les états en cours de génération / publié / en cours de traitement / traité au moyen des noms de fichiers ou des répertoires
- S’il y a plusieurs workers, prendre le claim de façon atomique avant de lire
- Utiliser le lock file et les verrous du système d’exploitation comme un appoint, et laisser l’idempotence rattraper le reste en dernier recours
En résumé, dans l’intégration par fichiers, ce n’est pas tant le contrôle d’exclusion que la conception d’un protocole de remise en main propre qui constitue l’essentiel. Ce n’est jamais aussi simple que d’appeler une seule fonction de verrouillage.
2. Schémas de conflit qui surviennent dans l’intégration par fichiers (diagrammes)
2.1. Lire un fichier encore en cours d’écriture
Cet incident se produit dès qu’on commence à écrire directement sous le nom de fichier final. Pour un JSON, il manquera l’accolade fermante ; pour un CSV, il manquera des lignes ; pour un ZIP, il sera tout simplement corrompu.
sequenceDiagram
participant Exp as Émetteur
participant Partage as Dossier partagé
participant Rec as Récepteur
Exp->>Partage: Crée orders.csv sous son nom final
Exp->>Partage: Écrit les lignes 1 à 5000
Rec->>Partage: Détecte orders.csv
Rec->>Partage: Commence à lire immédiatement
Note over Rec: Encore incomplet
Exp->>Partage: Écrit le reste
Note over Rec: Lignes manquantes / échec d'analyse / traitement partiel
2.2. Plusieurs workers récupèrent le même fichier simultanément
Avec un flux du type « lister le répertoire, ouvrir ce qui n’est pas encore traité », deux workers peuvent s’emparer du même fichier. C’est le point de départ des doubles comptages et des envois en double.
sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant Dir as incoming
W1->>Dir: Trouve a.csv
W2->>Dir: Trouve a.csv
W1->>Dir: Commence la lecture
W2->>Dir: Commence la lecture
Note over W1,W2: La même entrée est traitée deux fois
2.3. Un stale lock bloque tout le monde
Une conception qui se contente de déposer un lock file a tendance à se gripper en cas d’arrêt anormal. Si l’on ne peut pas savoir à qui appartient le lock, s’il est encore actif, ni jusqu’à quand il reste valide, les workers suivants attendent indéfiniment.
sequenceDiagram
participant A as Worker A
participant Lock as fichier lock
participant B as Worker B
A->>Lock: Crée le lock
Note over A: Se termine ici de façon anormale
B->>Lock: Vérifie l'existence du lock
B->>Lock: Reporte le démarrage du traitement
B->>Lock: Continue d'attendre
Note over B,Lock: Impossible de déterminer s'il est stale - tout le monde s'arrête
3. Anti-patterns
3.1. La vérification en deux temps Exists -> Create
Le problème ici, c’est que « vérifier » et « acquérir » sont deux opérations distinctes. Un autre processus peut s’intercaler entre les deux, ce qui fait que ce n’est pas une exclusion réelle.
sequenceDiagram
participant A as Processus A
participant B as Processus B
participant FS as Système de fichiers
A->>FS: Vérifie l'absence de lock
B->>FS: Vérifie l'absence de lock
FS-->>A: Absent
FS-->>B: Absent
A->>FS: Crée le lock
B->>FS: Crée le lock
Note over A,B: Les deux processus continuent
L’exemple typique à éviter ressemble à ceci.
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, Environment.ProcessId.ToString());
ProcessFile();
}
Ce qu’il faut, c’est faire de « créer si absent » une seule opération.
En .NET, cela signifie la famille FileMode.CreateNew ; sous POSIX, une création atomique comme O_CREAT | O_EXCL.
3.2. Écrire directement dans le nom de fichier final
Si le récepteur interprète « dès que ce nom est visible, on peut le lire », la partie est déjà perdue au moment où l’on commence à écrire directement sous le nom final. La règle de base est de ne pas confondre le fait d’être visible avec le fait d’être lisible.
flowchart LR
A[Le nom final devient visible] --> B[Le récepteur le détecte]
B --> C[L'émetteur est encore en train d'écrire]
C --> D[Des données incomplètes sont lues]
using var writer = OpenForWrite(finalPath); // finalPath devient visible dès cet instant
foreach (var row in rows)
{
writer.WriteLine(row);
}
Cette manière de faire provoque elle-même l’incident décrit en 2.1.
3.3. Considérer que la taille qui n’évolue plus signifie que c’est terminé
Cela semble pratique, mais c’est en réalité assez dangereux. Une copie via le réseau, une pause côté émetteur, du buffering ou une nouvelle tentative peuvent tout à fait perturber ce signal.
sequenceDiagram
participant Exp as Émetteur
participant Partage as Dossier partagé
participant Rec as Récepteur
Exp->>Partage: Commence à copier data.zip
Exp->>Partage: Se met en pause en cours de route
Rec->>Partage: La taille n'a pas changé depuis 10 secondes
Note over Rec: Juge à tort que c'est terminé
Rec->>Partage: Commence la lecture
Exp->>Partage: Reprend la copie
if (currentLength == lastLength && stableSeconds >= 10)
{
return Ready;
}
Décider de l’achèvement par supposition finit par vous piéger, en particulier avec des dossiers partagés ou des fichiers volumineux. Il est plus stable de rendre l’achèvement explicite via un manifest ou un fichier done.
3.4. Faire mettre à jour un fichier partagé par tout le monde
Une conception où tout le monde lit et met à jour un même status.csv ou counter.json finit en général par donner la victoire au dernier qui a écrit.
C’est là que les choses commencent à devenir douloureuses, dès lors que l’on se met à utiliser l’intégration par fichiers comme une base de données de fortune.
sequenceDiagram
participant A as Batch A
participant B as Batch B
participant F as status.csv
A->>F: Lit v1
B->>F: Lit v1
A->>F: Écrit v2-A
B->>F: Écrit v2-B
Note over F: La mise à jour de A est perdue
On peut envisager de se replier sur un mode append-only, mais sa sémantique varie selon le système de fichiers et le mode de déploiement. S’il faut réellement une mise à jour partagée, mieux vaut ne pas forcer l’intégration par fichiers sur ce point.
3.5. Croire qu’une API de verrouillage est toute-puissante
Les API de verrouillage sont importantes, mais elles ne fonctionnent que lorsque tous les participants respectent les mêmes règles. Dans une intégration entre systèmes hétérogènes, il est plus prudent de ne pas leur faire une confiance excessive.
Remarques complémentaires :
- Sous Linux,
flockest un advisory lock : un interlocuteur qui ignore l’accord peut tout à fait écrire quand même - Sous Windows, le byte-range lock est ignoré par les fichiers mappés en mémoire
- Autrement dit, il ne faut pas faire porter à un simple verrou du système d’exploitation, à lui seul, la conception de la notification d’achèvement et de la propriété
4. Bonnes pratiques
4.1. Publier via temp -> close -> rename / replace
C’est la voie royale.
Le fichier en cours de génération reste enfermé sous un nom temp, puis, une fois le close effectué, on bascule vers le nom final.
Le récepteur ne surveille que le nom final.
flowchart LR
A[Créer un nom temp unique] --> B[Écrire tout le contenu dans temp]
B --> C[flush / close]
C --> D[rename / replace vers le nom final, dans le même répertoire]
D --> E[Le récepteur ne surveille que le nom final]
Points clés :
- Placer
tempetfinaldans le même répertoire, ou au minimum sur le même volume / système de fichiers - Sous Windows / .NET, la famille
File.Replacemérite d’être envisagée - Faire de « dès que le nom final est visible, le contenu est déjà complet » une règle établie
Si l’on place temp sur un autre disque, le rename dégénère en une simple copie, ou Replace échoue.
Ce prérequis paraît anodin, mais il est en réalité très important.
4.2. Rendre l’intégralité explicite avec un fichier done / manifest
Au-delà des données elles-mêmes, expliciter dans un fichier séparé « ce qui est effectivement terminé » stabilise le récepteur. C’est particulièrement efficace dans une intégration entre systèmes hétérogènes.
flowchart TD
A[Générer data.tmp] --> B[Publier sous data.csv]
B --> C[Créer data.done / manifest.json]
C --> D[Le récepteur détecte done / manifest]
D --> E[Vérifie le nom de fichier, la taille et le hash]
Voici les éléments qu’il vaut la peine d’inclure dans le manifest.
- Nom du fichier concerné
- Taille
- Hash
- Nombre d’enregistrements
- ID d’intégration / idempotency key
- Horodatage de génération
L’ordre compte également.
Déposer done avant de publier le corps du fichier ne relève pas d’une notification d’achèvement, mais d’un avant-goût de l’incident.
4.3. Le récepteur prend le claim de façon atomique
Si plusieurs workers surveillent le même incoming, l’approche la plus claire consiste à « déplacer le fichier vers son propre espace avant de le lire ».
Seul le worker dont le rename de incoming vers processing/<worker>/ réussit a le droit de traiter le fichier.
sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant IN as incoming
participant PR as processing
W1->>IN: Trouve a.csv
W2->>IN: Trouve a.csv
W1->>PR: Fait un rename de a.csv
W2->>PR: Fait un rename de a.csv
Note over W1,W2: Seul celui qui réussit en premier obtient la propriété
Sur le plan opérationnel, séparer aussi les répertoires facilite le suivi.
flowchart LR
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|succès| A[archive]
P -->|échec| E[error]
Le rename utilisé pour le claim doit lui aussi s’effectuer sur le même système de fichiers - c’est un prérequis.
4.4. Si l’on s’appuie sur un lock file, en faire un lease
Si l’on utilise un lock file, il ne doit pas s’agir d’un simple fichier vide, mais d’une information de propriété à durée de vie limitée. Un lock dont on ne sait pas qui l’a pris finit toujours par poser problème par la suite.
flowchart TD
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
Points clés :
- Effectuer la création de façon atomique
- Utiliser l’arrêt des mises à jour comme indice pour juger qu’il est stale
- Réserver la suppression, en principe, à celui qui l’a créé
- Prévoir une procédure de récupération en partant du principe qu’une libération peut être manquée
Un lock file n’est en définitive qu’un jeton de coopération. Chercher à garantir toute la cohérence avec cette seule pièce finit généralement mal.
4.5. Partir du principe de l’idempotence
Le contrôle d’exclusion est important, mais en exploitation réelle, on ne peut jamais ramener à zéro les cas où « la même entrée arrive parfois deux fois » ou « le traitement est relancé en cours de route ». En dernier ressort, ce qui fait la différence, c’est une conception qui ne casse pas même si l’on nourrit à nouveau le même intrant.
flowchart LR
A[Entrée + idempotency key] --> B{Déjà traité ?}
B -- Oui --> C[Traiter comme un succès sans réexécuter]
B -- Non --> D[Exécuter le traitement]
D --> E[Enregistrer dans le registre des traitements effectués]
Par exemple, on attribue un ID d’intégration à chaque fichier reçu et on l’enregistre dans un registre des traitements effectués. Si l’on fait en sorte que même une rupture ponctuelle de l’exclusion n’entraîne pas de double comptage des résultats, l’exploitation devient nettement plus confortable.
5. Pseudocode (extraits)
5.1. Le schéma d’échec typique
var lockPath = finalPath + ".lock";
if (!File.Exists(lockPath))
{
File.WriteAllText(lockPath, "");
using var writer = OpenForWrite(finalPath); // Écrit directement dans le nom final
WritePayload(writer);
File.Delete(lockPath);
}
Il y a trois problèmes.
ExistsetWriteAllTextsont deux opérations distinctesfinalPathdevient visible alors qu’il est encore en cours d’écriture- Le
lockreste présent en cas d’arrêt anormal
5.2. Un exemple dans la bonne direction (esquissé rapidement)
var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);
PublishByRenameOrReplace(tempPath, finalPath); // Suppose le même système de fichiers / volume
PublishDoneFile(finalPath + ".done", new
{
FileName = Path.GetFileName(finalPath),
Size = GetFileSize(finalPath),
Hash = ComputeHash(finalPath),
IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
return; // Un autre worker l'a déjà pris
}
var manifest = ReadDoneFile(Path.Combine(processingDir, baseName + ".done"));
VerifyPayload(Path.Combine(processingDir, baseName), manifest);
if (AlreadyProcessed(manifest.IdempotencyKey))
{
MoveBundle(processingDir, archiveDir, baseName);
return;
}
Process(Path.Combine(processingDir, baseName));
RecordProcessed(manifest.IdempotencyKey);
MoveBundle(processingDir, archiveDir, baseName);
Ici, ce qui compte, c’est davantage l’ordre que le détail de l’implémentation. Ne pas mélanger « écrire », « publier », « prendre la propriété » et « enregistrer comme traité » rend l’ensemble bien plus difficile à casser.
6. Comment choisir, à grands traits
- Avec un seul writer / un seul reader / sur un même hôte,
temp -> renameseul suffit déjà à obtenir une bonne stabilité - S’il y a plusieurs consumers, ajouter le claim rename
incoming -> processing - Pour une intégration entre systèmes hétérogènes, un NAS ou un dossier partagé, il est plus sûr d’aller jusqu’au manifest / done et à l’idempotence
- Si plusieurs writers doivent mettre à jour un même état logique, ne pas trop forcer sur l’intégration par fichiers et envisager aussi une base de données ou une file d’attente
- Les verrous du système d’exploitation sont efficaces au sein d’un même groupe d’applications partageant les mêmes hypothèses, mais ils ne remplacent pas un protocole de remise en main propre
Ce dernier point relève aussi d’une décision de repli. Il existe réellement des problèmes qu’il est pénible de résoudre uniquement par des fichiers.
7. Conclusion
Le contrôle d’exclusion de l’intégration par fichiers ne consiste pas à appeler une fonction de verrouillage, mais à définir des transitions d’état. C’est l’ossature de cet article. Représentez les états en cours de génération / publié / en cours de traitement / traité par des noms et des répertoires, et évitez la vérification en deux temps Exists -> Create, l’écriture directe dans le nom de fichier final, l’attente d’une stabilisation de la taille, la mise à jour mutuelle d’un fichier partagé, et une confiance excessive dans les API de verrouillage. En combinant ensuite temp -> close -> rename / replace, un fichier done / manifest, le claim rename, le lease et l’idempotence, on évite une bonne partie des incidents liés à l’intégration via un dossier partagé.
Dans l’intégration par fichiers, l’astuce consiste à ne jamais confondre « pouvoir être lu » et « avoir le droit d’être lu ». Il suffit de séparer ces deux notions pour réduire nettement ce type d’incident qui ne se manifeste qu’en pleine nuit.
8. Références
- Ensemble complet du code d’exemple de cet article (bibliothèque, démo, tests unitaires) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Guide pratique de FileSystemWatcher - Gérer les événements manqués et les doublons
Nous faisons le point sur l'utilisation de FileSystemWatcher et ses pièges : événements manqués, notifications en double, pièges de la dé...
Introduction à l'ADR (Architecture Decision Record) — la méthode minimale pour conserver « pourquoi on a choisi cette conception » sur un petit projet
Le code ne dit jamais pourquoi il a été écrit ainsi. Nous expliquons comment utiliser l'ADR (Architecture Decision Record) — un fichier M...
Migrer une application Windows vers le Web : les cas à éviter — tableau de décision et la solution réaliste du « fractionnement »
Les demandes de migration d'applications Windows internes vers le Web se multiplient, mais pour les applications reposant sur l'intégrati...
Pourquoi préférer l'attente sur événement à Sleep(1) sous Windows
Sous Windows, la précision des attentes courtes avec délai (timed wait) est limitée par la granularité de l'horloge système et par l'ordo...
Un tableau de décision pour choisir entre arrêt et poursuite après une exception inattendue
Lorsqu'une exception inattendue survient, faut-il arrêter l'application ou la laisser continuer ? Cet article organise la décision sous l...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Dans le développement d'applications Windows impliquant l'intégration via un dossier partagé ou des traitements batch de nuit, la conception du contrôle d'exclusion se répercute directement sur la qualité de l'implémentation.
Conseil technique et revue de conception
Si vous souhaitez d'abord clarifier la répartition des responsabilités entre verrouillage, claim atomique et idempotence, ce sujet se prête bien au conseil technique et à la revue de conception.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Une API de verrouillage suffit-elle à elle seule pour le contrôle d'exclusion de l'intégration par fichiers ?
- C'est souvent insuffisant. Sous Linux, flock est un advisory lock qu'un interlocuteur peu coopératif peut tout simplement ignorer, et sous Windows, le byte-range lock est ignoré par les fichiers mappés en mémoire. Les verrous du système d'exploitation restent efficaces au sein d'un même groupe d'applications partageant les mêmes hypothèses, mais il faut les utiliser comme un appoint : l'essentiel réside dans la conception d'un protocole de remise en main propre - temp -> rename, fichier done/manifest, claim atomique et idempotence.
- Comment éviter que l'on lise un fichier encore en cours d'écriture ?
- La voie royale consiste à publier via temp -> close -> rename/replace. On enferme le fichier en cours de génération sous un nom temp, puis, une fois le fichier fermé, on le fait basculer vers son nom final dans le même répertoire ; le récepteur ne surveille alors que le nom final. Cela suppose que temp et final se trouvent dans le même répertoire, ou au minimum sur le même volume / système de fichiers, avec pour règle que dès que le nom final est visible, le contenu est déjà complet.
- Comment empêcher que plusieurs workers traitent le même fichier en même temps ?
- Il faut prendre le claim de façon atomique avant de lire. Concrètement, seul le worker dont le rename depuis incoming vers processing/<worker>/ réussit a le droit de traiter le fichier. Une vérification en deux temps du type Exists -> Create sépare la « vérification » et l'« acquisition » en deux opérations distinctes, ce qui laisse la place à un autre processus pour s'intercaler entre les deux : ce n'est donc pas une exclusion réelle. S'il faut une création atomique, on utilise la famille FileMode.CreateNew en .NET ou O_CREAT | O_EXCL sous POSIX.
- Quelles précautions prendre lorsqu'on utilise un lock file ?
- Il ne doit pas s'agir d'un simple fichier vide, mais d'un lease (une information de propriété) à durée de vie limitée, portant ownerId, host, pid, acquiredAt, expiresAt et heartbeatAt. La création doit être atomique, l'arrêt des mises à jour doit servir d'indice pour juger qu'un lock est stale, la suppression doit en principe être réservée à celui qui l'a créé, et il faut prévoir une procédure de récupération en partant du principe qu'une libération peut être manquée. En pratique, il est plus solide de ne pas chercher à garantir toute la cohérence avec un seul lock file, et de laisser l'idempotence rattraper le reste en dernier recours.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics