Odborný článok

Štruktúrovaná diagnostika namiesto booleovských výsledkov v HotXLS

Spustite dávkovú konverziu cez desaťtisíc tabuliek cez noc, a do rána sa tri z nich vrátia s False. To je celá posmrtná pitva, akú vám booleovský výsledok uloženia dá: počet zlyhaní, bez čohokoľvek o tom, ktorý súbor, ktorý hárok, alebo ktorá z tuctu možných príčin bola zodpovedná. HotXLS, natívny komponent losLab pre súbory Excelu v Delphi a C++Builder, nahrádza tento jediný bit štruktúrovanou diagnostikou. Rozhranie IXLSWorkbookProgress sprístupňuje zoznam Diagnostics a udalosť OnDiagnostic, ktoré hlásia stabilný číselný kód, úroveň závažnosti, operáciu, ktorá zlyhala, a hárok, kde sa to stalo, pre každé volanie Open, SaveAs a Recalculate

Prečo booleovský výsledok uloženia zlyháva vo veľkom meradle?

Jeden zlyhaný súbor nie je problém, ktorý booleovský výsledok vytvára; tisíc z nich áno. Keď SaveAs vráti niečo iné než úspech pre tri súbory z desaťtisíc, ďalšia otázka je vždy tá istá: sú tieto tri opakovateľné, alebo potrebujú človeka? Chyba oprávnenia na sieťovom disku nie je ten istý incident ako vzorec, ktorý výpočtový engine nedokáže vyhodnotiť, a ani jedno z toho nie je to isté ako hárok, ktorý ticho prekročil formátový limit. S iba výsledkom prešlo/neprešlo na prácu sa každý z nich stáva identickým support tiketom, a niekto musí otvoriť každý súbor ručne, v Exceli, a hľadieť naň, kým sa príčina nestane zjavnou. Táto ručná triáž je skutočná cena booleovského API, a rastie lineárne s veľkosťou dávky, čo je presne tá vlastnosť, akú od ošetrenia chýb nechcete

Vnútri IXLSWorkbookProgress: čo nesie TXLSDiagnostic

IXLSWorkbookProgress je rozhranie, ktoré HotXLS používa na hlásenie oboch vecí, ako operácia postupuje a čo sa v nej pokazilo, a tieto dve polovice zdieľajú jeden kontrakt z dôvodu: obe sú veci, ktoré dlho bežiace volanie Open, SaveAs, alebo Recalculate potrebuje oznámiť bez vyvolania výnimky uprostred operácie. Polovica priebehu je OnProgress a OnProgressEx, ktoré sa vyvolajú s fázou, stavom a dvojicou aktuálne/celkovo. Polovica diagnostiky je tá, o ktorej je tento článok: vlastnosť Diagnostics, ktorá vráti zoznam TXLSDiagnostics, skratka LastDiagnostic pre najnovší záznam a udalosť OnDiagnostic, ktorá sa vyvolá vo chvíli, keď sa vytvorí každý záznam TXLSDiagnostic. Každý záznam nesie číselný Code, TXLSDiagnosticSeverity, TXLSDiagnosticOperation, ktorá ho vyprodukovala, čitateľnú Message, SheetIndex a SheetName, a NativeCode, ktorý zachováva akúkoľvek nižšie-úrovňovú návratovú hodnotu, ktorá záznam spustila

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;

Čítanie Diagnostics takto už samo osebe poráža booleovský výsledok, pretože Code a SheetName menia záhadu na konkrétny, filtrovateľný fakt. Záznam TXLSDiagnostic siaha ďalej, než čo tento príklad vypisuje: RecordId a StreamOffset existujú pre forenznú analýzu na úrovni bajtov vnútri prúdu BIFF, a PartName drží položku zipu OOXML, ako xl/worksheets/sheet3.xml, z ktorej problém pochádzal. Oplatí sa vedieť skôr, než okolo nich postavíte nástroje: v aktuálnom vydaní žiadne z vstavaných miest volania diagnostiky nenapĺňa RecordId ani StreamOffset, takže oboje zostáva pri svojej predvolenej hodnote konštruktora -1, čo znamená "neaplikovateľné", nie "nula". Berte ich neprítomnosť ako normálnu, nie ako chybu vo vašom handleri

Dva enginy, jeden tvar, jeden tichý rozdiel

