Techninis straipsnis

HotXLS atsparus gedimams įrašymas: laikini failai Delphi

Jei įrašymas nutrūksta viduryje dėl priverstinio paleidimo iš naujo, nutraukto proceso ar rašant prisipildžiusio disko, formatui, paremtam įrašymu vietoje, tai tradiciškai reiškia viena: gaunate tuos baitus, kurie iki nutrūkimo pasiekė diską, o sutrumpinta darbaknygė vėl nebeatsidaro. HotXLS užveria šį gedimo scenarijų atspariu gedimams įrašymo keliu, naudojamu kiekvienam jos įrašomam XLSX, ODS ir klasikiniam XLS failui. Kiekvienas SaveAs iškvietimas įrašo visą naują failą į laikiną failą, sukurtą šalia paskirties failo, tada įvykdo pakeitimą vienu atominiu MoveFileExW pervadinimu iš Windows API, todėl nutrūkęs įrašymas gali tik nesukurti naujo failo, bet niekada nepažeidžia jau turėto failo. Ta pati nuosekli paruošimo ir pakeitimo tvarka vienodai veikia abiejuose HotXLS įrašymo varikliuose: BIFF8 rašyklėje, naudojamoje klasikiniam XLS, ir OOXML rašyklėje, naudojamoje XLSX bei ODS, todėl šį principą verta pritaikyti bet kuriam failui, kurį jūsų Delphi kodas perrašo tiesiogiai, ne tik skaičiuoklėms

Kas nutinka, jei darbaknygės įrašymas nutrūksta viduryje?

Tiesioginis atsakymas visiškai priklauso nuo to, kaip rašyklė paliečia paskirties failą, o įprastas būdas, kai paskirties failas atidaromas ir naujas turinys srautu įrašomas tiesiai į jį, veikia tol, kol niekas nenutinka. Vos tik kas nors nutinka — įvyksta gedimas, procesas priverstinai nutraukiamas ar tinklo bendrinimas nutrūksta rašymo metu — diske lieka tokia tarpinė būsena, kurią rašyklė buvo pasiekusi: XLSX arba ODS faile neatsiradęs ZIP centrinis katalogas arba klasikiniame XLS faile trūkstami BIFF srauto įrašai, kurių tikisi skaitytuvas. Excel tokio failo tinkamai nepataiso, kaip ir jokia kita programa, tikinti, kad failas yra išbaigtas, todėl praktiškai gaunate darbaknygę, kuri vakar atsidarė puikiai, o šiandien atsisako atsidaryti

Kaip HotXLS paruošia kiekvieną įrašymą prieš vieną atominį pakeitimą

HotXLS niekada tiesiogiai neatveria paskirties failo rašymui nė vienam iš trijų palaikomų formatų. Kiekvieną kartą seka tokia pati: sukurti visą išvestį kitoje vietoje, kuri nėra naudotojo jau turimas failas diske, ir perkelti ją į vietą tik visiškai sėkmingai užbaigus kūrimą. Konkrečiai SaveAs sukuria tuščią laikiną failą tame pačiame aplanke kaip paskirties kelias, įrašo į jį visą naują darbaknygę ir tik tada, kai šis įrašymas grįžta be klaidos, vienu pervadinimu pakeičia paskirties failą laikinuoju. Nereikia įjungti jokios ypatybės; paprastas failo kelias SaveAs atveju taip veikia kiekvieno iškvietimo metu

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;

Ta pati tvarka taikoma ir klasikinei XLS rašyklei, ne tik OOXML rašyklei, o abu laikini failai netgi naudoja tą pačią vardų suteikimo taisyklę: abu iškviečia Windows GetTempFileNameW API su hxl priešdėliu, todėl prieš valymą nutrūkęs įrašymas šalia darbaknygės gali palikti failą, kurio vardas panašus į hxl4C2A.tmp. Tai nėra pažeidimas — tai įrodymas, kad mechanizmas veikė tiksliai pagal paskirtį: nebaigtas įrašymas sustojo ten, o tikroji darbaknygė apskritai nebuvo atverta rašymui. Po gedimo tokį failą saugu ištrinti ir nereikia nieko tirti

Kodėl laikiną failą kurti šalia darbaknygės, o ne %TEMP%?

