Technický článek

Strukturovaná diagnostika místo booleovských výsledků v HotXLS

Spusťte přes noc dávkovou konverzi na deset tisíc tabulek a do rána se tři z nich vrátí jako False. To je celá posmrtná zpráva, kterou vám dá booleovský výsledek uložení: počet selhání, bez čehokoli o tom, který soubor, který list, nebo která z tuctu možných příčin za to mohla. HotXLS, nativní komponenta losLab pro soubory Excelu v Delphi a C++Builderu, nahrazuje tento jediný bit strukturovanou diagnostikou. Rozhraní IXLSWorkbookProgress vystavuje seznam Diagnostics a událost OnDiagnostic, které pro každé volání Open, SaveAs a Recalculate hlásí stabilní číselný kód, úroveň závažnosti, operaci, která selhala, a list, kde se to stalo

Proč booleovský výsledek uložení selhává ve velkém měřítku?

Problém, který booleovský výsledek vytváří, není jeden selhaný soubor; je to tisíc z nich. Když SaveAs u tří souborů z deseti tisíc vrátí něco jiného než úspěch, další otázka je vždy stejná: jsou tyto tři opakovatelné, nebo potřebují člověka? Chyba oprávnění na síťovém sdílení není stejný incident jako vzorec, který kalkulační engine nedokáže vyhodnotit, a to není stejné ani jako list, který tiše překročil limit formátu. S k dispozici jen výsledkem projít/neprojít se z každého z nich stane identický lístek podpory a někdo musí každý soubor otevřít ručně, v Excelu, a zírat na něj, dokud se příčina nestane zjevnou. Tato ruční triáž je skutečná cena booleovského API a škáluje lineárně s velikostí dávky, což je přesně ta vlastnost, kterou od zpracování chyb nechcete

Uvnitř IXLSWorkbookProgress: co nese TXLSDiagnostic

IXLSWorkbookProgress je rozhraní, které HotXLS používá k hlášení jak toho, jak operace postupuje, tak toho, co se uvnitř ní pokazilo, a obě poloviny sdílejí jednu smlouvu z určitého důvodu: obě jsou věci, které dlouho běžící volání Open, SaveAs nebo Recalculate potřebuje sdělit, aniž by uprostřed operace vyvolalo výjimku. Polovina pro průběh je OnProgress a OnProgressEx, vyvolávaná s fází, stavem a dvojicí aktuální/celkem. Polovina pro diagnostiku je ta, o které je tento článek: vlastnost Diagnostics, která vrátí seznam TXLSDiagnostics, zkratka LastDiagnostic pro nejnovější záznam, a událost OnDiagnostic, která se vyvolá v okamžiku vytvoření každého záznamu TXLSDiagnostic. Každý záznam nese číselný Code, TXLSDiagnosticSeverity, TXLSDiagnosticOperation, která jej vyprodukovala, čitelnou Message, SheetIndex a SheetName, a NativeCode, který zachovává jakoukoli nižší úrovňovou návratovou hodnotu, která záznam vyvolala

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;

Číst Diagnostics takto už samo o sobě předčí booleovský výsledek, protože Code a SheetName promění záhadu na konkrétní, filtrovatelný fakt. Záznam TXLSDiagnostic jde dál, než co tento příklad vypisuje: RecordId a StreamOffset existují pro forenzní analýzu na úrovni bajtů uvnitř proudu BIFF a PartName drží zip záznam OOXML, jako xl/worksheets/sheet3.xml, ze kterého problém pochází. Stojí za znalost dřív, než kolem nich postavíte nástroje: v aktuálním vydání žádné z vestavěných diagnostických míst volání nenaplňuje RecordId ani StreamOffset, takže obě zůstávají na svém výchozím konstruktoru -1, což znamená „nevztahuje se", ne „nula". Berte jejich nepřítomnost jako normální, ne jako chybu ve vašem obsluhovači

Dva enginy, jeden tvar, jeden tichý rozdíl

