Technisch artikel

Gestructureerde diagnostiek in plaats van boolean-resultaten in HotXLS

Voer 's nachts een batchconversie uit over tienduizend spreadsheets, en tegen de ochtend komen er drie terug als False. Dat is het volledige postmortem dat een boolean opslagresultaat u geeft: een aantal mislukkingen, zonder enige informatie over welk bestand, welk blad, of welke van een dozijn mogelijke oorzaken verantwoordelijk was. HotXLS, losLab's native Delphi- en C++Builder-component voor Excel-bestanden, vervangt dat ene bit door gestructureerde diagnostiek. De interface IXLSWorkbookProgress ontsluit een Diagnostics-lijst en een OnDiagnostic-gebeurtenis die een stabiele numerieke code, een ernstniveau, de bewerking die faalde, en het blad waar het gebeurde rapporteren, voor elke Open-, SaveAs-, en Recalculate-aanroep

Waarom faalt een boolean opslagresultaat op schaal?

Eén mislukt bestand is niet het probleem dat een boolean resultaat creëert; duizend ervan zijn dat wel. Wanneer SaveAs iets anders dan succes teruggeeft voor drie van de tienduizend bestanden, is de volgende vraag altijd hetzelfde: zijn deze drie opnieuw te proberen, of hebben ze een mens nodig? Een rechtenfout op een netwerkshare is niet hetzelfde incident als een formule die de rekenengine niet kan evalueren, en geen van beide is hetzelfde als een werkblad dat stilzwijgend een formaatlimiet overschreed. Met alleen een geslaagd/mislukt-resultaat om mee te werken, wordt elk van die een identiek supportticket, en iemand moet elk bestand met de hand openen, in Excel, en ernaar staren totdat de oorzaak duidelijk wordt. Die handmatige triage is de echte kostenpost van een boolean-API, en die schaalt lineair mee met de grootte van de batch, precies de eigenschap die u niet wilt van foutafhandeling

Binnen IXLSWorkbookProgress: wat een TXLSDiagnostic draagt

IXLSWorkbookProgress is de interface die HotXLS gebruikt om te rapporteren zowel hoe een bewerking verloopt als wat er intern misging, en de twee helften delen om een reden één contract: beide zijn dingen die een langlopende Open-, SaveAs-, of Recalculate-aanroep moet communiceren zonder halverwege een uitzondering op te werpen. De voortgangshelft is OnProgress en OnProgressEx, die afgaan met een fase, een status, en een huidig/totaal-paar. De diagnostiekhelft is waar dit artikel over gaat: een Diagnostics-eigenschap die een TXLSDiagnostics-lijst teruggeeft, een LastDiagnostic-snelkoppeling voor de meest recente vermelding, en een OnDiagnostic-gebeurtenis die afgaat op het moment dat elk TXLSDiagnostic-record wordt aangemaakt. Elk record draagt een numerieke Code, een TXLSDiagnosticSeverity, de TXLSDiagnosticOperation die het produceerde, een leesbaar Message, een SheetIndex en SheetName, en een NativeCode die bewaart welke lagere-niveau retourwaarde de vermelding ook triggerde

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;

Diagnostics zo lezen verslaat al op zichzelf een boolean resultaat, omdat Code en SheetName een mysterie omzetten in een specifiek, filterbaar feit. Het TXLSDiagnostic-record reikt verder dan wat dit voorbeeld afdrukt: RecordId en StreamOffset bestaan voor forensisch onderzoek op byteniveau binnen een BIFF-stroom, en PartName bevat de OOXML-zip-vermelding, zoals xl/worksheets/sheet3.xml, waar een probleem vandaan kwam. Het is de moeite waard om te weten voordat u er tooling omheen bouwt: in de huidige release vult geen van de ingebouwde diagnostiek-aanroeppunten RecordId of StreamOffset in, dus beide blijven op hun constructorstandaard van -1 staan, wat "niet van toepassing" betekent in plaats van "nul". Behandel hun afwezigheid als normaal, niet als een bug in uw handler

Twee engines, één vorm, één stil verschil

