Teknisk artikkel

HotXLS krasjsikre lagringer: iscenesatte midlertidige filer i Delphi

En lagring som dør halvveis, enten fra en tvungen omstart, en drept prosess, eller en disk som blir full midt i skrivingen, har tradisjonelt betydd én ting for et format bygget rundt på-stedet-skriving: hvilke bytes som enn nådde disk før avbruddet, er det man får tilbake, og en avkuttet arbeidsbok åpner seg ikke igjen. HotXLS lukker den feilmodusen med en krasjsikker lagringsvei brukt for hver eneste XLSX-, ODS-, og klassisk XLS-fil den skriver. Hvert SaveAs-kall skriver den komplette nye filen til en midlertidig fil opprettet ved siden av destinasjonen, og bekrefter den deretter med en enkelt atomisk MoveFileExW-omdøping fra Windows-API-et, slik at en avbrutt lagring bare kan feile i å produsere den nye filen, den skader aldri den man allerede hadde. Den samme iscenesett-så-bytt-disiplinen kjører ensartet på tvers av begge HotXLS' lagringsmotorer, BIFF8-skriveren bak klassisk XLS og OOXML-skriveren bak XLSX og ODS, og det er et mønster verdt å låne for enhver fil din egen Delphi-kode overskriver direkte, regneark eller ei

Hva skjer hvis en arbeidsbok-lagring avbrytes halvveis?

Det direkte svaret er at det avhenger fullstendig av hvordan skriveren rører destinasjonsfilen, og den vanlige implementasjonen, å åpne målfilen og strømme nytt innhold direkte inn i den, er greit så lenge ingenting noensinne går galt. I det øyeblikket noe gjør det, et krasj, en tvungen prosess-drap, en nettverksdeling som faller ut midt i skrivingen, blir filen på disk stående i hvilken som helst mellomtilstand skriveren hadde nådd: en ZIP-sentralkatalog som aldri ble lagt til for XLSX eller ODS, eller en BIFF-strøm som mangler records en leser forventer for klassisk XLS. Excel reparerer ikke det elegant, og det gjør heller ingen annen konsument som forventer en komplett fil, så det praktiske resultatet er en arbeidsbok som åpnet greit i går og nekter å åpne i dag

Hvordan HotXLS iscenesetter hver lagring bak ett atomisk bytte

HotXLS åpner aldri destinasjonsfilen for skriving direkte, for noen av de tre formatene den lagrer. Sekvensen har samme form hver gang: bygg den komplette utdataen et sted som ikke er filen brukeren allerede har på disk, og flytt den bare på plass når det bygget har fullstendig lyktes. Konkret oppretter SaveAs en tom midlertidig fil i samme mappe som målstien, skriver hele den nye arbeidsboken inn i den midlertidige filen, og først etter at den skrivingen returnerer uten feil, bekrefter den den midlertidige filen over destinasjonen med én enkelt omdøping. Ingenting av dette krever en egenskap å velge inn i; det er ganske enkelt hva SaveAs gjør for en vanlig filsti, ved hvert kall

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;

Den samme disiplinen gjelder for den klassiske XLS-skriveren, ikke bare OOXML-en, og de to midlertidige filene deler til og med en navnekonvensjon: begge kaller Windows-API-et GetTempFileNameW med prefikset hxl, slik at en lagring avbrutt før opprydding kan etterlate en villfaren fil med et navn som hxl4C2A.tmp liggende ved siden av arbeidsboken din. Den filen er ikke korrupsjon, den er bevis på at mekanismen fungerte nøyaktig som designet: den ufullstendige skrivingen stoppet der, og din faktiske arbeidsbok ble aldri åpnet for skriving i utgangspunktet. Å se en slik etter et krasj er trygt å slette og ingenting å undersøke

Hvorfor iscenesette den midlertidige filen ved siden av arbeidsboken i stedet for i %TEMP%?

Det korte svaret er at MoveFileExWs omdøping bare er atomisk når kilde og destinasjon ligger på samme volum, og den sikreste måten å garantere det uten å be kalleren om å konfigurere noe, er å utlede den midlertidige filens plassering fra selve destinasjonsstien. HotXLS beregner målets egen mappe og gir den katalogen rett til GetTempFileNameW, slik at den midlertidige filen alltid opprettes på samme stasjon, samme volum, som filen den snart skal erstatte, automatisk, ved hver lagring. Hadde biblioteket i stedet iscenesatt skrivinger i systemets midlertidige mappe, ville en målsti på en annen stasjon eller et tilordnet nettverksvolum gjort det siste trinnet til en operasjon på tvers av volum, noe Windows-API-et enten avslår rett ut, eller, hvis en kaller eksplisitt velger inn med et ekstra flagg HotXLS ikke setter her, stille degraderer til en ikke-atomisk kopi etterfulgt av en sletting, som gjenåpner nøyaktig det avbruddsvinduet hele denne mekanismen finnes for å lukke

Bekreftelsestrinnet: MoveFileExW, write-through, og hva som skjer ved feil

