Grundlagen des wechselseitigen Ausschlusses bei der Dateiintegration - Best Practices für Dateisperren und atomare Claims

· · Dateiintegration, Sperren, Design, Windows-Entwicklung

Der wechselseitige Ausschluss bei der Dateiintegration wird bei gemeinsamen Ordnern, nächtlichen Batch-Läufen und der Kopplung separater Prozesse fast immer zum Problem. Besonders häufig gesucht werden Fragen wie: Reicht eine Dateisperre allein aus, wie verhindert man, dass mehrere Worker dieselbe Datei aufgreifen, und wie vermeidet man das Lesen einer noch unvollständig geschriebenen Datei?

In diesem Artikel betrachten wir den wechselseitigen Ausschluss bei der Dateiintegration entlang der Achsen Dateisperren, atomarer claim, temp -> rename und idempotency.

Begriffe zuerst festlegen

In diesem Bereich haben sich viele Begriffe im englischen Original eingebürgert, und wenn ihre Bedeutung vage bleibt, wird der Text schwer lesbar. Deshalb legen wir zunächst fest, was sie in diesem Artikel bedeuten.

Begriff Bedeutung in diesem Artikel
atomar (atomic) Eine Operation, deren Zwischenzustand für andere nicht sichtbar ist. Sie endet entweder vollständig erfolgreich oder gar nicht
claim Das Sichern des Verarbeitungsrechts nach dem Prinzip „diese Datei verarbeite ich“. In diesem Artikel bezeichnet es vor allem die Form, bei der nur die Seite Eigentümer wird, deren rename von incoming nach processing/<worker>/ gelingt
atomarer claim Diesen claim in einer einzigen Operation durchzuführen. Sind Prüfung und Sicherung getrennt, kann sich in der Lücke dazwischen ein anderer Prozess einschieben (3.1)
lease Eigentum mit Ablaufzeit. In der lock file wird festgehalten, „wer“ „bis wann“ Eigentümer ist, damit ein anderer Worker nach Ablauf übernehmen kann (4.4)
stale Der Zustand einer lock oder eines claim, der zurückbleibt, obwohl der Besitzer abnormal beendet wurde. Kann nicht entschieden werden, ob er noch lebt oder tot ist, bleiben alle stehen (2.3)
manifest Eine separat von der eigentlichen Datei abgelegte Beschreibungsdatei. Sie enthält Dateiname, Größe, Hash, Datensatzanzahl und Ähnliches und dient der Prüfung durch den Empfänger. Die done-Datei ist ihre minimale Variante (4.2)
idempotency (Idempotenz) Die Eigenschaft, dass sich das Ergebnis nicht ändert, auch wenn dieselbe Eingabe erneut verarbeitet wird (4.5)
advisory lock Eine Sperre, die nur wirkt, wenn sich alle Beteiligten an die Abmachung halten. Da das Betriebssystem sie nicht erzwingt, lässt sich ohne Weiteres ein Programm schreiben, das sie ignoriert und trotzdem liest und schreibt. Linux’ flock gehört zu diesem Typ
byte-range lock Eine Sperre, die nicht die gesamte Datei, sondern nur einen angegebenen Bereich betrifft. Das bekannteste Beispiel ist Windows’ LockFileEx, und hier erzwingt das Betriebssystem die Sperre tatsächlich. Es gibt jedoch eine Ausnahme (3.5)

Inhaltsverzeichnis

  1. Zuerst das Fazit (in einem Satz)
  2. Race-Muster bei der Dateiintegration (Diagramme)
    • 2.1. Eine noch unvollständig geschriebene Datei wird gelesen
    • 2.2. Mehrere Worker greifen gleichzeitig dieselbe Datei auf
    • 2.3. Ein stale lock lässt alle stehen bleiben
  3. Antipatterns
    • 3.1. Die zweistufige Prüfung Exists -> Create
    • 3.2. Direktes Schreiben unter dem endgültigen Dateinamen
    • 3.3. Abgeschlossen, sobald sich die Dateigröße nicht mehr ändert
    • 3.4. Eine gemeinsame Datei wird von allen aktualisiert
    • 3.5. Die Lock-API für allmächtig halten
  4. Best Practices
    • 4.1. Veröffentlichen über temp -> close -> rename / replace
    • 4.2. Vollständigkeit explizit über done / manifest ausdrücken
    • 4.3. Der Empfänger nimmt den claim atomar
    • 4.4. Wer sich auf eine lock file verlässt, sollte sie als lease gestalten
    • 4.5. idempotency voraussetzen
  5. Pseudocode (Auszug)
  6. Grobe Entscheidungshilfe
  7. Fazit
  8. Referenzen