Trumpas atsakymas toks: MoveFileExW pervadinimas yra atominis tik tada, kai šaltinis ir paskirties failas yra tame pačiame tome, o patikimiausias būdas tai garantuoti nieko nekonfigūruojant yra nustatyti laikino failo vietą pagal patį paskirties kelią. HotXLS apskaičiuoja paskirties aplanką ir perduoda šį katalogą tiesiai GetTempFileNameW, todėl laikinas failas visada automatiškai sukuriamas tame pačiame diske ir tame pačiame tome kaip failas, kurį netrukus pakeis, kiekvieno įrašymo metu. Jei biblioteka būtų ruošusi įrašymą sistemos laikinajame aplanke, paskirties kelias kitame diske ar susietame tinklo tome paskutinį veiksmą paverstų kelių tomų operacija, kurios Windows API arba iškart atsisako, arba, jei naudotojas aiškiai įjungia papildomą vėliavėlę, kurios HotXLS čia nenustato, tyliai suprastina iki neatominių kopijų ir ištrynimo, vėl atverdama tą pačią nutrūkimo spragą, kurią šis mechanizmas turi užverti

Pakeitimo etapas: MoveFileExW, įrašymas į diską ir kas nutinka gedimo atveju

Paskutinis kiekvieno įrašymo etapas yra lygiai vienas Windows API iškvietimas — MoveFileExW, perduodant dvi vėliavėles, kurių kiekviena atlieka skirtingą darbą. MOVEFILE_REPLACE_EXISTING leidžia pervadinimu pakeisti jau egzistuojantį failą; be jo pervadinimas, nukreiptas į esamą kelią, tiesiog nepavyktų, o tai paneigtų visą įrašymo, kuriuo pakeičiama jau turima darbaknygė, prasmę. MOVEFILE_WRITE_THROUGH užtikrina patvarumą: funkcija negrįžta tol, kol perkėlimas iš tikrųjų nebaigtas diske, užuot grįžusi vos tik pervadinimas tik įtraukiamas į eilę, taip užveriant siauresnę, bet realią lenktyniavimo situaciją, kai gedimas iškart po SaveAs grįžimo dar galėtų užklupti vykstantį pakeitimą. Jei laikino failo nepavyksta sukurti arba galutinis pervadinimas dėl bet kokios priežasties nepavyksta (teisių problema, užrakintas paskirties failas, tomų neatitikimas), HotXLS pati ištrina laikiną failą, nepalikdama šiukšlių, o paskirties failas lieka lygiai toks, koks buvo prieš iškvietimą

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;

Pats SaveAs visame HotXLS naudoja bendrą grąžinamąją taisyklę: vienetas reiškia sėkmę, neigiamas skaičius — nesėkmę, tačiau vien tik sveikasis skaičius nepasako, kodėl įrašymas nepavyko, o vienodai traktuojant visus neigiamus rezultatus prarandama informacija, kurią iš tikrųjų būtų galima panaudoti pakartojimo strategijoje. LastDiagnostic ypatybė ir išsamesnė už jos esanti Diagnostics kolekcija pateikia HotXLS viduje sugeneruotą pranešimą ir atskiria atvejį, kai nepavyko sukurti laikino failo, nuo atvejo, kai Windows atmetė pervadinimą. Paketinis darbas, kiekvieno nepavykusio SaveAs metu registruojantis Code ir Message, sukaupia būtent tuos įrodymus, kurių reikia vienintelį kartą klientui pranešus, kad įrašymas tyliai nieko neatliko

Klasikinis XLS moka atmintimi, XLSX ir ODS — disku

