Technisch artikel

HotXLS crashveilig opslaan: gefaseerde tijdelijke bestanden in Delphi

Een opslag die halverwege sterft, of het nu door een gedwongen herstart, een gedode processen, of een schijf die tijdens het schrijven vol raakt komt, betekende traditioneel één ding voor een formaat gebouwd rond ter-plekke-schrijven: welke bytes ook op schijf terechtkwamen vóór de onderbreking, dat is wat u terugkrijgt, en een afgekapt werkboek gaat niet meer open. HotXLS sluit die faalmodus af met een crashveilig opslagpad dat wordt gebruikt voor elk XLSX-, ODS-, en klassiek XLS-bestand dat het schrijft. Elke SaveAs-aanroep schrijft het volledige nieuwe bestand naar een tijdelijk bestand dat naast de bestemming wordt aangemaakt, en committeert het vervolgens met één atomaire MoveFileExW-hernoeming uit de Windows-API, zodat een onderbroken opslag alleen kan falen in het produceren van het nieuwe bestand, het beschadigt nooit het bestand dat u al had. Dezelfde faseer-dan-verwissel-discipline draait uniform over beide opslagengines van HotXLS, de BIFF8-schrijver achter klassiek XLS en de OOXML-schrijver achter XLSX en ODS, en het is een patroon dat de moeite waard is om te lenen voor elk bestand dat uw eigen Delphi-code rechtstreeks overschrijft, spreadsheets of niet

Wat gebeurt er als het opslaan van een werkboek halverwege wordt onderbroken?

Het directe antwoord is dat het volledig afhangt van hoe de schrijver het bestemmingsbestand aanraakt, en de gangbare implementatie, het doelbestand openen en nieuwe inhoud er rechtstreeks in streamen, is prima zolang er nooit iets misgaat. Zodra er iets misgaat, een crash, een gedwongen procesdoding, een netwerkshare die halverwege het schrijven wegvalt, blijft het bestand op schijf achter in welke tussentoestand de schrijver ook had bereikt: een ZIP-centrale-directory die nooit werd toegevoegd voor XLSX of ODS, of een BIFF-stroom die records mist die een lezer verwacht voor klassiek XLS. Excel herstelt dat niet netjes, en geen enkele andere consument die een volledig bestand verwacht ook niet, dus het praktische resultaat is een werkboek dat gisteren prima opende en vandaag weigert te openen

Hoe HotXLS elke opslag faseert achter één atomaire wissel

HotXLS opent het bestemmingsbestand nooit rechtstreeks voor schrijven, voor geen van de drie formaten die het opslaat. De volgorde heeft elke keer dezelfde vorm: bouw de volledige uitvoer ergens op dat niet het bestand is dat de gebruiker al op schijf heeft, en verplaats het pas naar zijn plek zodra die opbouw volledig is geslaagd. Concreet maakt SaveAs een leeg tijdelijk bestand aan in dezelfde map als het doelpad, schrijft het hele nieuwe werkboek naar dat tijdelijke bestand, en pas nadat dat schrijven zonder fout terugkeert, committeert het het tijdelijke bestand over de bestemming met één hernoeming. Niets hiervan vereist een eigenschap om in te schakelen; het is gewoon wat SaveAs doet voor een gewoon bestandspad, bij elke aanroep

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;

Dezelfde discipline geldt voor de klassieke XLS-schrijver, niet alleen voor de OOXML-schrijver, en de twee tijdelijke bestanden delen zelfs een naamgevingsconventie: beide roepen de Windows-API GetTempFileNameW aan met het voorvoegsel hxl, dus een opslag die vóór opruiming wordt onderbroken, kan een verdwaald bestand achterlaten met een naam als hxl4C2A.tmp naast uw werkboek. Dat bestand is geen corruptie, het is bewijs dat het mechanisme precies werkte zoals ontworpen: het onvolledige schrijven stopte daar, en uw werkelijke werkboek werd nooit voor schrijven geopend om te beginnen. Zo'n bestand zien na een crash is veilig om te verwijderen en niets om te onderzoeken

Waarom het tijdelijke bestand naast het werkboek faseren in plaats van in %TEMP%?