Bei der Dateiintegration ist es eher die „Übergabeabsprache“ als der Code selbst, die leicht zerbricht. In Unit-Tests läuft alles durch, aber in der Produktion bricht es nur gelegentlich im gemeinsamen Ordner oder im nächtlichen Batch-Lauf – und lässt sich dann kaum reproduzieren. Das kommt recht häufig vor.

Die Ursache liegt meist nicht in der Datei-I/O-API selbst, sondern in der Unklarheit dieser drei Punkte:

  • Wann darf gelesen werden?
  • Wer hält das Verarbeitungsrecht?
  • Wie wird im Fehlerfall wiederhergestellt?

In diesem Artikel beschränken wir den wechselseitigen Ausschluss bei der Dateiintegration nicht auf das Thema OS-Sperren, sondern ordnen ihn als Übergabeprotokoll ein.

Der in diesem Artikel gezeigte Code ist als vollständiges, bau- und lauffähiges Beispielset (eine Bibliothek, eine Demo, die die claim-Konkurrenz zwischen zwei Workern und die Übernahme eines lease vorführt, sowie Unit-Tests, die Konkurrenzsituationen, Beschädigungen und stale locks nachbilden) auf GitHub veröffentlicht.

file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)

1. Zuerst das Fazit (in einem Satz)

  • Am wichtigsten bei der Dateiintegration ist, dass in dem Moment, in dem der endgültige Dateiname sichtbar wird, der Zustand „jetzt darf gelesen werden“ hergestellt ist
  • In Erzeugung / veröffentlicht / in Bearbeitung / verarbeitet unterscheiden, ausgedrückt über Dateinamen oder Verzeichnisse
  • Bei mehreren Workern vor dem Lesen den claim atomar sichern
  • lock files und OS-Sperren nur als Hilfsmittel einsetzen, am Ende idempotency als Auffangnetz nutzen

Kurz gesagt: Bei der Dateiintegration liegt der eigentliche Kern nicht so sehr im wechselseitigen Ausschluss, sondern im Design des Übergabeprotokolls. Es ist keineswegs damit getan, eine einzige Sperrfunktion aufzurufen.

2. Race-Muster bei der Dateiintegration (Diagramme)

2.1. Eine noch unvollständig geschriebene Datei wird gelesen

Wenn direkt unter dem endgültigen Dateinamen zu schreiben begonnen wird, passiert genau dieser Unfall. Bei JSON fehlt die schließende Klammer, bei CSV fehlen Zeilen, und ein ZIP ist schlicht beschädigt.

EmpfängerFreigegebener OrdnerSenderEmpfängerFreigegebener OrdnerSenderNoch nicht fertigFehlende Zeilen / Parse-Fehler / nur teilweise verarbeitetErstellt orders.csv unter dem endgültigen NamenSchreibt Zeile 1 bis 5000Erkennt orders.csvBeginnt sofort zu lesenSchreibt den Rest

2.2. Mehrere Worker greifen gleichzeitig dieselbe Datei auf

Bei einem Ablauf wie „Liste ansehen, unbearbeitete Datei öffnen“ können zwei Worker dieselbe Datei ergreifen. Das ist der Anfang von Doppelzählung und doppeltem Versand.

incomingWorker 2Worker 1incomingWorker 2Worker 1Dieselbe Eingabe wird doppelt verarbeitetFindet a.csvFindet a.csvBeginnt zu lesenBeginnt zu lesen

2.3. Ein stale lock lässt alle stehen bleiben

Ein Design, das nur eine lock file ablegt, verklemmt sich bei abnormaler Beendigung leicht. Ist nicht erkennbar, wem die lock gehört, ob sie noch lebt oder wie lange sie gültig ist, wartet alles Nachfolgende auf ewig.

Worker Block-DateiWorker AWorker Block-DateiWorker ABricht hier abKann nicht entscheiden, ob sie stale ist – alle stehen stillErstellt die lockPrüft, ob die lock existiertVerschiebt den Beginn der VerarbeitungWartet weiter