HotXLS levert twee engines achter dit zelfde rapportagemodel, een BIFF8-gevel voor legacy .xls-bestanden en een OOXML-gevel voor .xlsx, en ze ontsluiten IXLSWorkbookProgress niet identiek. TXLSWorkbook, de .xls-engine, implementeert IXLSWorkbookProgress formeel, dus het kan overal worden doorgegeven waar dat interfacetype wordt verwacht. TXLSXWorkbook, de .xlsx-engine, ontsluit dezelfde Diagnostics-, LastDiagnostic-, OnDiagnostic-, OnProgress-, en OnProgressEx-leden met identieke namen en typen, maar als een gewone klasse in plaats van een formele implementatie van die interface, dus het zal een IXLSWorkbookProgress-parameter niet op zichzelf voldoen. In de praktijk doet dit zelden ertoe, omdat de meeste code met één concrete werkboekklasse tegelijk werkt, maar het betekent wel dat u geen enkele helper kunt schrijven getypeerd op IXLSWorkbookProgress en die het werkboekobject van beide engines onderling uitwisselbaar kunt geven. Het ene veldverschil dat rechtstreeks voortvloeit uit de formaatsplitsing is PartName: alleen de XLSX-engine vult het in, omdat alleen OOXML zip-onderdelen heeft om te benoemen

Wat maakt een diagnostiekcode iets waarop u veilig kunt vertakken?

Het veld Code is het enige onderdeel van een diagnose dat de moeite waard is om een vergelijking tegen hard te coderen; Message is dat niet, omdat proza precies het soort ding is dat wordt geherformuleerd, opnieuw vertaald, of uitgebreid met meer detail in een latere release zonder dat iemand het als een breaking change behandelt. HotXLS's ingebouwde diagnostiekcodes lezen al alsof ze met dat onderscheid in gedachten zijn ontworpen: opslag-gerelateerde codes lopen van 1000 tot en met 1005, open-gerelateerde codes staan op 1100 en 1101, bereken-gerelateerde codes op 1200 en 1201, en een niet-ondersteund-formaat-code op 1300, met gaten binnen elke band gelaten in plaats van dat de codes over hen allen achtereenvolgens lopen. Die spatiëring is wat een leverancier in staat stelt een nieuwe opslagtijd-faalmodus toe te voegen op, zeg, 1006 zonder de codes te hernummeren waarop uw switch-statement al vertrouwt, en het is de moeite waard om dit te controleren in elke diagnostiek-API voordat u zich committeert aan het matchen op een code in productie, niet alleen deze ene. Houd een standaardtak in uw eigen dispatchlogica ongeacht hoe stabiel de nummering eruitziet, omdat nieuwe faalmodi precies zijn wat een evoluerende parser of schrijver blijft ontdekken. NativeCode en ExceptionClass bevinden zich één laag onder Code voor wanneer u moet escaleren: NativeCode bewaart de onderliggende retourwaarde, een HRESULT van een Structured Storage-aanroep daaronder, en ExceptionClass registreert het Delphi-uitzonderingstype wanneer er een betrokken was, wat meestal genoeg is om een precies supportverzoek te openen zonder een volledige stacktrace bij te voegen

Ernst en bewerking bepalen wat uw code hierna doet

Ernst en bewerking zijn wat een diagnose omzet van een logregel naar een routeringsbeslissing. TXLSDiagnosticSeverity loopt van Info, Warning, Error, tot Fatal, en TXLSDiagnosticOperation tagt elke vermelding met de aanroep die het produceerde: Open, Save, Calculate, of Export. De twee assen zijn door ontwerp onafhankelijk: xlsDiagnosticUnhandledException is één vaste code die afgaat met Operation ingesteld op welke aanroep het ook echt opwierp, dus Code beantwoordt wat er misging terwijl Operation apart beantwoordt waar, in plaats van een aparte code nodig te hebben voor een uitzondering tijdens openen versus een tijdens opslaan. Die samenstelbaarheid is ook wat routering mechanisch maakt: log een waarschuwing en ga verder, een opslag geannuleerd via de Aborted-vlag is een typisch voorbeeld; tel een fout en houd de batch draaiende, een werkblad dat niet kon worden geserialiseerd is een typisch voorbeeld; stop de batch bij een fatale ernst, omdat dat niveau betekent dat een niet-afgehandelde uitzondering de aanroep al heeft afgewikkeld en doorgaan het risico loopt te werken vanuit een halfbijgewerkte toestand. Eén eerlijke kanttekening: Info bestaat in de enum als de standaard waarmee een verse TXLSDiagnostic begint, maar elk diagnostiek-aanroeppunt ingebouwd in de huidige HotXLS-release werpt ooit alleen Warning, Error, of Fatal op; Info is gereserveerd voor toekomstig gebruik, niet iets wat de engine vandaag uitstoot

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

