Technischer Artikel

HotXLS absturzsichere Speicherung: Gestufte Temp-Dateien in Delphi

Ein Speichervorgang, der auf halbem Weg stirbt, sei es durch einen erzwungenen Neustart, einen abgeschossenen Prozess oder eine Festplatte, die mitten im Schreiben volläuft, bedeutete für ein um In-Place-Schreiben herum gebautes Format traditionell eines: Was auch immer an Bytes vor der Unterbrechung die Festplatte erreichte, ist das, was zurückbleibt, und eine abgeschnittene Arbeitsmappe öffnet sich nicht mehr. HotXLS schließt diesen Fehlerfall mit einem absturzsicheren Speicherpfad, der für jede XLSX-, ODS- und klassische XLS-Datei verwendet wird, die es schreibt. Jeder SaveAs-Aufruf schreibt die vollständige neue Datei in eine temporäre Datei, die neben dem Ziel erstellt wird, und übergibt sie dann mit einem einzigen atomaren MoveFileExW-Rename aus der Windows-API, sodass ein unterbrochener Speichervorgang bestenfalls die neue Datei nicht erzeugen kann, aber niemals die bereits vorhandene beschädigt. Dieselbe Stage-dann-Swap-Disziplin läuft einheitlich über beide Speicher-Engines von HotXLS, den BIFF8-Writer hinter klassischem XLS und den OOXML-Writer hinter XLSX und ODS, und es ist ein Muster, das sich zu übernehmen lohnt, für jede Datei, die Ihr eigener Delphi-Code direkt überschreibt, Tabellen oder nicht

Was passiert, wenn das Speichern einer Arbeitsmappe auf halbem Weg unterbrochen wird?

Die direkte Antwort ist, dass es vollständig davon abhängt, wie der Writer die Zieldatei anfasst, und die übliche Implementierung, die Zieldatei zu öffnen und neuen Inhalt direkt hineinzustreamen, ist völlig in Ordnung, solange nichts schiefgeht. In dem Moment, in dem doch etwas schiefgeht, ein Absturz, ein erzwungenes Beenden des Prozesses, eine Netzwerkfreigabe, die mitten im Schreiben abbricht, bleibt die Datei auf der Festplatte in welchem Zwischenzustand auch immer der Writer erreicht hatte: ein ZIP-Zentralverzeichnis, das bei XLSX oder ODS nie angehängt wurde, oder ein BIFF-Stream bei klassischem XLS, dem Datensätze fehlen, die ein Reader erwartet. Excel repariert das nicht sauber, und kein anderer Konsument, der eine vollständige Datei erwartet, tut das ebenfalls nicht, sodass das praktische Ergebnis eine Arbeitsmappe ist, die gestern noch problemlos öffnete und sich heute weigert

Wie HotXLS jedes Speichern hinter einem atomaren Swap staffelt

HotXLS öffnet die Zieldatei bei keinem der drei Formate, die es speichert, jemals direkt zum Schreiben. Die Abfolge hat jedes Mal dieselbe Form: die vollständige Ausgabe irgendwo aufbauen, das nicht die Datei ist, die der Benutzer bereits auf der Festplatte hat, und sie erst dann an ihren Platz verschieben, wenn dieser Aufbau vollständig erfolgreich war. Konkret erzeugt SaveAs eine leere temporäre Datei im selben Ordner wie der Zielpfad, schreibt die gesamte neue Arbeitsmappe in diese temporäre Datei, und erst nachdem dieses Schreiben ohne Fehler zurückgekehrt ist, übergibt es die temporäre Datei mit einem einzigen Rename über das Ziel. Nichts davon erfordert eine Eigenschaft zum Einschalten; es ist einfach das, was SaveAs für einen einfachen Dateipfad tut, bei jedem Aufruf

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Report');
    Sheet.Cells[1, 1].Value := 'Nothing special to enable here';
    // If this call is interrupted, monthly-report.xlsx on disk stays
    // either the old version, complete, or the new version, complete
    if Book.SaveAs('monthly-report.xlsx', xlsxOpenXMLWorkbook) <> 1 then
      raise Exception.Create('Save failed, see Book.LastDiagnostic');
  finally
    Book.Free;
  end;