HotXLS dodáva dva enginy za týmto istým reportovacím modelom, fasádu BIFF8 pre staršie súbory .xls a fasádu OOXML pre .xlsx, a nesprístupňujú IXLSWorkbookProgress identicky. TXLSWorkbook, engine pre .xls, formálne implementuje IXLSWorkbookProgress, takže sa dá odovzdať kdekoľvek, kde sa očakáva tento typ rozhrania. TXLSXWorkbook, engine pre .xlsx, sprístupňuje tie isté členy Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress a OnProgressEx s identickými názvami a typmi, no ako obyčajná trieda, nie ako formálna implementácia tohto rozhrania, takže sama osebe neuspokojí parameter typu IXLSWorkbookProgress. V praxi na tom málokedy záleží, pretože väčšina kódu pracuje naraz s jednou konkrétnou triedou zošita, no znamená to, že nemôžete napísať jediného pomocníka typovaného na IXLSWorkbookProgress a odovzdať mu objekt zošita ktoréhokoľvek enginu zameniteľne. Jediný rozdiel v poli, ktorý priamo vyplýva z rozdelenia formátov, je PartName: napĺňa ho iba engine XLSX, pretože iba OOXML má zip časti, ktoré treba pomenovať

Čo robí diagnostický kód niečím, na čom sa dá bezpečne vetviť?

Pole Code je jediná časť diagnostiky, oproti ktorej sa oplatí natvrdo napísať porovnanie; Message nie, pretože próza je presne to, čo sa v neskoršom vydaní preformuluje, znova preloží, alebo rozšíri o viac detailov bez toho, aby to niekto považoval za rušivú zmenu. Vstavané diagnostické kódy HotXLS už čítajú tak, akoby boli navrhnuté s týmto rozlíšením na pamäti: kódy súvisiace s ukladaním bežia od 1000 do 1005, kódy súvisiace s otváraním sedia na 1100 a 1101, kódy súvisiace s výpočtom na 1200 a 1201, a kód nepodporovaného formátu na 1300, s medzerami ponechanými vnútri každého pásma namiesto toho, aby kódy bežali po sebe naprieč všetkými. Práve tento rozostup je to, čo umožňuje dodávateľovi pridať nový režim zlyhania pri ukladaní, povedzme, na 1006, bez preusporiadania kódov, na ktorých už závisí váš príkaz switch, a oplatí sa to skontrolovať pri akomkoľvek API pre diagnostiku ešte predtým, než sa v produkcii zaviažete porovnávať podľa kódu, nielen pri tomto jednom. Nechajte si vo vlastnej rozosielacej logike predvolenú vetvu bez ohľadu na to, aké stabilné číslovanie vyzerá, pretože nové režimy zlyhania sú presne to, čo vyvíjajúci sa parser alebo zapisovač neustále objavuje. NativeCode a ExceptionClass sedia o vrstvu nižšie než Code pre chvíle, keď potrebujete eskalovať: NativeCode zachováva podkladovú návratovú hodnotu, medzi nimi aj HRESULT z volania Structured Storage, a ExceptionClass zaznamenáva typ výnimky Delphi, keď bola zapojená, čo zvyčajne stačí na otvorenie presnej požiadavky na podporu bez pripájania celého stack trace

Závažnosť a operácia rozhodujú, čo váš kód urobí ďalej

Závažnosť a operácia sú to, čo mení diagnostiku z riadka logu na routovacie rozhodnutie. TXLSDiagnosticSeverity beží Info, Warning, Error a Fatal, a TXLSDiagnosticOperation označuje každý záznam volaním, ktoré ho vyprodukovalo: Open, Save, Calculate, alebo Export. Tieto dve osi sú zámerne nezávislé: xlsDiagnosticUnhandledException je jeden pevný kód, ktorý sa vyvolá s Operation nastaveným na to volanie, ktoré ho v skutočnosti vyvolalo, takže Code odpovedá, čo sa pokazilo, zatiaľ čo Operation samostatne odpovedá kde, namiesto toho, aby bol potrebný odlišný kód pre výnimku počas otvárania oproti počas ukladania. Táto skladateľnosť je aj to, čo robí routovanie mechanickým: zalogovať varovanie a pokračovať ďalej, uloženie zrušené cez príznak Aborted je typický príklad; spočítať chybu a nechať dávku bežať ďalej, hárok, ktorý sa nepodarilo serializovať, je typický príklad; zastaviť dávku pri závažnosti fatal, pretože táto úroveň znamená, že neošetrená výnimka už rozvinula volanie a pokračovanie riskuje prácu z polovične aktualizovaného stavu. Jedna čestná výhrada: Info existuje vo výčtovom type ako predvolená hodnota, s akou čerstvý TXLSDiagnostic začína, no každé miesto volania diagnostiky vstavané do dnešného vydania HotXLS vyvoláva iba Warning, Error, alebo Fatal; Info je vyhradené na budúce použitie, nie niečo, čo engine dnes vydáva