3. Antipatterns

3.1. Die zweistufige Prüfung Exists -> Create

Das Problem dabei ist, dass „Prüfen“ und „Sichern“ zwei getrennte Operationen sind. Dazwischen kann sich ein anderer Prozess einschieben, sodass kein wechselseitiger Ausschluss zustande kommt.

DateisystemProzess BProzess ADateisystemProzess BProzess ABeide fahren fortPrüft, ob keine lock existiertPrüft, ob keine lock existiertKeine vorhandenKeine vorhandenErstellt die lockErstellt die lock

Ein typisches schlechtes Beispiel sieht so aus.

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, Environment.ProcessId.ToString());
    ProcessFile();
}

Nötig ist, „Erstellen, falls nicht vorhanden“ zu einer einzigen Operation zu machen. Unter .NET ist das die Familie FileMode.CreateNew, unter POSIX-Systemen eine atomare Erstellung wie O_CREAT | O_EXCL.

3.2. Direktes Schreiben unter dem endgültigen Dateinamen

Interpretiert der Empfänger „sobald dieser Name sichtbar ist, darf gelesen werden“, ist bereits in dem Moment verloren, in dem direkt unter dem endgültigen Dateinamen zu schreiben begonnen wird. Grundregel: Sichtbarkeit und Lesbarkeit nicht gleichsetzen.

final-Name wird sichtbarEmpfänger erkennt ihnSender schreibt nochUnvollständige Daten werden gelesen
using var writer = OpenForWrite(finalPath); // Hier wird finalPath bereits sichtbar
foreach (var row in rows)
{
    writer.WriteLine(row);
}

Diese Vorgehensweise holt sich den Unfall aus 2.1 selbst ins Haus.

3.3. Abgeschlossen, sobald sich die Dateigröße nicht mehr ändert

Das wirkt praktisch, ist aber ziemlich riskant. Kopien über das Netzwerk, Pausen auf Senderseite, Pufferung und Wiederholungsversuche bringen das regelmäßig ins Wanken.

EmpfängerFreigegebener OrdnerSenderEmpfängerFreigegebener OrdnerSenderFehleinschätzung als abgeschlossenBeginnt data.zip zu kopierenPausiert mittendrinGröße ändert sich 10 Sekunden lang nichtBeginnt zu lesenSetzt die Kopie fort
if (currentLength == lastLength && stableSeconds >= 10)
{
    return Ready;
}

Wird der Abschluss geraten statt festgestellt, stolpert man bei gemeinsamen Ordnern und großen Dateien darüber. Stabiler ist es, den Abschluss über manifest oder eine done-Datei explizit zu machen.

3.4. Eine gemeinsame Datei wird von allen aktualisiert

Ein Design, bei dem alle eine einzelne status.csv oder counter.json lesen und aktualisieren, endet meist damit, dass der letzte Schreiber gewinnt. Sobald die Dateiintegration als Behelfs-Datenbank genutzt wird, wird es hier schmerzhaft.

status.csvBatch BBatch Astatus.csvBatch BBatch ADie Aktualisierung von A geht verlorenLiest v1Liest v1Schreibt v2-ASchreibt v2-B

Es gibt auch den Ausweg, append-only zu arbeiten, doch dessen Bedeutung schwankt je nach Dateisystem und Bereitstellungsform. Wird eine gemeinsame Aktualisierung wirklich benötigt, sollte man sich hier mit Dateiintegration nicht überheben.

3.5. Die Lock-API für allmächtig halten

Die Lock-API ist wichtig, wirkt aber nur, wenn alle Beteiligten sich an dieselbe Abmachung halten. Bei der Integration heterogener Systeme ist es sicherer, ihr hier nicht blind zu vertrauen.

Ergänzend:

  • Linux’ flock ist eine advisory lock, sodass sich ohne Weiteres ein Programm schreiben lässt, das die Abmachung ignoriert
  • Windows’ byte-range lock wird bei memory-mapped Dateien ignoriert
  • Mit anderen Worten: Man sollte die OS-Sperre allein nicht auch noch mit dem Design von Abschlussbenachrichtigung und Eigentümerschaft belasten