end;

Dieselbe Disziplin gilt für den klassischen XLS-Writer, nicht nur für den OOXML-Writer, und die beiden temporären Dateien teilen sich sogar eine Namenskonvention: Beide rufen die Windows-API GetTempFileNameW mit dem Präfix hxl auf, sodass ein vor der Bereinigung unterbrochener Speichervorgang eine verirrte Datei mit einem Namen wie hxl4C2A.tmp neben Ihrer Arbeitsmappe zurücklassen kann. Diese Datei ist keine Beschädigung, sie ist der Beweis, dass der Mechanismus genau wie vorgesehen funktioniert hat: Das unvollständige Schreiben blieb dort stehen, und Ihre eigentliche Arbeitsmappe wurde nie überhaupt zum Schreiben geöffnet. Eine solche Datei nach einem Absturz zu sehen ist gefahrlos zu löschen und nichts, was untersucht werden müsste

Warum die temporäre Datei neben der Arbeitsmappe staffeln statt in %TEMP%?

Die kurze Antwort ist, dass das Rename von MoveFileExW nur atomar ist, wenn Quelle und Ziel auf demselben Volume liegen, und der sicherste Weg, das zu garantieren, ohne den Aufrufer irgendetwas konfigurieren zu lassen, besteht darin, den Speicherort der temporären Datei aus dem Zielpfad selbst abzuleiten. HotXLS berechnet den eigenen Ordner des Ziels und übergibt dieses Verzeichnis direkt an GetTempFileNameW, sodass die temporäre Datei immer auf demselben Laufwerk, demselben Volume, erstellt wird wie die Datei, die sie gleich ersetzen wird, automatisch, bei jedem Speichervorgang. Hätte die Bibliothek stattdessen Schreibvorgänge im System-Temp-Ordner gestaffelt, würde ein Zielpfad auf einem anderen Laufwerk oder einem gemappten Netzwerk-Volume den letzten Schritt zu einer laufwerksübergreifenden Operation machen, die die Windows-API entweder rundweg ablehnt oder, falls ein Aufrufer sich mit einem zusätzlichen Flag, das HotXLS hier nicht setzt, ausdrücklich dafür entscheidet, still zu einer nicht-atomaren Kopie gefolgt von einem Löschen degradiert – und damit genau das Unterbrechungsfenster wieder öffnet, das dieser gesamte Mechanismus schließen soll

Der Commit-Schritt: MoveFileExW, Write-Through und was bei einem Fehlschlag passiert

Der letzte Schritt jedes Speichervorgangs ist genau ein Windows-API-Aufruf, MoveFileExW, mit zwei Flags, die jeweils unterschiedliche Arbeit leisten. MOVEFILE_REPLACE_EXISTING ist das, was das Rename auf eine bereits existierende Datei landen lässt; ohne es schlägt ein Rename, das auf einen existierenden Pfad zielt, einfach fehl, was den gesamten Zweck eines Speicherns, das eine bereits vorhandene Arbeitsmappe ersetzen soll, zunichtemachen würde. MOVEFILE_WRITE_THROUGH deckt Dauerhaftigkeit ab: Es weist die Funktion an, erst zurückzukehren, wenn die Verschiebung tatsächlich auf der Festplatte abgeschlossen ist, statt zurückzukehren, sobald das Rename lediglich eingereiht ist, und schließt damit ein engeres, aber reales Rennen, bei dem ein Absturz unmittelbar nach der Rückkehr von SaveAs den Swap immer noch mitten im Flug erwischen könnte. Kann die temporäre Datei nicht erstellt werden, oder schlägt das abschließende Rename aus irgendeinem Grund fehl (ein Berechtigungsproblem, ein gesperrtes Ziel, eine Volume-Diskrepanz), löscht HotXLS die temporäre Datei selbst, statt Müll zu hinterlassen, und die Zieldatei bleibt exakt so, wie sie vor dem Aufruf war

