Teknisk artikel

Strukturerad diagnostik i stället för booleska resultat i HotXLS

Kör en batchkonvertering över tio tusen kalkylblad över natten, och till morgonen kommer tre av dem tillbaka False. Det är hela obduktionen ett booleskt sparresultat ger dig: ett antal misslyckanden, utan något om vilken fil, vilket blad, eller vilken av ett dussin möjliga orsaker som var ansvarig. HotXLS, losLabs native komponent för Excel-filer för Delphi och C++Builder, ersätter den enda biten med strukturerad diagnostik. Gränssnittet IXLSWorkbookProgress exponerar en Diagnostics-lista och en OnDiagnostic-händelse som rapporterar en stabil numerisk kod, en allvarlighetsnivå, operationen som misslyckades, och bladet där det hände, för varje Open-, SaveAs-, och Recalculate-anrop

Varför misslyckas ett booleskt sparresultat i stor skala?

En enda misslyckad fil är inte problemet ett booleskt resultat skapar; tusen av dem är. När SaveAs returnerar något annat än lyckat för tre filer av tio tusen är nästa fråga alltid densamma: går dessa tre att försöka igen, eller behöver de en människa? Ett behörighetsfel på en nätverksresurs är inte samma incident som en formel beräkningsmotorn inte kan utvärdera, och ingen av dem är samma som ett kalkylblad som tyst överskred en formatgräns. Med bara ett godkänt/underkänt-resultat att arbeta med blir var och en av dessa ett identiskt supportärende, och någon måste öppna varje fil för hand, i Excel, och stirra på den tills orsaken blir uppenbar. Den manuella triagen är den verkliga kostnaden för ett booleskt API, och den skalar linjärt med batchens storlek, vilket är exakt den egenskap du inte vill ha från felhantering

Inuti IXLSWorkbookProgress: vad en TXLSDiagnostic bär

IXLSWorkbookProgress är gränssnittet HotXLS använder för att rapportera både hur en operation går och vad som gick fel inuti den, och de två halvorna delar ett kontrakt av en anledning: båda är saker ett långvarigt Open-, SaveAs-, eller Recalculate-anrop behöver kommunicera utan att kasta ett undantag mitt i operationen. Framstegshalvan är OnProgress och OnProgressEx, som utlöses med en fas, ett tillstånd, och ett aktuellt/totalt-par. Diagnostikhalvan är den den här artikeln handlar om: en Diagnostics-egenskap som returnerar en TXLSDiagnostics-lista, en LastDiagnostic-genväg till den senaste posten, och en OnDiagnostic-händelse som utlöses i samma stund varje TXLSDiagnostic-post skapas. Varje post bär en numerisk Code, en TXLSDiagnosticSeverity, den TXLSDiagnosticOperation som producerade den, ett läsbart Message, ett SheetIndex och SheetName, och en NativeCode som bevarar vilket lägre-nivå-returvärde som än utlöste posten

var
  Book: TXLSXWorkbook;
  Diag: TXLSDiagnostic;
  I: Integer;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.SaveAs('quarterly-report.xlsx') <> 1 then
      for I := 0 to Book.Diagnostics.Count - 1 do
      begin
        Diag := Book.Diagnostics[I];
        Writeln(Format('[%d] severity=%d sheet="%s": %s',
          [Diag.Code, Ord(Diag.Severity), Diag.SheetName, Diag.Message]));
      end;
  finally
    Book.Free;
  end;
end;

Att läsa Diagnostics så här slår redan ett booleskt resultat på egen hand, eftersom Code och SheetName gör om ett mysterium till ett specifikt, filtrerbart faktum. TXLSDiagnostic-posten sträcker sig längre än vad det här exemplet skriver ut: RecordId och StreamOffset finns för byte-nivå-forensik inuti en BIFF-ström, och PartName håller OOXML-zip-posten, som xl/worksheets/sheet3.xml, som ett problem kom ifrån. Värt att känna till innan du bygger verktyg kring dem: i den aktuella versionen fyller inget av de inbyggda diagnostikanropsställena i RecordId eller StreamOffset, så båda förblir på sitt konstruktörsstandardvärde -1, vilket betyder "inte tillämpligt" snarare än "noll". Behandla deras frånvaro som normalt, inte som en bugg i din hanterare

Två motorer, en form, en tyst skillnad