Der zweite Punkt ist als Windows-Spezifikation ausdrücklich dokumentiert. In Microsoft Learns Locking and Unlocking Byte Ranges in Files steht direkt nach der Aussage, dass der Zugriff eines anderen Prozesses auf einen bereits gesperrten Bereich immer fehlschlägt (Windows’ Bereichssperre wird also erzwungen, nicht nur als advisory behandelt), der Hinweis, dass eine byte-range lock ignoriert wird, wenn memory-mapped Dateien verwendet werden. Greift die Gegenseite über CreateFileMapping auf dieselbe Datei zu, wird die eigene Sperre also einfach umgangen.

Wer unter .NET eine Bereichssperre setzen will, verwendet FileStream.Lock / Unlock (unter Windows).

using var stream = new FileStream(
    path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

// Nur das erste Byte exklusiv sperren, als Markierung für „in Bearbeitung“
stream.Lock(0, 1);
try
{
    // Hier erfolgt das eigentliche Lesen/Schreiben
}
finally
{
    // Vor dem Schließen unbedingt wieder freigeben
    stream.Unlock(0, 1);
}

Diese Form funktioniert zwischen Anwendungen, die sich an dieselbe Abmachung halten. Wie oben beschrieben, wirkt sie jedoch nicht, wenn die Gegenseite über memory mapping zugreift, und es gibt ohnehin keine Garantie, dass ein anderes System diese Markierung überhaupt beachtet. Deshalb liegt der eigentliche Kern beim Übergabeprotokoll aus Kapitel 4.

4. Best Practices

Zunächst stellen wir die Zuordnung zu den Antipatterns aus Kapitel 3 gegenüber. Wer sich bewusst ist, eines davon zu begehen, kann direkt beim passenden Abschnitt weiterlesen.

Antipattern Was passiert Passende Gegenmaßnahme
3.1. Die zweistufige Prüfung Exists -> Create In der Lücke zwischen Prüfung und Sicherung schiebt sich ein zweiter Prozess ein, beide fahren gleichzeitig fort 4.3 Den claim atomar nehmen (rename oder FileMode.CreateNew)
3.2. Direktes Schreiben unter dem endgültigen Dateinamen Der Empfänger liest eine noch unvollständig geschriebene Datei 4.1 Veröffentlichen über temp -> close -> rename / replace
3.3. Abgeschlossen, sobald sich die Dateigröße nicht mehr ändert Eine Kopierpause wird fälschlich als Abschluss gewertet 4.2 Den Abschluss über done / manifest explizit machen
3.4. Eine gemeinsame Datei wird von allen aktualisiert Der später schreibende Teilnehmer überschreibt die Aktualisierung, sie geht verloren Mit 4.3 auf einen einzigen Schreiber begrenzen, mit 4.5 Doppelverarbeitung abfangen. Reicht das nicht, die Rückzugsentscheidung aus Kapitel 6
3.5. Die Lock-API für allmächtig halten Wird durch Gegenstellen gebrochen, die sich nicht an die Abmachung halten, oder über memory mapping umgangen 4.4 lock file als lease und 4.5 idempotency als Auffangnetz

4.1. Veröffentlichen über temp -> close -> rename / replace

Der Königsweg. Die gerade entstehende Datei wird unter einem temp-Namen eingesperrt und erst nach dem close auf den final-Namen umgestellt. Der Empfänger achtet nur auf final-Namen.

Eindeutigen temp-Namen erzeugenGesamten Inhalt nach temp schreibenflush / closeIm selben Verzeichnis auf final-Namen rename / replaceEmpfänger überwacht nur final-Namen

Wichtige Punkte:

  • temp und final im selben Verzeichnis ablegen, mindestens aber auf demselben Volume / Dateisystem
  • Unter Windows / .NET kommt die Familie File.Replace infrage
  • Vereinbaren, dass der Inhalt vollständig ist, sobald der final-Name sichtbar wird

Liegt temp auf einem anderen Laufwerk, verkommt rename zu einer bloßen Kopie, oder Replace schlägt fehl. Diese Voraussetzung wirkt unscheinbar, ist aber sehr wichtig.

Über einen gemeinsamen Ordner (SMB) hinweg geraten außerdem die folgenden vier Punkte ins Wanken. Da genau das das Haupteinsatzgebiet dieses Artikels ist, führen wir sie gesondert auf.

  • Ein rename innerhalb desselben Verzeichnisses derselben Freigabe wird serverseitig ausgeführt. Die Eigenschaft „der Zwischenname wird nicht sichtbar“ bleibt also erhalten. Wird dagegen die Freigabe gewechselt, etwa von \\server\shareA nach \\server\shareB, gilt das als anderes Volume, und Windows’ MoveFileEx ersetzt die Verschiebung bei Angabe von MOVEFILE_COPY_ALLOWED durch Kopieren und Löschen. Der Vorgang ist dann also nicht mehr atomar, und Zwischenzustände werden sichtbar. Die Voraussetzung, temp und final sowie incoming und processing innerhalb derselben Freigabe zu halten, ist hier noch wichtiger als lokal
  • Ein rename schlägt schon fehl, wenn irgendjemand die Datei nur geöffnet hat. Auf gemeinsame Ordner greifen Virenschutz, Suchindexer, Clients an anderen Standorten und andere einem nicht bekannte Akteure zu. Es ist realistisch, das Scheitern von publish- und claim-renames nicht als Ausnahme, sondern als normalen Verzweigungsfall zu behandeln und nach einer kurzen Wartezeit erneut zu versuchen
  • Zeitstempel taugen nicht als Entscheidungsgrundlage. In Microsoft Learns File Times steht, dass bei Dateizeiten nur garantiert ist, dass sie korrekt widergespiegelt werden, sobald das Handle, mit dem die Änderung vorgenommen wurde, geschlossen wird. Die letzte Änderungszeit während des Schreibens wird erst vollständig aktualisiert, wenn alle Schreib-Handles geschlossen sind. Auch die Granularität hängt vom Dateisystem ab: Bei FAT wird die letzte Änderungszeit in 2-Sekunden-Schritten geführt, bei NTFS kann sich die Aktualisierung der letzten Zugriffszeit um bis zu einer Stunde verzögern. Über SMB kommt hinzu, dass die Zeit von der Uhr des Servers stammt – weichen Client- und Serveruhr voneinander ab, verschiebt sich auch eine Regel wie „N Minuten nach der letzten Änderung verarbeiten“ entsprechend. Genau deshalb wird der Abschluss nicht über Zeit oder Größe, sondern über done / manifest aus 4.2 bestimmt
  • Auch Änderungsbenachrichtigungen gehen verloren. Es ist stabiler, sich bei der Überwachung eines gemeinsamen Ordners nicht allein auf Ereignisbenachrichtigungen zu verlassen, sondern zusätzlich regelmäßig das Verzeichnis aufzulisten. Dieses Thema wird in „Praxisleitfaden für FileSystemWatcher - Umgang mit verpassten und doppelten Ereignissen“ behandelt

4.2. Vollständigkeit explizit über done / manifest ausdrücken

Wird nicht nur die eigentliche Datendatei bereitgestellt, sondern in einer separaten Datei auch explizit angegeben, „was fertiggestellt wurde“, wird der Empfänger stabiler. Das ist besonders bei der Integration heterogener Systeme wirksam.

data.tmp erzeugenAls data.csv veröffentlichendata.done / manifest.json erstellenEmpfänger erkennt done / manifestDateiname, Größe und Hash prüfen

Im manifest sollten ungefähr diese Angaben stehen.

  • Name der Zieldatei
  • Größe
  • Hash
  • Anzahl der Datensätze
  • Integrations-ID / idempotency key
  • Erzeugungszeitpunkt

Auch die Reihenfolge ist wichtig. Wird done vor der Veröffentlichung der eigentlichen Datei platziert, ist das keine Abschlussbenachrichtigung, sondern eine Ankündigung eines Unfalls.

4.3. Der Empfänger nimmt den claim atomar

Sehen mehrere Worker in dasselbe incoming, ist „vor dem Lesen erst in den eigenen Bereich verschieben“ die klarste Lösung. Nur der Worker, dessen rename von incoming nach processing/<worker>/ gelingt, verarbeitet die Datei.

processingincomingWorker 2Worker 1processingincomingWorker 2Worker 1Nur wer zuerst erfolgreich ist, übernimmt die EigentümerschaftFindet a.csvFindet a.csvrename von a.csvrename von a.csv

In der Praxis lässt sich das durch getrennte Verzeichnisse auch besser nachverfolgen.

publishclaimErfolgFehlertempincomingprocessingarchiveerror

Auch das rename für den claim muss auf demselben Dateisystem stattfinden – das ist Voraussetzung.

4.4. Wer sich auf eine lock file verlässt, sollte sie als lease gestalten

Wird eine lock file eingesetzt, sollte sie keine bloße leere Datei sein, sondern eine Eigentumsinformation mit Ablaufzeit. Eine lock, bei der unklar ist, wer sie gesetzt hat, führt später zwangsläufig zu Streit.

lock.jsonownerIdhostpidacquiredAtexpiresAtheartbeatAt

Wichtige Punkte:

  • Die Erstellung atomar durchführen
  • Ausbleibende Aktualisierungen als Grundlage für die stale-Erkennung nutzen
  • Das Löschen im Regelfall nur dem Ersteller überlassen
  • Davon ausgehen, dass die Freigabe manchmal ausbleibt, und dafür ein Wiederherstellungsverfahren festlegen

Eine lock file ist letztlich nur ein Token für die Kooperation. Der Versuch, damit allein vollständige Konsistenz zu garantieren, wird meist eng.

4.5. idempotency voraussetzen

Wechselseitiger Ausschluss ist wichtig, aber im Realbetrieb lässt sich „kommt gelegentlich doppelt an“ oder „wird mittendrin erneut ausgeführt“ nicht auf null bringen. Am Ende zahlt sich ein Design aus, das nicht kaputtgeht, wenn dieselbe Eingabe noch einmal verarbeitet wird.

JaNeinEingabe + idempotency keyBereits verarbeitet?Als Erfolg werten, ohne erneut auszuführenVerarbeitung ausführenIm Verarbeitungsregister vermerken

Zum Beispiel erhält jede empfangene Datei eine Integrations-ID und wird im Verarbeitungsregister vermerkt. Bricht der Ausschluss einmal, sorgt diese Vorkehrung dafür, dass das Ergebnis nicht doppelt gezählt wird, was den Betrieb erheblich erleichtert.

5. Pseudocode (Auszug)

MakeTempPathSameDirectory oder TryClaimBundleByRename, die hier auftauchen, sind fiktive Funktionsnamen, die nur die Reihenfolge zeigen sollen. Die tatsächlich lauffähige Implementierung befindet sich im eingangs genannten Beispielset.

Die Implementierung dieses Pseudocodes (Bibliothek, Demo zur claim-Konkurrenz zwischen zwei Workern, Unit-Tests) - komurasoft-blog-samples (GitHub)

5.1. Das typische Fehlermuster

var lockPath = finalPath + ".lock";

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, "");
    using var writer = OpenForWrite(finalPath); // Schreibt direkt unter dem endgültigen Namen
    WritePayload(writer);

    File.Delete(lockPath);
}