Result := Book.SaveAs(TargetPath, xlsxOpenXMLWorkbook);
if Result <> 1 then
begin
  // TargetPath on disk is unchanged; safe to retry, alert, or
  // fall back to a different path without touching prior output
  LogWriter.Write(Format('SaveAs failed (%d): %s',
    [Book.LastDiagnostic.Code, Book.LastDiagnostic.Message]));
  Exit(False);
end;

SaveAs selbst behält die über HotXLS hinweg geteilte Rückgabekonvention bei, eine bei Erfolg, eine negative Zahl bei Fehlschlag, aber eine bloße Ganzzahl sagt nicht, warum ein Speichern fehlschlug, und jedes negative Ergebnis gleich zu behandeln verschenkt Information, die eine Retry-Strategie tatsächlich nutzen könnte. Die Eigenschaft LastDiagnostic, und die umfangreichere Diagnostics-Sammlung dahinter, trägt die intern von HotXLS erzeugte Meldung und unterscheidet eine temporäre Datei, die nicht erstellt werden konnte, von einem Rename, das Windows verweigert hat. Ein Batch-Job, der bei jedem fehlgeschlagenen SaveAs Code und Message protokolliert, baut genau die Beweise auf, die Sie brauchen, für das eine Mal, dass ein Kunde ein Speichern meldet, das still nichts getan hat

Klassisches XLS zahlt mit Speicher, XLSX und ODS zahlen mit Festplatte

Die beiden Speicher-Engines erreichen dasselbe absturzsichere Ergebnis auf unterschiedlichen Wegen, und der Unterschied zählt, falls Sie bereits eine der beiden für einen großen Batch-Job abstimmen. Der klassische XLS-Writer baut das gesamte OLE-Compound-Dokument zunächst im Speicher auf, unter Verwendung von Structured Storage, das von einem Speicher-Handle gestützt wird, und kopiert diesen fertigen Puffer erst in einem einzigen Schreibvorgang in die benachbarte temporäre Datei; die Begründung in HotXLSs eigenem Quellcode ist direkt: Die gesamte Datei zuerst im Speicher aufzubauen ist das, was verhindert, dass ein fehlgeschlagenes oder abgebrochenes Speichern jemals das Ziel abschneidet. Der XLSX- und ODS-Writer streamt seine ZIP-Einträge stattdessen in die temporäre Datei, während sie erzeugt werden, dieselbe Staffelung auf Dateiebene mit einem anderen Speicherprofil. Falls Sie sich bereits auf StreamingWrite verlassen, um große XLSX-Exporte innerhalb der Speichergrenze eines Containers zu halten, wissen Sie, dass der entsprechende Hebel für den klassischen XLS-Export in derselben Form nicht existiert: Die absturzsichere Garantie gilt so oder so bedingungslos, aber ein sehr großer Legacy-.xls-Export hält seine vollständige Ausgabe unabhängig davon im RAM, ein Kompromiss, der ausführlicher in unserem Artikel zu Streaming-Writes für Server-Batch-Jobs behandelt wird

Dasselbe Muster außerhalb von HotXLS anwenden, und wo die Garantie endet

Das Muster zu übernehmen ist größtenteils eine Frage, dieselben zwei Windows-API-Aufrufe zu verdrahten, auf die sich HotXLS intern verlässt. GetTempFileNameW gibt Ihnen eine eindeutig benannte, leere Datei in einem Ordner Ihrer Wahl, und MoveFileExW übergibt Ihr fertiges Schreiben in einem Schritt über das echte Ziel; eine minimale Version derselben Routine, die HotXLS vor jedem SaveAs ausführt, sieht so aus

function SaveFileAtomically(const Path: WideString; const Contents: TBytes): Boolean;
var
  Dir, TempName: WideString;
  Buffer: array[0..MAX_PATH] of WideChar;
  FS: TFileStream;
