Grundlagen des wechselseitigen Ausschlusses bei der Dateiintegration - Best Practices für Dateisperren und atomare Claims
· Go Komura · 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
- Zuerst das Fazit (in einem Satz)
- 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
- 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
- 3.1. Die zweistufige Prüfung
- 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
- 4.1. Veröffentlichen über
- Pseudocode (Auszug)
- Grobe Entscheidungshilfe
- Fazit
- 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.
sequenceDiagram
participant Sender as Sender
participant Freigabe as Freigegebener Ordner
participant Empfaenger as Empfänger
Sender->>Freigabe: Erstellt orders.csv unter dem endgültigen Namen
Sender->>Freigabe: Schreibt Zeile 1 bis 5000
Empfaenger->>Freigabe: Erkennt orders.csv
Empfaenger->>Freigabe: Beginnt sofort zu lesen
Note over Empfaenger: Noch nicht fertig
Sender->>Freigabe: Schreibt den Rest
Note over Empfaenger: Fehlende Zeilen / Parse-Fehler / nur teilweise verarbeitet
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.
sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant Dir as incoming
W1->>Dir: Findet a.csv
W2->>Dir: Findet a.csv
W1->>Dir: Beginnt zu lesen
W2->>Dir: Beginnt zu lesen
Note over W1,W2: Dieselbe Eingabe wird doppelt verarbeitet
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.
sequenceDiagram
participant A as Worker A
participant Lock as lock-Datei
participant B as Worker B
A->>Lock: Erstellt die lock
Note over A: Bricht hier ab
B->>Lock: Prüft, ob die lock existiert
B->>Lock: Verschiebt den Beginn der Verarbeitung
B->>Lock: Wartet weiter
Note over B,Lock: Kann nicht entscheiden, ob sie stale ist – alle stehen still
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.
sequenceDiagram
participant A as Prozess A
participant B as Prozess B
participant FS as Dateisystem
A->>FS: Prüft, ob keine lock existiert
B->>FS: Prüft, ob keine lock existiert
FS-->>A: Keine vorhanden
FS-->>B: Keine vorhanden
A->>FS: Erstellt die lock
B->>FS: Erstellt die lock
Note over A,B: Beide fahren fort
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.
flowchart LR
A[final-Name wird sichtbar] --> B[Empfänger erkennt ihn]
B --> C[Sender schreibt noch]
C --> D[Unvollstä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.
sequenceDiagram
participant Sender as Sender
participant Freigabe as Freigegebener Ordner
participant Empfaenger as Empfänger
Sender->>Freigabe: Beginnt data.zip zu kopieren
Sender->>Freigabe: Pausiert mittendrin
Empfaenger->>Freigabe: Größe ändert sich 10 Sekunden lang nicht
Note over Empfaenger: Fehleinschätzung als abgeschlossen
Empfaenger->>Freigabe: Beginnt zu lesen
Sender->>Freigabe: Setzt 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.
sequenceDiagram
participant A as Batch A
participant B as Batch B
participant F as status.csv
A->>F: Liest v1
B->>F: Liest v1
A->>F: Schreibt v2-A
B->>F: Schreibt v2-B
Note over F: Die Aktualisierung von A geht verloren
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’
flockist 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.
flowchart LR
A[Eindeutigen temp-Namen erzeugen] --> B[Gesamten Inhalt nach temp schreiben]
B --> C[flush / close]
C --> D[Im selben Verzeichnis auf final-Namen rename / replace]
D --> E[Empfä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.Replaceinfrage - 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\shareAnach\\server\shareB, gilt das als anderes Volume, und Windows’MoveFileExersetzt die Verschiebung bei Angabe vonMOVEFILE_COPY_ALLOWEDdurch Kopieren und Löschen. Der Vorgang ist dann also nicht mehr atomar, und Zwischenzustände werden sichtbar. Die Voraussetzung, temp und final sowieincomingundprocessinginnerhalb 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.
flowchart TD
A[data.tmp erzeugen] --> B[Als data.csv veröffentlichen]
B --> C[data.done / manifest.json erstellen]
C --> D[Empfänger erkennt done / manifest]
D --> E[Dateiname, 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.
sequenceDiagram
participant W1 as Worker 1
participant W2 as Worker 2
participant IN as incoming
participant PR as processing
W1->>IN: Findet a.csv
W2->>IN: Findet a.csv
W1->>PR: rename von a.csv
W2->>PR: rename von a.csv
Note over W1,W2: Nur wer zuerst erfolgreich ist, übernimmt die Eigentümerschaft
In der Praxis lässt sich das durch getrennte Verzeichnisse auch besser nachverfolgen.
flowchart LR
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|Erfolg| A[archive]
P -->|Fehler| E[error]
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.
flowchart TD
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
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.
flowchart LR
A[Eingabe + idempotency key] --> B{Bereits verarbeitet?}
B -- Ja --> C[Als Erfolg werten, ohne erneut auszuführen]
B -- Nein --> D[Verarbeitung ausführen]
D --> E[Im 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.
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.
ExistsundWriteAllTextsind getrennte OperationenfinalPathwird bereits während des Schreibens sichtbar- Bei abnormaler Beendigung bleibt
lockzurü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 -> renameallein recht stabil - Bei mehreren consumern das claim-rename
incoming -> processingeinfü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
- Das Beispielcode-Set zu diesem Artikel (Bibliothek, Demo, Unit-Tests) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- MoveFileEx function (Win32)
- File Times (Win32)
- FileStream.Lock Method (.NET)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Praxisleitfaden für FileSystemWatcher - Umgang mit verpassten und doppelten Ereignissen
Wir ordnen die Verwendung von FileSystemWatcher und ihre Fallstricke entlang von verpassten Ereignissen, doppelten Benachrichtigungen, Fa...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Eine Einführung in ADRs (Architecture Decision Records) — Der minimale Weg, „warum wir es so entworfen haben“ in einem kleinen Team festzuhalten
Code erklärt nie, warum er so geschrieben wurde. Wir zeigen, wie Sie mit einem ADR (Architecture Decision Record) — eine Entscheidung, ei...
Wenn man eine Windows-App besser nicht ins Web migriert: Entscheidungstabelle und die pragmatische Lösung der Aufteilung
Der Wunsch, interne Windows-Anwendungen ins Web zu migrieren, nimmt stetig zu. Doch bei Apps mit Gerätekopplung, lokaler Dateiverarbeitun...
CSV ist nicht „nur Text“ ── CSV in der Praxis für C#-Business-Anwendungen (Zeichenkodierung, Excel-Kompatibilität, Schutz vor Injection)
Typische Fehlerquellen bei der CSV-Ein-/Ausgabe in Business-Anwendungen – selbstgebautes Parsen mit Split(','), Excel-Zeichensalat bei UT...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
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.
Technische Beratung und Design-Review
Wenn Sie die Aufgabenteilung zwischen Sperren, atomaren Claims und idempotency zunächst als Design klären möchten, lässt sich das als technische Beratung und Design-Review behandeln.
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.