Es gibt drei Probleme.

  • Exists und WriteAllText sind getrennte Operationen
  • finalPath wird bereits während des Schreibens sichtbar
  • Bei abnormaler Beendigung bleibt lock zurück

5.2. Ein Beispiel in die richtige Richtung (grob skizziert)

var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);

PublishByRenameOrReplace(tempPath, finalPath); // Setzt dasselbe Dateisystem / Volume voraus
PublishDoneFile(finalPath + ".done", new
{
    FileName = Path.GetFileName(finalPath),
    Size = GetFileSize(finalPath),
    Hash = ComputeHash(finalPath),
    IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
    return; // Ein anderer Worker hat es zuerst übernommen
}

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);

Hier zählt die Reihenfolge mehr als die Implementierungsdetails. „Schreiben“, „veröffentlichen“, „Eigentümerschaft übernehmen“ und „als verarbeitet vermerken“ nicht zu vermischen macht das Ganze weniger anfällig.

6. Grobe Entscheidungshilfe

  • Bei einem einzelnen writer / einem einzelnen reader / demselben Host ist schon temp -> rename allein recht stabil
  • Bei mehreren consumern das claim-rename incoming -> processing einführen
  • Bei heterogener Systemintegration, NAS oder gemeinsamen Ordnern ist es sicherer, auch manifest / done und idempotency einzuführen
  • Sollen mehrere writer denselben logischen Zustand aktualisieren, sich nicht mit Dateiintegration überheben, sondern auch eine DB oder Queue in Betracht ziehen
  • OS-Sperren wirken innerhalb einer homogenen Anwendungsgruppe mit gleichen Voraussetzungen, ersetzen aber kein Übergabeprotokoll