HotXLS dodává za tímto stejným modelem hlášení dva enginy, fasádu BIFF8 pro starší soubory .xls a fasádu OOXML pro .xlsx, a nevystavují IXLSWorkbookProgress identicky. TXLSWorkbook, engine pro .xls, formálně implementuje IXLSWorkbookProgress, takže jej lze předat kamkoli, kde se očekává tento typ rozhraní. TXLSXWorkbook, engine pro .xlsx, vystavuje stejné členy Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress a OnProgressEx s identickými jmény a typy, ale jako obyčejná třída, ne jako formální implementace tohoto rozhraní, takže sám o sobě nesplní parametr IXLSWorkbookProgress. V praxi na tom málokdy záleží, protože většina kódu pracuje s jednou konkrétní třídou sešitu najednou, ale znamená to, že nemůžete napsat jednu pomocnou funkci typovanou na IXLSWorkbookProgress a předat jí objekt sešitu kteréhokoli enginu zaměnitelně. Jediný rozdíl v poli, který přímo vyplývá z rozdělení formátů, je PartName: naplňuje jej jen engine XLSX, protože jen OOXML má zip části k pojmenování

Co dělá diagnostický kód něčím, na co lze bezpečně větvit?

Pole Code je jediná část diagnostiky, na kterou se vyplatí natvrdo napsat porovnání; Message ne, protože prostý text je přesně to, co se v pozdějším vydání přeformuluje, přeloží znovu, nebo rozšíří o víc podrobností, aniž by to kdokoli považoval za změnu porušující kompatibilitu. Vestavěné diagnostické kódy HotXLS už čtou, jako by byly navrženy s tímto rozlišením na paměti: kódy vztahující se k uložení běží 1000 až 1005, kódy vztahující se k otevření sedí na 1100 a 1101, kódy vztahující se k výpočtu na 1200 a 1201 a kód nepodporovaného formátu na 1300, s mezerami ponechanými uvnitř každého pásma místo toho, aby kódy běžely postupně napříč všemi. Toto rozestupování je to, co dovoluje dodavateli přidat nový režim selhání při ukládání třeba na 1006, aniž by musel přečíslovat kódy, na kterých už závisí váš switch — a vyplatí se to zkontrolovat u jakéhokoli API pro diagnostiku dřív, než se v produkci zavážete k porovnávání podle kódu, ne jen u tohoto jednoho. Podržte si výchozí větev ve vlastní dispatchovací logice bez ohledu na to, jak stabilní číslování vypadá, protože nové režimy selhání jsou přesně to, co vyvíjející se parser nebo zapisovač neustále objevuje. NativeCode a ExceptionClass sedí o vrstvu pod Code pro chvíle, kdy potřebujete eskalovat: NativeCode zachovává podkladovou návratovou hodnotu, mimo jiné HRESULT z volání Structured Storage, a ExceptionClass zaznamenává typ výjimky Delphi, když byla zapojena nějaká, což obvykle stačí k otevření přesného požadavku na podporu bez přiložení celého stack trace

Závažnost a operace rozhodují, co váš kód udělá dál

Závažnost a operace jsou to, co promění diagnostiku z řádku v logu na rozhodnutí o směrování. TXLSDiagnosticSeverity běží Info, Warning, Error a Fatal, a TXLSDiagnosticOperation označuje každý záznam voláním, které jej vyprodukovalo: Open, Save, Calculate nebo Export. Obě osy jsou záměrně nezávislé: xlsDiagnosticUnhandledException je jeden pevný kód, který se vyvolá s Operation nastavenou na jakékoli volání, které jej skutečně vyvolalo, takže Code odpovídá na to, co se pokazilo, zatímco Operation samostatně odpovídá na to, kde, místo toho, aby byl potřeba odlišný kód pro výjimku při otevírání oproti té při ukládání. Tato skladatelnost je také to, co dělá směrování mechanickým: zalogovat varování a jít dál, uložení zrušené přes příznak Aborted je typický příklad; spočítat chybu a nechat dávku běžet dál, list, který se nepodařilo serializovat, je typický příklad; zastavit dávku při závažnosti fatal, protože tato úroveň znamená, že nezachycená výjimka už odvinula volání a pokračování riskuje práci z napůl aktualizovaného stavu. Jedna poctivá výhrada: Info existuje ve výčtu jako výchozí hodnota, se kterou začíná čerstvý TXLSDiagnostic, ale každé diagnostické místo volání vestavěné v dnešním vydání HotXLS kdy vyvolá jen Warning, Error, nebo Fatal; Info je rezervováno pro budoucí použití, ne něco, co engine dnes vydává

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

