Bài viết kỹ thuật

Chẩn đoán có cấu trúc thay vì kết quả boolean trong HotXLS

Chạy một lượt chuyển đổi hàng loạt trên mười nghìn bảng tính qua đêm, và đến sáng ba trong số đó trả về False. Đó là toàn bộ báo cáo hậu kiểm mà một kết quả lưu boolean cho bạn: một con số lỗi, không có gì về file nào, sheet nào, hay nguyên nhân nào trong hàng chục nguyên nhân khả dĩ là thủ phạm. HotXLS, thành phần Excel gốc của losLab cho Delphi và C++Builder, thay thế bit đơn đó bằng chẩn đoán có cấu trúc. Interface IXLSWorkbookProgress phơi bày một danh sách Diagnostics và một sự kiện OnDiagnostic báo cáo một mã số ổn định, một cấp độ nghiêm trọng, thao tác nào đã thất bại, và sheet nào nó xảy ra, cho mỗi lệnh gọi Open, SaveAs, và Recalculate

Vì sao một kết quả lưu boolean thất bại ở quy mô lớn?

Một file thất bại đơn lẻ không phải là vấn đề mà một kết quả boolean tạo ra; một nghìn file như vậy mới là vấn đề. Khi SaveAs trả về thứ gì đó khác với thành công cho ba trong số mười nghìn file, câu hỏi tiếp theo luôn giống nhau: liệu ba file này có thể thử lại được hay cần một con người can thiệp? Một lỗi quyền truy cập trên ổ mạng không phải là cùng một sự cố với một công thức mà engine tính toán không thể đánh giá được, và cũng không giống với một worksheet âm thầm vượt quá một giới hạn định dạng. Chỉ với một kết quả pass/fail để làm việc, mỗi trường hợp trong số đó trở thành một ticket hỗ trợ giống hệt nhau, và ai đó phải mở từng file bằng tay, trong Excel, và nhìn chằm chằm vào nó cho đến khi nguyên nhân trở nên rõ ràng. Việc phân loại thủ công đó là chi phí thực sự của một API boolean, và nó tăng tuyến tính theo kích thước của lô, chính xác là đặc tính bạn không muốn có từ việc xử lý lỗi

Bên trong IXLSWorkbookProgress: một TXLSDiagnostic mang theo những gì

IXLSWorkbookProgress là interface HotXLS dùng để báo cáo cả tiến độ một thao tác lẫn những gì đã sai bên trong nó, và hai nửa này chia sẻ một hợp đồng vì một lý do: cả hai đều là những thứ mà một lệnh gọi Open, SaveAs, hay Recalculate chạy lâu cần giao tiếp mà không ném ra ngoại lệ giữa thao tác. Nửa tiến độ là OnProgressOnProgressEx, phát ra với một giai đoạn, một trạng thái, và một cặp hiện-tại/tổng. Nửa chẩn đoán là nửa bài viết này nói đến: một thuộc tính Diagnostics trả về một danh sách TXLSDiagnostics, một lối tắt LastDiagnostic cho mục gần nhất, và một sự kiện OnDiagnostic phát ra ngay khi mỗi bản ghi TXLSDiagnostic được tạo ra. Mỗi bản ghi mang theo một Code dạng số, một TXLSDiagnosticSeverity, TXLSDiagnosticOperation đã tạo ra nó, một Message dễ đọc cho con người, một SheetIndexSheetName, và một NativeCode giữ lại bất cứ giá trị trả về cấp thấp hơn nào đã kích hoạt mục đó

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;

Việc đọc Diagnostics theo cách này đã tự nó vượt trội hơn một kết quả boolean, vì CodeSheetName biến một điều bí ẩn thành một sự thật cụ thể, có thể lọc được. Bản ghi TXLSDiagnostic vươn xa hơn những gì ví dụ này in ra: RecordIdStreamOffset tồn tại cho việc điều tra pháp y cấp byte bên trong một luồng BIFF, và PartName giữ mục zip OOXML, chẳng hạn xl/worksheets/sheet3.xml, mà một vấn đề bắt nguồn từ đó. Đáng biết trước khi bạn xây dựng công cụ xoay quanh chúng: trong bản phát hành hiện tại, không điểm gọi chẩn đoán tích hợp sẵn nào điền vào RecordId hay StreamOffset, nên cả hai giữ nguyên giá trị mặc định constructor là -1, nghĩa là "không áp dụng" chứ không phải "bằng không". Hãy coi sự vắng mặt của chúng là bình thường, không phải một lỗi trong handler của bạn

Hai engine, một hình dạng, một khác biệt lặng lẽ