Der letzte Punkt ist zugleich eine Rückzugsentscheidung. Probleme, die mit Dateien wirklich schmerzhaft werden, gibt es tatsächlich.

7. Fazit

Wechselseitiger Ausschluss bei der Dateiintegration bedeutet nicht, eine Sperrfunktion aufzurufen, sondern Zustandsübergänge festzulegen. Das ist der Kerngedanke dieses Artikels. Erzeugung / veröffentlicht / in Bearbeitung / verarbeitet werden über Namen und Verzeichnisse ausgedrückt, während die zweistufige Prüfung Exists -> Create, das direkte Schreiben unter dem endgültigen Dateinamen, das Warten auf eine stabile Größe, die gegenseitige Aktualisierung gemeinsamer Dateien und das blinde Vertrauen in Lock-APIs vermieden werden. Kombiniert man darüber hinaus temp -> close -> rename / replace, done / manifest, claim-rename, lease und idempotency, lassen sich die meisten Unfälle bei der Integration über gemeinsame Ordner vermeiden.

Der Trick bei der Dateiintegration besteht darin, „lesbar sein“ und „gelesen werden dürfen“ nicht gleichzusetzen. Allein diese Trennung reduziert die Art von Unfall, die nur mitten in der Nacht auftritt, erheblich.

