Odborný článok

Ukladanie odolné voči pádu v HotXLS: dočasné súbory v etapách v Delphi

Uloženie, ktoré zomrie v polovici, či už kvôli vynútenému reštartu, ukončenému procesu, alebo disku, ktorý sa zaplní uprostred zápisu, tradične znamenalo jedno pre formát postavený na zápise na mieste: akékoľvek bajty, ktoré sa dostali na disk pred prerušením, sú to, čo dostanete späť, a orezaný zošit sa už znova neotvorí. HotXLS tento druh zlyhania uzatvára cestou ukladania odolnou voči pádu, ktorá sa používa pre každý súbor XLSX, ODS a klasický XLS, aký zapisuje. Každé volanie SaveAs zapíše kompletný nový súbor do dočasného súboru vytvoreného vedľa cieľa, a potom ho potvrdí jediným atomickým premenovaním MoveFileExW z API Windows, takže prerušené uloženie môže nanajvýš zlyhať pri vytvorení nového súboru, no nikdy nepoškodí ten, ktorý ste už mali. Rovnaká disciplína etapa-a-potom-výmena beží rovnomerne cez oba ukladacie enginy HotXLS, zapisovač BIFF8 za klasickým XLS aj zapisovač OOXML za XLSX a ODS, a je to vzor, ktorý sa oplatí prevziať pre akýkoľvek súbor, ktorý váš vlastný kód v Delphi priamo prepisuje, tabuľky alebo nie

Čo sa stane, ak sa uloženie zošita preruší uprostred?

Priama odpoveď je, že to úplne závisí od toho, ako sa zapisovač dotýka cieľového súboru, a bežná implementácia, otvorenie cieľového súboru a streamovanie nového obsahu priamo doňho, je v poriadku, pokým sa nič nepokazí. Vo chvíli, keď sa niečo pokazí, pád, vynútené ukončenie procesu, sieťový disk, ktorý odpadne uprostred zápisu, súbor na disku zostane v akomkoľvek medzistave, ktorý zapisovač dosiahol: centrálny adresár ZIP, ktorý sa nikdy nepripojil pre XLSX alebo ODS, alebo prúd BIFF chýbajúci záznamy, ktoré čítačka očakáva pre klasický XLS. Excel to elegantne neopraví, a nerobí to ani žiadny iný konzument očakávajúci kompletný súbor, takže praktickým výsledkom je zošit, ktorý sa včera otvoril v poriadku a dnes sa otvoriť odmieta

Ako HotXLS stagingom pripraví každé uloženie za jednu atomickú výmenu

HotXLS nikdy neotvára cieľový súbor priamo na zápis, pre žiadny z troch formátov, aké ukladá. Postupnosť má rovnaký tvar zakaždým: zostaviť kompletný výstup niekde, čo nie je súbor, ktorý už používateľ má na disku, a presunúť ho na miesto až vtedy, keď táto stavba plne uspeje. Konkrétne, SaveAs vytvorí prázdny dočasný súbor v tom istom priečinku ako cieľová cesta, zapíše celý nový zošit do tohto dočasného súboru, a až potom, čo sa tento zápis vráti bez chyby, potvrdí dočasný súbor nad cieľom jediným premenovaním. Nič z toho nevyžaduje vlastnosť na zapnutie; je to jednoducho to, čo SaveAs robí pre obyčajnú cestu k súboru, pri každom volaní

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;

Rovnaká disciplína platí aj pre klasický zapisovač XLS, nielen pre OOXML, a oba dočasné súbory dokonca zdieľajú konvenciu pomenovania: oba volajú API Windows GetTempFileNameW s prefixom hxl, takže uloženie prerušené ešte pred vyčistením môže za sebou zanechať zablúdený súbor s názvom podobným hxl4C2A.tmp sediaci vedľa vášho zošita. Tento súbor nie je poškodenie, je to dôkaz, že mechanizmus fungoval presne tak, ako bol navrhnutý: nekompletný zápis sa tam zastavil, a váš skutočný zošit nebol na zápis vôbec otvorený. Uvidieť taký súbor po páde je bezpečné vymazať a nie je čo vyšetrovať

Prečo staging dočasného súboru vedľa zošita namiesto v %TEMP%?