HotXLS gửi kèm hai engine đằng sau cùng mô hình báo cáo này, một mặt tiền BIFF8 cho file .xls cũ và một mặt tiền OOXML cho .xlsx, và chúng không phơi bày IXLSWorkbookProgress giống hệt nhau. TXLSWorkbook, engine .xls, chính thức triển khai IXLSWorkbookProgress, nên nó có thể được truyền vào bất cứ đâu kiểu interface đó được mong đợi. TXLSXWorkbook, engine .xlsx, phơi bày cùng các thành viên Diagnostics, LastDiagnostic, OnDiagnostic, OnProgress, và OnProgressEx với tên và kiểu giống hệt, nhưng như một lớp thuần túy chứ không phải một triển khai chính thức của interface đó, nên nó sẽ không tự thỏa mãn một tham số IXLSWorkbookProgress. Trong thực tế điều này hiếm khi quan trọng, vì hầu hết code làm việc với một lớp workbook cụ thể tại một thời điểm, nhưng nó có nghĩa là bạn không thể viết một hàm hỗ trợ duy nhất kiểu IXLSWorkbookProgress và đưa cho nó đối tượng workbook của một trong hai engine thay thế lẫn nhau. Sự khác biệt trường duy nhất theo trực tiếp từ sự chia tách định dạng là PartName: chỉ engine XLSX điền vào nó, vì chỉ OOXML mới có các phần zip để đặt tên

Điều gì khiến một mã chẩn đoán trở thành thứ bạn có thể rẽ nhánh an toàn theo nó?

Trường Code là phần duy nhất của một chẩn đoán đáng để hardcode một phép so sánh; Message thì không, vì văn xuôi chính xác là loại thứ bị diễn đạt lại, dịch lại, hoặc mở rộng thêm chi tiết trong một bản phát hành sau mà không ai coi đó là một thay đổi phá vỡ tương thích. Các mã chẩn đoán tích hợp sẵn của HotXLS đã đọc như thể chúng được thiết kế với sự phân biệt đó trong đầu: các mã liên quan đến save chạy từ 1000 đến 1005, các mã liên quan đến open nằm ở 1100 và 1101, các mã liên quan đến calculate ở 1200 và 1201, và một mã định dạng không được hỗ trợ ở 1300, với các khoảng trống được để lại bên trong mỗi dải thay vì các mã chạy liên tiếp trên toàn bộ chúng. Khoảng cách đó là điều cho phép một nhà cung cấp thêm một chế độ lỗi mới lúc lưu, ví dụ tại 1006, mà không cần đánh số lại các mã mà câu lệnh switch của bạn đã phụ thuộc vào, và điều này đáng kiểm tra trong bất kỳ API chẩn đoán nào trước khi bạn cam kết so khớp theo một mã trong sản xuất, không chỉ riêng API này. Giữ một nhánh mặc định trong logic điều phối của riêng bạn bất kể việc đánh số trông ổn định đến đâu, vì các chế độ lỗi mới chính xác là những gì một trình phân tích hay bộ ghi đang tiến hóa liên tục phát hiện ra. NativeCodeExceptionClass nằm một lớp bên dưới Code cho khi bạn cần leo thang: NativeCode giữ lại giá trị trả về bên dưới, một HRESULT từ một lệnh gọi Structured Storage trong số đó, và ExceptionClass ghi lại kiểu ngoại lệ Delphi khi có một ngoại lệ liên quan, thường đủ để mở một yêu cầu hỗ trợ chính xác mà không cần đính kèm toàn bộ stack trace

Cấp độ nghiêm trọng và thao tác quyết định code của bạn làm gì tiếp theo

Cấp độ nghiêm trọng và thao tác là những gì biến một chẩn đoán từ một dòng log thành một quyết định định tuyến. TXLSDiagnosticSeverity chạy Info, Warning, Error, và Fatal, và TXLSDiagnosticOperation gắn nhãn mỗi mục với lệnh gọi đã tạo ra nó: Open, Save, Calculate, hoặc Export. Hai trục này độc lập theo thiết kế: xlsDiagnosticUnhandledException là một mã cố định duy nhất phát ra với Operation được đặt thành bất kỳ lệnh gọi nào thực sự đã ném ra nó, nên Code trả lời điều gì đã sai trong khi Operation riêng biệt trả lời ở đâu, thay vì cần một mã riêng biệt cho một ngoại lệ trong lúc open so với một ngoại lệ trong lúc save. Khả năng kết hợp đó cũng là điều khiến việc định tuyến trở nên máy móc: ghi log một cảnh báo rồi tiếp tục, một lần lưu bị hủy qua cờ Aborted là một ví dụ điển hình; đếm một lỗi và giữ lô tiếp tục chạy, một worksheet thất bại trong việc serialize là một ví dụ điển hình; dừng lô ở mức nghiêm trọng fatal, vì mức đó có nghĩa là một ngoại lệ chưa được xử lý đã unwind lệnh gọi và tiếp tục chạy có nguy cơ làm việc từ một trạng thái cập nhật dở dang. Một lưu ý trung thực: Info tồn tại trong enum như mặc định mà một TXLSDiagnostic mới bắt đầu với, nhưng mỗi điểm gọi chẩn đoán được xây dựng vào bản phát hành HotXLS hiện tại chỉ bao giờ ném ra Warning, Error, hoặc Fatal; Info được dành cho việc sử dụng trong tương lai, không phải thứ engine phát ra hôm nay

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