Het korte antwoord is dat de hernoeming van MoveFileExW alleen atomair is wanneer de bron en de bestemming zich op hetzelfde volume bevinden, en de zekerste manier om dat te garanderen zonder de aanroeper iets te laten configureren, is de locatie van het tijdelijke bestand af te leiden van het bestemmingspad zelf. HotXLS berekent de eigen map van het doel en geeft die map rechtstreeks door aan GetTempFileNameW, zodat het tijdelijke bestand altijd wordt aangemaakt op hetzelfde station, hetzelfde volume, als het bestand dat het op het punt staat te vervangen, automatisch, bij elke opslag. Had de bibliotheek in plaats daarvan schrijfacties gefaseerd in de systeem-tijdelijke map, dan zou een doelpad op een ander station of een gekoppeld netwerkvolume de laatste stap veranderen in een cross-volume-bewerking, wat de Windows-API ofwel ronduit weigert, of, als een aanroeper expliciet kiest met een extra vlag die HotXLS hier niet instelt, stilzwijgend degradeert tot een niet-atomaire kopie gevolgd door een verwijdering, waardoor precies het onderbrekingsvenster wordt heropend dat dit hele mechanisme moet sluiten

De commit-stap: MoveFileExW, write-through, en wat er gebeurt bij falen

De laatste stap van elke opslag is precies één Windows-API-aanroep, MoveFileExW, met twee vlaggen die elk apart werk doen. MOVEFILE_REPLACE_EXISTING is wat de hernoeming toestaat om te landen op een bestand dat al bestaat; zonder deze vlag faalt een hernoeming die een bestaand pad als doel heeft gewoon, wat het hele doel van een opslag die bedoeld is om een werkboek te vervangen dat u al heeft, teniet zou doen. MOVEFILE_WRITE_THROUGH dekt duurzaamheid: het vertelt de functie niet terug te keren totdat de verplaatsing daadwerkelijk op schijf is voltooid, in plaats van terug te keren zodra de hernoeming slechts in de wachtrij staat, waardoor een smaller maar reëel raceconditie wordt gesloten waarbij een crash onmiddellijk nadat SaveAs terugkeert de wissel nog in uitvoering zou kunnen betrappen. Als het tijdelijke bestand niet kan worden aangemaakt, of de uiteindelijke hernoeming om welke reden dan ook faalt (een rechtenprobleem, een vergrendelde bestemming, een volumemismatch), verwijdert HotXLS het tijdelijke bestand zelf in plaats van rommel achter te laten, en het bestemmingsbestand blijft precies zoals het was vóór de aanroep

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 zelf houdt de retourconventie aan die overal in HotXLS wordt gedeeld, één bij succes, een negatief getal bij falen, maar een kaal geheel getal zegt niet waarom een opslag faalde, en elk negatief resultaat op dezelfde manier behandelen gooit informatie weg die een retrybeleid daadwerkelijk zou kunnen gebruiken. De eigenschap LastDiagnostic, en de bredere Diagnostics-verzameling erachter, draagt het bericht dat HotXLS intern genereerde, en onderscheidt een tijdelijk bestand dat niet kon worden aangemaakt van een hernoeming die Windows weigerde. Een batchtaak die Code en Message logt bij elke mislukte SaveAs bouwt precies het bewijs op dat u wilt de ene keer dat een klant meldt dat een opslag stilzwijgend niets deed

Klassiek XLS betaalt met geheugen, XLSX en ODS betalen met schijf