Det siste trinnet i hver lagring er nøyaktig ett Windows-API-kall, MoveFileExW, som bærer to flagg som hver gjør distinkt arbeid. MOVEFILE_REPLACE_EXISTING er det som lar omdøpingen lande på en fil som allerede finnes; uten det feiler en omdøping som sikter mot en eksisterende sti, rett og slett, noe som ville ødelegge hele poenget med en lagring ment å erstatte en arbeidsbok man allerede har. MOVEFILE_WRITE_THROUGH dekker holdbarhet: den forteller funksjonen å ikke returnere før flyttingen faktisk er fullført på disk, i stedet for å returnere så snart omdøpingen bare er satt i kø, og lukker et smalere, men reelt kappløp der et krasj umiddelbart etter at SaveAs returnerer, fortsatt kunne fange byttet midt i utførelse. Hvis den midlertidige filen ikke kan opprettes, eller den endelige omdøpingen feiler av en eller annen grunn (et tillatelsesproblem, en låst destinasjon, et volum-mismatch), sletter HotXLS den midlertidige filen selv i stedet for å etterlate rot, og destinasjonsfilen forblir nøyaktig slik den var før kallet

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 selv holder på returkonvensjonen delt på tvers av HotXLS, én ved suksess, et negativt tall ved feil, men et bart heltall sier ikke hvorfor en lagring feilet, og å behandle hvert negativt resultat likt, kaster bort informasjon en retry-policy faktisk kunne brukt. LastDiagnostic-egenskapen, og den fyldigere Diagnostics-samlingen bak den, bærer meldingen HotXLS genererte internt, og skiller en midlertidig fil som ikke kunne opprettes fra en omdøping Windows avslo. En batch-jobb som logger Code og Message ved hver mislykket SaveAs, bygger opp nøyaktig det beviset man ønsker den ene gangen en kunde rapporterer en lagring som stille ikke gjorde noe

Klassisk XLS betaler med minne, XLSX og ODS betaler med disk

De to lagringsmotorene når det samme krasjsikre utfallet via forskjellige veier, og forskjellen betyr noe hvis man allerede finjusterer en av dem for en stor batch-jobb. Den klassiske XLS-skriveren bygger hele OLE-forbindelsesdokumentet i minnet først, ved bruk av strukturert lagring støttet av et minnehandle, og kopierer først den ferdige bufferen ut til søster-den midlertidige filen i én skriving; resonnementet i HotXLS' egen kildekode er direkte: å bygge hele filen i minnet først er det som hindrer en mislykket eller avbrutt lagring fra noensinne å kutte destinasjonen. XLSX- og ODS-skriveren strømmer i stedet ZIP-oppføringene sine inn i den midlertidige filen etter hvert som de produseres, den samme fil-nivå-iscenesettelsen med en annen minneprofil. Hvis man allerede lener seg på StreamingWrite for å holde store XLSX-eksporter innenfor en containers minnegrense, bør man vite at den tilsvarende spaken for klassisk XLS-eksport ikke finnes i samme form: den krasjsikre garantien er ubetinget uansett, men en veldig stor eldre .xls-eksport holder sin komplette utdata i RAM uansett, en avveining dekket mer i dybden i vår artikkel om strømmende skriving for batch-jobber på server

Å anvende det samme mønsteret utenfor HotXLS, og hvor garantien slutter

Å låne mønsteret handler for det meste om å koble opp de samme to Windows-API-kallene HotXLS er avhengig av internt. GetTempFileNameW gir deg en unikt navngitt, tom fil i en mappe du velger, og MoveFileExW bekrefter din ferdige skriving over den ekte destinasjonen i ett steg; en minimal versjon av den samme rutinen HotXLS kjører før hver SaveAs, ser slik ut

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;

Garantien har reelle grenser verdt å kjenne til før man stoler blindt på den. Å iscenesette en full kopi før man erstatter originalen betyr at en lagring kort trenger diskplass for både den gamle filen og den nye, omtrent det dobbelte av arbeidsbokens størrelse gjennom varigheten av skrivingen, noe som er greit for en rapport og verdt å sjekke for en flere-gigabyte-eksport som kjører mot et nesten fullt volum. Den midlertidige filen må også havne i samme mappe som destinasjonen, så uansett hvilken konto HotXLS kjører under, trenger den opprett-fil-tillatelse på nettopp den mappen, ikke bare tillatelse til å overskrive den ene filen den allerede kjenner til; en driftssetting som låser en destinasjonsmappe ned til på-stedet-redigeringer av spesifikke eksisterende filnavn, snarere enn mappe-nivå-skrivetilgang, vil se SaveAs feile på det midlertidige-fil-trinnet selv om den tilsvarende direkte skrivingen ville lyktes

To grenser til er verdt å flagge klart. En destinasjon på en nettverksdeling eller inne i en mappe synkronisert av OneDrive eller en lignende klient kan oppføre seg annerledes enn lokal NTFS selv om Windows fortsatt rapporterer den som ett enkelt volum, ettersom filsystemdriveren foran den kanskje ikke implementerer omdøping på samme måte; hvis driftsmålet ditt lagrer over en nettverkssti, er det verdt å teste et tvunget avbrudd der spesifikt i stedet for å anta at lokal-disk-oppførselen overføres. Og hele mekanismen er avgrenset til å lagre inn i en navngitt fil. Kall SaveAs mot en TStream i stedet, og HotXLS skriver inn i hvilken som helst strøm du ga den direkte, uten noen destinasjonsfil å iscenesette eller beskytte, fordi holdbarheten til den strømmen (en minnebuffer, en nettverksopplasting, en database-blob) er fullstendig din kodes ansvar fra det punktet

En verifiseringspassering får lov til å stole på nøyaktig denne garantien etterpå, inkludert den typen bygget inn i en arbeidsbok-revisjons- og konverterings-arbeidsbenk: en fil åpnet på nytt som kommer tilbake for kort eller manglende, er et ekte konverteringsproblem å spore opp, aldri en lagring som ble avbrutt halvveis og etterlot noe tvetydig på disk. Krasjsikre iscenesatte skrivinger er bygget inn i SaveAs for hver eneste XLSX-, ODS-, og klassisk XLS-arbeidsbok produsert av HotXLS-komponenten for Delphi og C++Builder, uten at noen konfigurasjon kreves for å skru den på