Kết nối OnDiagnostic vào một pipeline hàng loạt

Polling Diagnostics sau mỗi lệnh gọi hoạt động tốt cho một file đơn; nó ngừng hoạt động một khi bạn quay lại lô mười nghìn file qua đêm đó, vì Diagnostics bị xóa sạch khi bắt đầu mỗi lệnh gọi Open, SaveAs, và Recalculate. Đọc nó sau file thứ ba trong một vòng lặp và bạn chỉ thấy chẩn đoán của file thứ ba; bất cứ điều gì hai file đầu đã báo cáo đã biến mất. OnDiagnostic giải quyết điều đó bằng cách biến tập hợp thành một luồng: đăng ký một lần trước khi vòng lặp bắt đầu, và cùng một handler phát ra cho mỗi file, theo thứ tự, với tên file vẫn còn trong phạm vi thông qua một trường instance

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;

Callback này thực sự tốn chi phí gì

OnDiagnostic rẻ vì một lý do cấu trúc: nó chỉ phát ra khi đã có gì đó sai, và sai thì hiếm so với số lượng ô, hàng, hay worksheet mà một workbook mang theo. So sánh điều đó với OnProgressOnProgressEx, những sự kiện báo cáo tiến độ thường xuyên và phải được thiết kế xoay quanh tần suất lệnh gọi ngay từ đầu. HotXLS phát ra tiến độ cấp worksheet một lần cho mỗi sheet trong lúc OpenSaveAs, không phải một lần cho mỗi ô hay hàng, đó là điều giữ chi phí trên mỗi lệnh gọi nhỏ ngay cả trên các workbook với hàng triệu ô; Recalculate đi xa hơn và điều tiết sự kiện tiến độ của chính nó xuống còn khoảng mỗi bốn phần trăm của đồ thị phụ thuộc, nên một lần tính toán lại đầy đủ cho bạn một nhịp tim thay vì làm ngập luồng UI của bạn với các sự kiện. Chẩn đoán không cần bất kỳ điều tiết nào như vậy, vì số lượng sự kiện bị giới hạn bởi số lượng vấn đề thực tế, không phải bởi kích thước của file

Nơi duy nhất hiệu năng vẫn phụ thuộc vào bạn là bên trong chính handler. OnDiagnostic phát ra đồng bộ, trên luồng đang chạy Open, SaveAs, hay Recalculate, nên một handler bị chặn, một lần ghi đồng bộ đến một dịch vụ log từ xa chẳng hạn, trở thành một phần của thời gian thực tế của lệnh gọi đó. Với một file đơn điều đó vô hình. Nhân lên trên một lô mười nghìn file, đó là sự khác biệt giữa một job hoàn tất qua đêm và một job vẫn đang chạy vào giờ ăn trưa, nên hãy đệm những gì handler cần làm và xả nó bất đồng bộ thay vì làm phần chậm inline

Chẩn đoán có cấu trúc có giá trị nhất chính xác ở nơi một kết quả boolean yếu nhất, trong các luồng công việc chạm vào nhiều file thay vì một. Một pipeline kiểm toán và chuyển đổi workbook là ví dụ rõ ràng nhất: thay vì ghi lại một pass/fail trần trụi cho mỗi file, đính kèm danh sách Diagnostics của mỗi file vào bản ghi kiểm toán của nó, và báo cáo nói cho bạn không chỉ điều gì thất bại mà còn vì sao, đó là phần lớn những gì bài viết của chúng tôi về xây dựng một xưởng kiểm toán và chuyển đổi workbook đang cố làm đúng ngay từ đầu. Cùng sự kết hợp giữa tiến độ và chẩn đoán cũng thuộc về bất kỳ luồng công việc nào đã cần báo cáo tiến độ cho chính mục đích của nó, chính xác là lãnh thổ được nói đến trong hướng dẫn của chúng tôi về hiệu năng workbook lớn trong HotXLS, nơi một lệnh gọi Open hay SaveAs dài đủ phổ biến để OnProgress đã được kết nối sẵn và OnDiagnostic là một bổ sung tự nhiên, gần như miễn phí bên cạnh nó

Không điều nào trong số này đòi hỏi Excel được cài đặt ở bất cứ đâu trong pipeline, và không điều nào đòi hỏi bắt một ngoại lệ chung chung và đoán nó có nghĩa là gì. IXLSWorkbookProgress và các thành viên Diagnostics, LastDiagnostic, và OnDiagnostic của nó là một phần của HotXLS Component tiêu chuẩn dành cho Delphi và C++Builder, cùng với tài liệu tham khảo mã chẩn đoán đầy đủ và phần còn lại của bề mặt Open, SaveAs, và Recalculate mà bài viết này đã đi qua