// 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;

Zapojenie OnDiagnostic do dávkovej pipeline

Opakované dopytovanie Diagnostics po každom volaní funguje pre jeden súbor; prestane fungovať vo chvíli, keď sa vrátite k tej celonočnej dávke desaťtisíc súborov, pretože Diagnostics sa vymaže na začiatku každého volania Open, SaveAs, a Recalculate. Prečítajte si ho po treťom súbore v slučke a uvidíte iba diagnostiku tretieho súboru; čokoľvek, čo hlásili prvé dva súbory, je už preč. OnDiagnostic to rieši tým, že premení kolekciu na prúd: prihláste sa raz pred začiatkom slučky, a ten istý handler sa vyvolá pre každý súbor, v poradí, s názvom súboru stále v rozsahu platnosti cez inštančné pole

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;

Čo callback v skutočnosti stojí

OnDiagnostic je lacný zo štrukturálneho dôvodu: vyvolá sa iba vtedy, keď je už niečo zle, a zle je zriedkavé v porovnaní s počtom buniek, riadkov, alebo hárkov, aké zošit drží. Porovnajte to s OnProgress a OnProgressEx, ktoré hlásia bežný priebeh a od začiatku ich bolo treba navrhnúť okolo frekvencie volaní. HotXLS vyvoláva priebeh na úrovni hárka raz za hárok počas Open a SaveAs, nie raz za bunku či riadok, čo je to, čo drží réžiu na volanie malú aj pri zošitoch s miliónmi buniek; Recalculate ide ďalej a obmedzuje vlastnú udalosť priebehu zhruba na každé štyri percentá grafu závislostí, takže plný prepočet vám dáva srdcový tep namiesto toho, aby zaplavil vaše vlákno UI udalosťami. Diagnostika nepotrebovala žiadne takéto obmedzovanie, pretože počet udalostí je ohraničený počtom skutočných problémov, nie veľkosťou súboru

Jediné miesto, kde výkon stále závisí od vás, je vnútri samotného handlera. OnDiagnostic sa vyvolá synchrónne, na vlákne, ktoré beží Open, SaveAs, alebo Recalculate, takže handler, ktorý blokuje, napríklad synchrónny zápis do vzdialenej logovacej služby, sa stáva súčasťou reálneho času tohto volania. Pre jeden súbor je to neviditeľné. Vynásobené naprieč dávkou desaťtisíc súborov je to rozdiel medzi úlohou, ktorá sa dokončí cez noc, a takou, ktorá ešte beží na obed, takže si napufrujte, čo handler potrebuje urobiť, a vyprázdnite to asynchrónne namiesto toho, aby ste pomalú časť robili priamo v ňom

Štruktúrovaná diagnostika je najcennejšia presne tam, kde je booleovský výsledok najslabší, v pracovných postupoch, ktoré sa dotýkajú mnohých súborov namiesto jedného. Pipeline na audit a konverziu zošitov je najjasnejší príklad: namiesto zaznamenania holého prešlo/neprešlo na súbor priložte zoznam Diagnostics každého súboru k jeho auditnému záznamu, a report vám povie nielen čo zlyhalo, ale aj prečo, čo je väčšina toho, o čo sa v prvom rade snaží náš článok o stavbe pracovnej stanice na audit a konverziu zošitov. To isté párovanie priebehu a diagnostiky patrí aj do akéhokoľvek pracovného postupu, ktorý už kvôli sebe samému potrebuje hlásenie priebehu, čo je presne územie pokryté v našom sprievodcovi výkonom veľkých zošitov v HotXLS, kde je dlhé volanie Open alebo SaveAs dosť bežné na to, že OnProgress je už zapojený a OnDiagnostic je prirodzený, takmer bezplatný doplnok vedľa neho

Nič z toho nevyžaduje mať Excel nainštalovaný kdekoľvek v pipeline, a nič z toho nevyžaduje chytanie všeobecnej výnimky a hádanie, čo znamenala. IXLSWorkbookProgress a jeho členy Diagnostics, LastDiagnostic a OnDiagnostic sú súčasťou štandardného komponentu HotXLS pre Delphi a C++Builder, spolu s plnou referenciou diagnostických kódov a zvyškom plochy Open, SaveAs, a Recalculate, ktorú tento článok prechádzal