De twee opslagengines bereiken hetzelfde crashveilige resultaat via verschillende routes, en het verschil doet ertoe als u al een van beide aan het afstemmen bent voor een grote batchtaak. De klassieke XLS-schrijver bouwt eerst het hele OLE compound document in het geheugen op, met behulp van structured storage ondersteund door een geheugenhandle, en kopieert die voltooide buffer pas in één keer naar het naastgelegen tijdelijke bestand; de redenering in HotXLS's eigen broncode is direct: het hele bestand eerst in het geheugen opbouwen is wat voorkomt dat een mislukte of geannuleerde opslag de bestemming ooit afkapt. De XLSX- en ODS-schrijver streamt in plaats daarvan zijn ZIP-vermeldingen naar het tijdelijke bestand terwijl ze worden geproduceerd, dezelfde fasering op bestandsniveau met een ander geheugenprofiel. Als u al leunt op StreamingWrite om grote XLSX-exports binnen het geheugenlimiet van een container te houden, weet dan dat de equivalente hendel voor klassieke XLS-export niet in dezelfde vorm bestaat: de crashveilige garantie is in beide gevallen onvoorwaardelijk, maar een zeer grote legacy .xls-export houdt zijn volledige uitvoer sowieso in RAM, een afweging die uitgebreider wordt behandeld in ons artikel over streaming writes voor server-batchtaken

Hetzelfde patroon toepassen buiten HotXLS, en waar de garantie ophoudt

Het patroon lenen is vooral een kwestie van dezelfde twee Windows-API-aanroepen aansluiten waar HotXLS intern op leunt. GetTempFileNameW geeft u een uniek genoemd, leeg bestand in een map die u kiest, en MoveFileExW committeert uw voltooide schrijfactie in één stap over de echte bestemming; een minimale versie van diezelfde routine die HotXLS vóór elke SaveAs uitvoert, ziet er zo uit

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;

De garantie heeft reële randen die de moeite waard zijn om te kennen voordat u er blindelings op vertrouwt. Het faseren van een volledige kopie voordat het origineel wordt vervangen, betekent dat een opslag tijdelijk schijfruimte nodig heeft voor zowel het oude als het nieuwe bestand, ruwweg het dubbele van de grootte van het werkboek gedurende het schrijven, wat prima is voor een rapport en de moeite waard om te controleren voor een export van meerdere gigabytes die draait tegen een bijna vol volume. Het tijdelijke bestand moet ook in dezelfde map terechtkomen als de bestemming, dus welk account HotXLS ook onder draait, dat heeft specifiek toestemming nodig om bestanden aan te maken in die map, niet alleen toestemming om het ene bestand te overschrijven dat het al kent; een implementatie die een bestemmingsmap vergrendelt tot ter-plekke-bewerkingen van specifieke bestaande bestandsnamen, in plaats van schrijftoegang op mapniveau, zal SaveAs zien falen op de tijdelijke-bestandsstap, ook al zou het equivalente rechtstreekse schrijven zou zijn geslaagd

Twee andere grenzen zijn het waard om ronduit te noemen. Een bestemming op een netwerkshare of binnen een map die door OneDrive of een vergelijkbare client wordt gesynchroniseerd, kan zich anders gedragen dan lokale NTFS, ook al meldt Windows het nog steeds als één enkel volume, omdat het bestandssysteemstuurprogramma ervoor hernoeming mogelijk niet op dezelfde manier implementeert; als uw implementatiedoel opslaat over een netwerkpad, is het de moeite waard daar specifiek een gedwongen onderbreking te testen in plaats van aan te nemen dat lokaal-schijfgedrag overdraagt. En het hele mechanisme is beperkt tot opslaan in een benoemd bestand. Roep SaveAs in plaats daarvan aan tegen een TStream, en HotXLS schrijft rechtstreeks in welke stream u ook heeft gegeven, zonder bestemmingsbestand om te faseren of te beschermen, omdat de duurzaamheid van die stream (een geheugenbuffer, een netwerk-upload, een database-blob) vanaf dat punt volledig de verantwoordelijkheid van uw code is

Een verificatiepas kan achteraf precies op deze garantie vertrouwen, inclusief het soort dat is ingebouwd in een workbench voor werkboekaudit en -conversie: een heropend bestand dat kort of ontbrekend terugkomt, is een echt conversieprobleem om na te trekken, nooit een opslag die halverwege werd onderbroken en iets dubbelzinnigs op schijf achterliet. Crashveilige gefaseerde schrijfacties zijn ingebouwd in SaveAs voor elk XLSX-, ODS-, en klassiek XLS-werkboek geproduceerd door de HotXLS-component voor Delphi en C++Builder, zonder dat configuratie nodig is om het in te schakelen