HotXLS levererar två motorer bakom samma rapporteringsmodell, en BIFF8-fasad för legacy-.xls-filer och en OOXML-fasad för .xlsx, och de exponerar inte IXLSWorkbookProgress identiskt. TXLSWorkbook, .xls-motorn, implementerar formellt IXLSWorkbookProgress, så den kan skickas var som helst det gränssnittstypen förväntas. TXLSXWorkbook, .xlsx-motorn, exponerar samma Diagnostics-, LastDiagnostic-, OnDiagnostic-, OnProgress-, och OnProgressEx-medlemmar med identiska namn och typer, men som en vanlig klass snarare än en formell implementation av det gränssnittet, så den kommer inte att uppfylla en IXLSWorkbookProgress-parameter på egen hand. I praktiken spelar detta sällan roll, eftersom det mesta kod arbetar mot en konkret arbetsboksklass åt gången, men det innebär att du inte kan skriva en enda hjälpfunktion typad till IXLSWorkbookProgress och lämna den endera motorns arbetsboksobjekt utbytbart. Den enda fältskillnaden som följer direkt av formatuppdelningen är PartName: bara XLSX-motorn fyller i den, eftersom bara OOXML har zip-delar att namnge

Vad gör en diagnostikkod något du säkert kan förgrena på?

Code-fältet är den enda delen av en diagnostik värd att hårdkoda en jämförelse mot; Message är det inte, eftersom prosa är exakt det slags sak som blir omformulerad, omöversatt, eller utökad med mer detalj i en senare version utan att någon behandlar det som en brytande ändring. HotXLS:s inbyggda diagnostikkoder läser redan som om de designades med den distinktionen i åtanke: sparrelaterade koder går 1000 till 1005, öppningsrelaterade koder sitter på 1100 och 1101, beräkningsrelaterade koder på 1200 och 1201, och en ostödd-format-kod på 1300, med luckor lämnade inuti varje band snarare än att koderna löper konsekutivt över alla dem. Den mellanrumsindelningen är vad som låter en leverantör lägga till ett nytt sparfelläge vid, säg, 1006 utan att omnumrera koderna din switch-sats redan beror på, och det är värt att kontrollera för i vilket diagnostik-API som helst innan du binder dig till att matcha på en kod i produktion, inte bara det här. Behåll en standardgren i din egen dispatch-logik oavsett hur stabil numreringen ser ut, eftersom nya felläge är exakt vad en motor under utveckling fortsätter upptäcka. NativeCode och ExceptionClass sitter ett lager under Code för när du behöver eskalera: NativeCode bevarar det underliggande returvärdet, en HRESULT från ett Structured-Storage-anrop bland dem, och ExceptionClass registrerar Delphi-undantagstypen när en var inblandad, vilket vanligtvis räcker för att öppna en precis supportförfrågan utan att bifoga en full stacktrace

Allvarlighetsgrad och operation avgör vad din kod gör härnäst

Allvarlighetsgrad och operation är vad som gör om en diagnostik från en loggrad till ett routingbeslut. TXLSDiagnosticSeverity går Info, Warning, Error, och Fatal, och TXLSDiagnosticOperation märker varje post med anropet som producerade den: Open, Save, Calculate, eller Export. De två axlarna är oberoende av design: xlsDiagnosticUnhandledException är en fast kod som utlöses med Operation satt till vilket anrop som än faktiskt kastade den, så Code svarar på vad som gick fel medan Operation separat svarar på var, i stället för att behöva en distinkt kod för ett undantag under öppning kontra ett under sparning. Den komponerbarheten är också vad som gör routing mekanisk: logga en varning och fortsätt, en sparning avbruten via Aborted-flaggan är ett typiskt exempel; räkna ett fel och fortsätt köra batchen, ett kalkylblad som misslyckades med serialisering är ett typiskt exempel; stoppa batchen vid en fatal allvarlighetsgrad, eftersom den nivån betyder att ett ohanterat undantag redan har rullat tillbaka anropet och att fortsätta riskerar att arbeta från ett halvuppdaterat tillstånd. En ärlig brasklapp: Info finns i enumen som standardvärdet en ny TXLSDiagnostic börjar med, men varje diagnostikanropsställe inbyggt i dagens HotXLS-version utlöser bara någonsin Warning, Error, eller Fatal; Info är reserverad för framtida bruk, inte något motorn avger idag

// same Diagnostics loop as above, routed by severity instead of printed flat:
for I := 0 to Book.Diagnostics.Count - 1 do
begin
  Diag := Book.Diagnostics[I];
  case Diag.Severity of
    xlsDiagnosticWarning:
      Writeln(Format('WARN  [%d] %s', [Diag.Code, Diag.Message]));
    xlsDiagnosticError:
      begin
        Writeln(Format('ERROR [%d] %s (sheet %s, native %d)',
          [Diag.Code, Diag.Message, Diag.SheetName, Diag.NativeCode]));
        Inc(FailedSheetCount);
      end;
    xlsDiagnosticFatal:
      raise Exception.CreateFmt('Fatal HotXLS diagnostic %d: %s', [Diag.Code, Diag.Message]);
  end;