Zapojení OnDiagnostic do dávkové pipeline

Pollovat Diagnostics po každém volání funguje pro jeden soubor; přestane fungovat, jakmile se vrátíte k té přesnoční dávce deseti tisíc souborů, protože Diagnostics se vyčistí na začátku každého volání Open, SaveAs a Recalculate. Přečtěte si to po třetím souboru ve smyčce a uvidíte jen diagnostiku třetího souboru; cokoli hlásily první dva soubory je už pryč. OnDiagnostic to řeší tím, že promění kolekci na stream: přihlaste se k odběru jednou před začátkem smyčky, a stejný obsluhovač se vyvolá pro každý soubor, po pořádku, s názvem souboru pořád v rozsahu platnosti přes instanč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;

Co zpětné volání skutečně stojí

OnDiagnostic je levné ze strukturálního důvodu: vyvolá se jen tehdy, když už je něco špatně, a špatně je vzácné ve srovnání s počtem buněk, řádků nebo listů, které sešit drží. Porovnejte to s OnProgress a OnProgressEx, které hlásí běžný postup a musely se od začátku navrhovat s ohledem na frekvenci volání. HotXLS vyvolává průběh na úrovni listu jednou za list během Open a SaveAs, ne jednou za buňku nebo řádek, což je to, co udržuje režii na volání malou i u sešitů s miliony buněk; Recalculate jde dál a přiškrcuje vlastní událost postupu na zhruba každá čtyři procenta grafu závislostí, takže plný přepočet vám dá tep místo zaplavení vlákna UI událostmi. Diagnostika žádné takové přiškrcení nepotřebovala, protože počet událostí je omezený počtem skutečných problémů, ne velikostí souboru

Jediné místo, kde výkon pořád závisí na vás, je uvnitř samotného obsluhovače. OnDiagnostic se vyvolává synchronně, na vlákně, které pouští Open, SaveAs, nebo Recalculate, takže obsluhovač, který blokuje, třeba synchronní zápis do vzdálené logovací služby, se stane součástí doby trvání tohoto volání. U jednoho souboru je to neviditelné. Vynásobené přes dávku deseti tisíc souborů je to rozdíl mezi úlohou, která doběhne přes noc, a tou, která ještě běží v poledne, takže si pufrujte, co obsluhovač potřebuje udělat, a vyprázdněte to asynchronně místo toho, abyste pomalou část dělali přímo v místě volání

Strukturovaná diagnostika je nejcennější přesně tam, kde je booleovský výsledek nejslabší, v pracovních postupech, které se dotýkají mnoha souborů místo jednoho. Pipeline auditu a konverze sešitů je nejjasnější příklad: místo záznamu holého projít/neprojít na soubor připojte seznam Diagnostics každého souboru k jeho záznamu auditu, a report vám řekne nejen co selhalo, ale proč, což je z velké části to, o co se snaží náš článek o stavbě pracovního nástroje pro audit a konverzi sešitů. Stejné párování postupu a diagnostiky patří i do každého pracovního postupu, který už potřebuje hlášení postupu pro svůj vlastní účel, což je přesně území popsané v našem průvodci výkonem velkých sešitů v HotXLS, kde je dlouhé volání Open nebo SaveAs dost běžné na to, že OnProgress je už zapojený a OnDiagnostic je přirozeným, téměř zadarmo dodaným doplňkem vedle něj

Nic z toho nevyžaduje mít kdekoli v pipeline nainstalovaný Excel, a nic z toho nevyžaduje zachytit obecnou výjimku a hádat, co znamenala. IXLSWorkbookProgress a jeho členy Diagnostics, LastDiagnostic a OnDiagnostic jsou součástí standardní komponenty HotXLS pro Delphi a C++Builder, spolu s úplnou referencí diagnostických kódů a zbytkem plochy Open, SaveAs a Recalculate, kterou tento článek procházel