Krátka odpoveď je, že premenovanie cez MoveFileExW je atomické iba vtedy, keď zdroj a cieľ sedia na tom istom zväzku, a najistejší spôsob, ako to zaručiť bez toho, aby ste žiadali volajúceho čokoľvek nastaviť, je odvodiť polohu dočasného súboru priamo z cieľovej cesty. HotXLS vypočíta vlastný priečinok cieľa a tento adresár odovzdá priamo GetTempFileNameW, takže dočasný súbor sa vždy vytvorí na tom istom disku, tom istom zväzku, ako súbor, ktorý má nahradiť, automaticky, pri každom uložení. Keby knižnica namiesto toho stagovala zápisy v systémovom priečinku temp, cieľová cesta na inom disku alebo pripojenom sieťovom zväzku by zmenila záverečný krok na operáciu naprieč zväzkami, ktorú API Windows buď rovno odmietne, alebo, ak sa volajúci výslovne prihlási extra príznakom, ktorý tu HotXLS nenastavuje, ticho degraduje na neatomickú kombináciu kopírovania a mazania, čím sa znova otvorí presne to okno prerušenia, kvôli ktorému tento celý mechanizmus existuje

Krok potvrdenia: MoveFileExW, zápis naskrz a čo sa stane pri zlyhaní

Posledný krok každého uloženia je presne jedno volanie API Windows, MoveFileExW, nesúce dva príznaky, ktoré každý robí odlišnú prácu. MOVEFILE_REPLACE_EXISTING je to, čo dovoľuje premenovaniu skončiť na súbore, ktorý už existuje; bez neho premenovanie mieriace na existujúcu cestu jednoducho zlyhá, čo by porazilo celý zmysel uloženia určeného na nahradenie zošita, ktorý už máte. MOVEFILE_WRITE_THROUGH pokrýva trvácnosť: hovorí funkcii, aby sa nevrátila, kým sa presun skutočne nedokončí na disku, namiesto toho, aby sa vrátila hneď, ako je premenovanie iba zaradené do fronty, čím sa uzatvára užší, ale skutočný pretek, kde pád tesne po tom, čo sa SaveAs vráti, by mohol stále zachytiť výmenu vo vzduchu. Ak sa dočasný súbor nedá vytvoriť, alebo záverečné premenovanie z akéhokoľvek dôvodu zlyhá (problém s oprávnením, uzamknutý cieľ, nesúlad zväzkov), HotXLS sám vymaže dočasný súbor namiesto toho, aby zanechal odpad, a cieľový súbor zostane presne taký, aký bol pred volaním

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;

Samotné SaveAs drží konvenciu návratovej hodnoty zdieľanú naprieč HotXLS, jednotku pri úspechu, záporné číslo pri zlyhaní, no holé celé číslo nehovorí, prečo uloženie zlyhalo, a zaobchádzať s každým záporným výsledkom rovnako zahadzuje informáciu, ktorú by politika opakovania mohla skutočne využiť. Vlastnosť LastDiagnostic, a plnšia kolekcia Diagnostics za ňou, nesie správu, ktorú HotXLS interne vygeneroval, rozlišujúc dočasný súbor, ktorý sa nepodarilo vytvoriť, od premenovania, ktoré Windows odmietol. Dávková úloha, ktorá pri každom zlyhanom SaveAs zaloguje Code a Message, si vybuduje presne ten dôkaz, aký chcete mať v ten jeden raz, keď zákazník ohlási uloženie, ktoré ticho neurobilo nič

Klasický XLS platí pamäťou, XLSX a ODS platia diskom