end;

Att koppla in OnDiagnostic i en batch-pipeline

Att polla Diagnostics efter varje anrop fungerar för en enda fil; det slutar fungera så fort du är tillbaka till den där batchen på tio tusen filer över natten, eftersom Diagnostics rensas i början av varje Open-, SaveAs-, och Recalculate-anrop. Läs den efter den tredje filen i en loop och du ser bara den tredje filens diagnostik; vad de första två filerna rapporterade är redan borta. OnDiagnostic löser det genom att göra om samlingen till en ström: prenumerera en gång innan loopen börjar, och samma hanterare utlöses för varje fil, i ordning, med filnamnet fortfarande i scope via ett instansfält

type
  TBatchConverter = class
  private
    FCurrentFile: string;
    FFailedFiles: TStringList;
    procedure HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
  end;

procedure TBatchConverter.HandleDiagnostic(Sender: TObject; Diagnostic: TXLSDiagnostic);
begin
  if Diagnostic.Severity >= xlsDiagnosticError then
    FFailedFiles.Add(Format('%s: [%d] %s (sheet %s)',
      [FCurrentFile, Diagnostic.Code, Diagnostic.Message, Diagnostic.SheetName]));
end;

// inside the batch loop:
Book.OnDiagnostic := HandleDiagnostic;
for I := 0 to FileNames.Count - 1 do
begin
  FCurrentFile := FileNames[I];
  if Book.Open(FCurrentFile) = 1 then
    Book.SaveAs(ChangeFileExt(FCurrentFile, '.xlsx'));
end;

Vad callbacken faktiskt kostar

OnDiagnostic är billig av en strukturell anledning: den utlöses bara när något redan är fel, och fel är sällsynt jämfört med antalet celler, rader, eller kalkylblad en arbetsbok innehåller. Kontrastera det med OnProgress och OnProgressEx, som rapporterar rutinmässiga framsteg och behövde designas kring anropsfrekvens från början. HotXLS utlöser bladnivå-framsteg en gång per blad under Open och SaveAs, inte en gång per cell eller rad, vilket är vad som håller overheaden per anrop liten även på arbetsböcker med miljontals celler; Recalculate går längre och strypar sin egen framstegshändelse till ungefär var fjärde procent av beroendegrafen, så en fullständig omberäkning ger dig en hjärtslagsignal i stället för att översvämma din UI-tråd med händelser. Diagnostik behövde ingen sådan strypning, eftersom händelseantalet begränsas av antalet faktiska problem, inte av filens storlek

Det enda stället prestanda fortfarande beror på dig är inuti själva hanteraren. OnDiagnostic utlöses synkront, på tråden som kör Open, SaveAs, eller Recalculate, så en hanterare som blockerar, en synkron skrivning till en fjärrloggningstjänst till exempel, blir en del av det anropets väggklockstid. För en enda fil är det osynligt. Multiplicerat över en batch på tio tusen filer är det skillnaden mellan ett jobb som blir klart över natten och ett som fortfarande körs vid lunch, så buffra vad hanteraren behöver göra och flusha det asynkront i stället för att göra den långsamma delen inline

Strukturerad diagnostik är som mest värdefull exakt där ett booleskt resultat är svagast, i arbetsflöden som rör många filer i stället för en. En arbetsboksrevision- och konverteringspipeline är det tydligaste exemplet: i stället för att registrera ett rakt godkänt/underkänt per fil, fäst varje fils Diagnostics-lista till dess revisionspost, och rapporten berättar inte bara vad som misslyckades utan varför, vilket är det mesta av vad vår artikel om att bygga en arbetsboksrevision- och konverteringsarbetsbänk försöker få rätt från början. Samma ihopparning av framsteg och diagnostik hör också hemma i vilket arbetsflöde som helst som redan behöver framstegsrapportering för sin egen skull, vilket är exakt det territorium som täcks i vår guide till stora arbetsboksprestanda i HotXLS, där ett långt Open- eller SaveAs-anrop är vanligt nog att OnProgress redan är inkopplad och OnDiagnostic är ett naturligt, nästan gratis tillägg bredvid den

Inget av detta kräver Excel installerat någonstans i pipelinen, och inget av det kräver att fånga ett generiskt undantag och gissa vad det betydde. IXLSWorkbookProgress och dess Diagnostics-, LastDiagnostic-, och OnDiagnostic-medlemmar är en del av standardversionen av HotXLS-komponenten för Delphi och C++Builder, tillsammans med den fullständiga diagnostikkodreferensen och resten av Open-, SaveAs-, och Recalculate-ytan den här artikeln har gått igenom