begin
  Result := False;
  Dir := ExtractFilePath(ExpandFileName(Path));
  FillChar(Buffer, SizeOf(Buffer), 0);
  if GetTempFileNameW(PWideChar(Dir), 'app', 0, @Buffer[0]) = 0 then
    Exit;
  TempName := PWideChar(@Buffer[0]);
  try
    FS := TFileStream.Create(TempName, fmCreate or fmShareExclusive);
    try
      FS.WriteBuffer(Contents[0], Length(Contents));
    finally
      FS.Free;
    end;
    Result := MoveFileExW(PWideChar(TempName), PWideChar(ExpandFileName(Path)),
      MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
  finally
    if not Result then
      DeleteFileW(PWideChar(TempName));
  end;
end;

Die Garantie hat reale Grenzen, die es sich lohnt zu kennen, bevor man sich blind auf sie verlässt. Eine vollständige Kopie zu staffeln, bevor das Original ersetzt wird, bedeutet, dass ein Speichervorgang für die Dauer des Schreibens kurzzeitig Festplattenplatz sowohl für die alte als auch für die neue Datei braucht, ungefähr das Doppelte der Arbeitsmappengröße, was für einen Report kein Problem ist und bei einem mehrere Gigabyte großen Export gegen ein fast volles Volume geprüft werden sollte. Die temporäre Datei muss auch im selben Ordner wie das Ziel landen, sodass welches Konto auch immer HotXLS ausführt, speziell auf diesem Ordner die Berechtigung zum Erstellen von Dateien braucht, nicht nur die Berechtigung, die eine bereits bekannte Datei zu überschreiben; eine Bereitstellung, die einen Zielordner auf In-Place-Bearbeitungen bestimmter existierender Dateinamen beschränkt, statt Schreibzugriff auf Ordnerebene zu gewähren, wird SaveAs beim Temp-Datei-Schritt scheitern sehen, obwohl das entsprechende direkte Schreiben erfolgreich gewesen wäre

Zwei weitere Grenzen sollten klar benannt werden. Ein Ziel auf einer Netzwerkfreigabe oder innerhalb eines von OneDrive oder einem ähnlichen Client synchronisierten Ordners kann sich anders verhalten als lokales NTFS, selbst wenn Windows es weiterhin als einzelnes Volume meldet, da der davor liegende Dateisystemtreiber Rename möglicherweise nicht auf dieselbe Weise implementiert; falls Ihr Bereitstellungsziel über einen Netzwerkpfad speichert, lohnt es sich, eine erzwungene Unterbrechung dort speziell zu testen, statt anzunehmen, dass sich lokales Festplattenverhalten überträgt. Und der gesamte Mechanismus ist auf das Speichern in eine benannte Datei beschränkt. Rufen Sie SaveAs stattdessen gegen einen TStream auf, und HotXLS schreibt direkt in welchen Stream Sie auch immer übergeben haben, ohne Zieldatei zu staffeln oder zu schützen, weil die Dauerhaftigkeit dieses Streams (ein Speicherpuffer, ein Netzwerk-Upload, ein Datenbank-Blob) ab diesem Punkt vollständig in der Verantwortung Ihres Codes liegt

Ein Verifizierungsdurchlauf kann sich anschließend genau auf diese Garantie verlassen, einschließlich der Art, die in eine Arbeitsmappen-Audit- und Konvertierungswerkbank eingebaut ist: Eine wieder geöffnete Datei, die verkürzt oder fehlend zurückkommt, ist ein echtes Konvertierungsproblem, das nachverfolgt werden muss, niemals ein auf halbem Weg unterbrochenes Speichern, das etwas Zweideutiges auf der Festplatte hinterlassen hat. Absturzsichere gestaffelte Schreibvorgänge sind in SaveAs für jede von der HotXLS-Komponente für Delphi und C++Builder erzeugte XLSX-, ODS- und klassische XLS-Arbeitsmappe eingebaut, ohne dass eine Konfiguration zum Einschalten nötig wäre