Abu įrašymo varikliai pasiekia tą patį atsparų gedimams rezultatą skirtingais keliais, ir šis skirtumas svarbus, jei dideliam paketiniam darbui jau derinate kurį nors iš jų. Klasikinė XLS rašyklė pirmiausia visą OLE sudėtinį dokumentą sukuria atmintyje, naudodama struktūrinę saugyklą, paremtą atminties rankena, ir tik tada vienu įrašymu nukopijuoja užbaigtą buferį į šalia esantį laikiną failą; pačios HotXLS išeities kodo argumentas tiesus: viso failo sukūrimas atmintyje pirmiausia neleidžia nepavykusiam ar atšauktam įrašymui kada nors sutrumpinti paskirties failo. XLSX ir ODS rašyklė vietoj to rašo ZIP įrašus į laikiną failą srautu, kai jie sukuriami, išlaikydama tą patį failo lygio paruošimą, bet kitokį atminties profilį. Jei jau naudojate StreamingWrite, kad dideli XLSX eksportai neviršytų konteinerio atminties limito, žinokite, kad lygiavertės rankenėlės klasikiniam XLS eksportui tokia pačia forma nėra: atsparumo gedimams garantija vis tiek besąlyginė, tačiau labai didelis senojo formato .xls eksportas visą išvestį laiko RAM atmintyje, o šis kompromisas išsamiau aptariamas mūsų straipsnyje apie srautinį įrašymą serverio paketiniuose darbuose

To paties principo taikymas ne HotXLS aplinkoje ir kur baigiasi garantija

Perimti šį principą dažniausiai reiškia sujungti tuos pačius du Windows API iškvietimus, kuriais HotXLS remiasi viduje. GetTempFileNameW pateikia unikaliu vardu pavadintą tuščią failą pasirinktame aplanke, o MoveFileExW vienu veiksmu įrašo jūsų užbaigtą turinį į tikrą paskirties failą; minimali tos pačios tvarkos, kurią HotXLS vykdo prieš kiekvieną SaveAs, versija atrodo taip

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;

Garantija turi aiškias ribas, kurias verta žinoti prieš aklai ja pasikliaujant. Visos kopijos paruošimas prieš pakeičiant originalą reiškia, kad įrašymo metu trumpam reikia vietos ir senam, ir naujam failui — maždaug dvigubai didesnės nei darbaknygė vietos, kol vyksta rašymas; ataskaitai tai nesunku, tačiau verta patikrinti kelių gigabaitų eksportą beveik pilname tome. Laikinas failas taip pat turi būti sukurtas tame pačiame aplanke kaip paskirties failas, todėl paskyrai, kuria veikia HotXLS, konkrečiame aplanke reikia leidimo kurti failus, o ne vien teisės perrašyti vieną jau žinomą failą; diegimas, kuriame paskirties aplanke leidžiama vietoje keisti tik konkrečiais vardais esančius failus, bet nesuteikiama aplanko lygio rašymo teisė, matys, kad SaveAs nepavyksta laikino failo etape, nors lygiavertis tiesioginis įrašymas būtų pavykęs

Dar verta aiškiai paminėti dvi ribas. Paskirties failas tinklo bendrinime arba aplanke, kurį sinchronizuoja OneDrive ar panašus klientas, gali elgtis kitaip nei vietinis NTFS, nors Windows vis dar praneša apie vieną tomą, nes tarpinė failų sistemos tvarkyklė gali įgyvendinti pervadinimą kitaip; jei jūsų diegimas įrašo tinklo kelyje, verta konkrečiai ten išbandyti priverstinį nutrūkimą, o ne manyti, kad vietinio disko elgsena išlieka tokia pati. Be to, visas mechanizmas taikomas tik įrašant į vardinį failą. Iškvieskite SaveAs su TStream ir HotXLS rašys tiesiai į jūsų perduotą srautą, neturėdama paskirties failo, kurį būtų galima paruošti ar apsaugoti, nes nuo to momento už to srauto patvarumą (atminties buferį, tinklo įkėlimą ar duomenų bazės dvejetainį objektą) visiškai atsako jūsų kodas

Vėlesnis patikrinimas gali remtis būtent šia garantija, įskaitant tą, kuri įdiegta darbaknygių audito ir konvertavimo darbo vietoje: jei iš naujo atidarytas failas grįžta sutrumpintas ar su trūkstamu turiniu, tai tikra konvertavimo problema, kurią reikia tirti, o ne neaiškus įrašymo, nutrūkusio viduryje, padarinys diske. Atsparus gedimams paruoštas įrašymas yra integruotas į SaveAs kiekvienai XLSX, ODS ir klasikinei XLS darbaknygei, kurią Delphi ir C++Builder aplinkoms sukuria HotXLS komponentas, ir jam įjungti nereikia jokios konfigūracijos