Oba ukladacie enginy dosahujú rovnaký výsledok odolný voči pádu odlišnými cestami, a tento rozdiel je dôležitý, ak už dolaďujete niektorý z nich pre veľkú dávkovú úlohu. Zapisovač klasického XLS najprv postaví celý zložený dokument OLE v pamäti, pomocou štruktúrovaného úložiska podloženého handlom pamäte, a až potom skopíruje tento hotový buffer do sesterského dočasného súboru jedným zápisom; úvaha vo vlastnom zdrojovom kóde HotXLS je priama: postaviť celý súbor najprv v pamäti je to, čo bráni zlyhanému alebo zrušenému uloženiu, aby niekedy orezal cieľ. Zapisovač XLSX a ODS namiesto toho streamuje svoje položky ZIP do dočasného súboru tak, ako sa produkujú, rovnaký staging na úrovni súboru s odlišným pamäťovým profilom. Ak sa už spoliehate na StreamingWrite, aby ste udržali veľké exporty XLSX vnútri pamäťových limitov kontajnera, vedzte, že ekvivalentná páka pre export klasického XLS v tej istej forme neexistuje: záruka odolnosti voči pádu je bezpodmienečná v oboch prípadoch, no veľmi veľký export do staršieho .xls drží svoj kompletný výstup v RAM bez ohľadu na to, kompromis, ktorý do hĺbky pokrýva náš článok o streamovanom zápise pre dávkové úlohy na serveri

Aplikovanie rovnakého vzoru mimo HotXLS a kde záruka končí

Prevzatie tohto vzoru je väčšinou otázkou zapojenia tých istých dvoch volaní API Windows, na ktoré sa HotXLS interne spolieha. GetTempFileNameW vám odovzdá jedinečne pomenovaný, prázdny súbor v priečinku podľa vášho výberu, a MoveFileExW potvrdí váš hotový zápis nad skutočným cieľom v jednom kroku; minimálna verzia tej istej rutiny, ktorú HotXLS spúšťa pred každým SaveAs, vyzerá takto

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;

Táto záruka má skutočné hrany, ktoré sa oplatí poznať skôr, než sa na ňu spoľahnete naslepo. Staging plnej kópie pred nahradením originálu znamená, že uloženie krátkodobo potrebuje diskový priestor pre starý aj nový súbor naraz, zhruba dvojnásobok veľkosti zošita na dobu zápisu, čo je v poriadku pre report a stojí za kontrolu pri exporte veľkosti niekoľkých gigabajtov bežiacom voči takmer plnému zväzku. Dočasný súbor sa navyše musí ocitnúť v tom istom priečinku ako cieľ, takže akýkoľvek účet, pod ktorým HotXLS beží, potrebuje oprávnenie na vytváranie súborov práve v tomto priečinku, nielen oprávnenie na prepísanie toho jedného súboru, o ktorom už vie; nasadenie, ktoré uzamkne cieľový priečinok iba na úpravy na mieste konkrétnych existujúcich názvov súborov, namiesto prístupu na zápis na úrovni priečinka, uvidí zlyhanie SaveAs na kroku dočasného súboru, aj keby ekvivalentný priamy zápis uspel

Ešte dve hranice sa oplatí jasne pomenovať. Cieľ na sieťovom disku alebo vnútri priečinka synchronizovaného cez OneDrive alebo podobného klienta sa môže správať odlišne od lokálneho NTFS, aj keď ho Windows stále hlási ako jediný zväzok, keďže ovládač súborového systému pred ním nemusí implementovať premenovanie rovnakým spôsobom; ak vaše nasadenie ukladá cez sieťovú cestu, oplatí sa otestovať vynútené prerušenie tam špecificky namiesto predpokladu, že sa správanie lokálneho disku prenesie ďalej. A celý mechanizmus je obmedzený na ukladanie do pomenovaného súboru. Zavolajte SaveAs voči TStream namiesto toho a HotXLS zapisuje priamo do akéhokoľvek prúdu, ktorý ste mu odovzdali, bez akéhokoľvek cieľového súboru na staging alebo ochranu, pretože trvácnosť tohto prúdu (pamäťový buffer, sieťový upload, databázový blob) je od toho bodu úplne zodpovednosťou vášho kódu

Overovací prechod sa potom môže spoľahnúť presne na túto záruku, vrátane toho vstavaného v pracovnej stanici na audit a konverziu zošitov: znova otvorený súbor, ktorý sa vráti skrátený alebo chýbajúci, je skutočný konverzný problém na vystopovanie, nikdy uloženie, ktoré sa prerušilo v polovici a zanechalo na disku niečo nejednoznačné. Ukladanie v etapách odolné voči pádu je zabudované do SaveAs pre každý zošit XLSX, ODS a klasický XLS produkovaný komponentom HotXLS pre Delphi a C++Builder, bez akejkoľvek konfigurácie potrebnej na jeho zapnutie