OnDiagnostic aansluiten op een batchpijplijn

Diagnostics pollen na elke aanroep werkt voor één bestand; het houdt op te werken zodra u terug bent bij die batch van tienduizend 's nachts, omdat Diagnostics aan het begin van elke Open-, SaveAs-, en Recalculate-aanroep wordt geleegd. Lees het na het derde bestand in een lus, en u ziet alleen de diagnostiek van het derde bestand; wat de eerste twee bestanden ook rapporteerden is al verdwenen. OnDiagnostic lost dit op door de verzameling om te zetten in een stroom: abonneer u eenmaal voordat de lus begint, en dezelfde handler gaat af voor elk bestand, in volgorde, met de bestandsnaam nog steeds binnen bereik via een instantieveld

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;

Wat de callback daadwerkelijk kost

OnDiagnostic is goedkoop om een structurele reden: het gaat alleen af wanneer er al iets mis is, en mis is zeldzaam vergeleken met het aantal cellen, rijen, of werkbladen dat een werkboek bevat. Vergelijk dat met OnProgress en OnProgressEx, die routinematige voortgang rapporteren en vanaf het begin rond aanroepfrequentie moesten worden ontworpen. HotXLS activeert voortgang op werkbladniveau eenmaal per blad tijdens Open en SaveAs, niet eenmaal per cel of rij, wat de overhead per aanroep klein houdt zelfs op werkboeken met miljoenen cellen; Recalculate gaat verder en drosselt zijn eigen voortgangsgebeurtenis tot ruwweg elke vier procent van de afhankelijkheidsgraaf, zodat een volledige herberekening u een hartslag geeft in plaats van uw UI-thread te overspoelen met gebeurtenissen. Diagnostiek had niets van die drosseling nodig, omdat het aantal gebeurtenissen wordt begrensd door het aantal daadwerkelijke problemen, niet door de grootte van het bestand

De ene plek waar prestaties nog steeds van u afhangen, is binnen de handler zelf. OnDiagnostic gaat synchroon af, op de thread die Open, SaveAs, of Recalculate draait, dus een handler die blokkeert, een synchrone schrijfactie naar een externe logservice bijvoorbeeld, wordt onderdeel van de klokwandtijd van die aanroep. Voor één bestand is dat onzichtbaar. Vermenigvuldigd over een batch van tienduizend bestanden is het het verschil tussen een taak die 's nachts klaar is en een die tijdens de lunch nog draait, dus buffer wat de handler moet doen en spoel het asynchroon door in plaats van het trage deel inline te doen

Gestructureerde diagnostiek is het meest waardevol precies waar een boolean resultaat het zwakst is, in workflows die vele bestanden raken in plaats van één. Een audit- en conversiepijplijn voor werkboeken is het duidelijkste voorbeeld: in plaats van een kaal geslaagd/mislukt per bestand vast te leggen, koppel de Diagnostics-lijst van elk bestand aan zijn auditrecord, en het rapport vertelt u niet alleen wat mislukte maar waarom, wat het grootste deel is van wat ons artikel over het bouwen van een workbench voor werkboekaudit en -conversie in de eerste plaats probeert goed te krijgen. Diezelfde koppeling van voortgang en diagnostiek hoort ook thuis in elke workflow die al voortgangsrapportage nodig heeft omwille van zichzelf, precies het terrein dat wordt behandeld in onze gids over prestaties van grote werkboeken in HotXLS, waar een lange Open- of SaveAs-aanroep vaak genoeg voorkomt dat OnProgress er al is aangesloten en OnDiagnostic een natuurlijke, bijna gratis toevoeging ernaast is

Niets van dit alles vereist Excel ergens in de pijplijn geïnstalleerd, en niets van dit alles vereist het opvangen van een generieke uitzondering en gissen wat het betekende. IXLSWorkbookProgress en zijn leden Diagnostics, LastDiagnostic, en OnDiagnostic maken deel uit van de standaard HotXLS-component voor Delphi en C++Builder, naast de volledige diagnostiekcodereferentie en de rest van het Open-, SaveAs-, en Recalculate-oppervlak dat dit artikel heeft doorlopen