8. Referenzen

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.

Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.

Windows-App-Entwicklung

In der Windows-App-Entwicklung, die Integration über gemeinsame Ordner und nächtliche Batch-Läufe einschließt, wirkt sich das Design des wechselseitigen Ausschlusses direkt auf die Implementierungsqualität aus.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Reicht bei der Dateiintegration eine Lock-API allein für den wechselseitigen Ausschluss aus?
Meistens nicht. Linux' flock ist eine advisory lock, sodass sich ohne Weiteres ein Programm schreiben lässt, das die Abmachung ignoriert, und Windows' byte-range lock wird bei memory-mapped Dateien ignoriert. OS-Sperren sind innerhalb derselben Anwendungsgruppe mit denselben Voraussetzungen wirksam, sollten aber nur als Hilfsmittel dienen. Der eigentliche Kern sollte das Design eines Übergabeprotokolls aus temp -> rename, done/manifest, atomarem claim und idempotency sein.
Wie verhindert man, dass eine noch unvollständig geschriebene Datei gelesen wird?
Der Königsweg ist die Veröffentlichung über temp -> close -> rename/replace. Die entstehende Datei wird unter einem temp-Namen eingesperrt und nach dem close im selben Verzeichnis auf den final-Namen umgestellt; der Empfänger achtet nur auf den final-Namen. Voraussetzung ist, dass temp und final im selben Verzeichnis, mindestens aber auf demselben Volume / Dateisystem liegen, und die Abmachung lautet: Sobald der final-Name sichtbar wird, ist der Inhalt vollständig.
Wie verhindert man, dass mehrere Worker dieselbe Datei gleichzeitig verarbeiten?
Man nimmt vor dem Lesen den claim atomar. Konkret verarbeitet nur der Worker die Datei, dessen rename von incoming nach processing/<worker>/ gelingt. Die zweistufige Prüfung Exists -> Create ist kein wechselseitiger Ausschluss, weil „Prüfen“ und „Sichern“ getrennte Operationen sind und sich dazwischen ein anderer Prozess einschieben kann. Ist eine atomare Erstellung nötig, verwendet man unter .NET die Familie FileMode.CreateNew oder unter POSIX O_CREAT | O_EXCL.
Gibt es Dinge zu beachten, wenn man eine lock file einsetzt?
Sie sollte keine bloße leere Datei sein, sondern eine lease (Eigentumsinformation) mit Ablaufzeit, die ownerId, host, pid, acquiredAt, expiresAt und heartbeatAt enthält. Die Erstellung erfolgt atomar, ausbleibende Aktualisierungen dienen als Grundlage für die stale-Erkennung, das Löschen bleibt im Regelfall dem Ersteller vorbehalten, und für den Fall einer ausbleibenden Freigabe sollte im Voraus ein Wiederherstellungsverfahren festgelegt werden. Statt zu versuchen, mit einer einzigen lock file vollständige Konsistenz zu garantieren, ist in der Praxis ein Design überlegen, das am Ende idempotency als Auffangnetz nutzt.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Geschäftsführer von KomuraSoft LLC

Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.

Zurück zum Blog