Bài viết kỹ thuật

Lưu an toàn trước sự cố trong HotXLS: File tạm dàn dựng trong Delphi

Một lần lưu chết giữa chừng, dù do khởi động lại cưỡng bức, tiến trình bị kill, hay một ổ đĩa đầy giữa lúc ghi, theo truyền thống đồng nghĩa với một điều đối với một định dạng được xây dựng quanh việc ghi tại chỗ: bất cứ byte nào đến được đĩa trước khi bị gián đoạn là những gì bạn nhận lại, và một workbook bị cắt cụt sẽ không mở lại được nữa. HotXLS đóng kiểu lỗi đó lại bằng một đường lưu an toàn trước sự cố được dùng cho mọi file XLSX, ODS, và XLS cổ điển mà nó ghi. Mỗi lệnh gọi SaveAs ghi toàn bộ file mới vào một file tạm được tạo cạnh đích, sau đó commit nó bằng một lệnh đổi tên nguyên tử duy nhất MoveFileExW từ Windows API, nên một lần lưu bị gián đoạn chỉ có thể thất bại trong việc tạo ra file mới, nó không bao giờ làm hỏng file bạn đã có sẵn. Cùng kỷ luật dàn-dựng-rồi-hoán-đổi này chạy đồng nhất qua cả hai engine lưu của HotXLS, bộ ghi BIFF8 đứng sau XLS cổ điển và bộ ghi OOXML đứng sau XLSX và ODS, và đây là một mẫu hình đáng học hỏi cho bất kỳ file nào mà chính code Delphi của bạn ghi đè trực tiếp, dù là bảng tính hay không

Điều gì xảy ra nếu việc lưu một workbook bị gián đoạn giữa chừng?

Câu trả lời trực tiếp là điều đó hoàn toàn phụ thuộc vào cách bộ ghi đụng vào file đích, và cách triển khai phổ biến, mở file đích và truyền nội dung mới trực tiếp vào nó, thì ổn chừng nào không có gì sai sót. Ngay khi có sự cố, một lần crash, một tiến trình bị kill cưỡng bức, một ổ mạng bị rớt giữa lúc ghi, file trên đĩa bị bỏ lại ở bất cứ trạng thái trung gian nào mà bộ ghi đã đạt đến: một central directory ZIP chưa bao giờ được nối thêm cho XLSX hay ODS, hay một luồng BIFF thiếu các bản ghi mà một trình đọc mong đợi cho XLS cổ điển. Excel không sửa chữa điều đó một cách nhẹ nhàng, và không bên tiêu thụ nào khác mong đợi một file hoàn chỉnh cũng vậy, nên kết quả thực tế là một workbook mở tốt hôm qua và từ chối mở hôm nay

HotXLS dàn dựng mỗi lần lưu đằng sau một lượt hoán đổi nguyên tử như thế nào

HotXLS không bao giờ mở file đích để ghi trực tiếp, cho bất kỳ định dạng nào trong ba định dạng nó lưu. Chuỗi thao tác luôn có cùng hình dạng mỗi lần: dựng toàn bộ đầu ra ở đâu đó không phải file mà người dùng đã có sẵn trên đĩa, và chỉ chuyển nó vào đúng vị trí một khi việc dựng đó đã hoàn toàn thành công. Cụ thể, SaveAs tạo một file tạm rỗng trong cùng thư mục với đường dẫn đích, ghi toàn bộ workbook mới vào file tạm đó, và chỉ sau khi lệnh ghi đó trả về không lỗi mới commit file tạm đè lên đích bằng một lệnh đổi tên duy nhất. Không điều nào trong số này cần một thuộc tính để bật lên; đó đơn giản là những gì SaveAs làm cho một đường dẫn file thuần túy, ở mỗi lệnh gọi

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Report');
    Sheet.Cells[1, 1].Value := 'Nothing special to enable here';
    // If this call is interrupted, monthly-report.xlsx on disk stays
    // either the old version, complete, or the new version, complete
    if Book.SaveAs('monthly-report.xlsx', xlsxOpenXMLWorkbook) <> 1 then
      raise Exception.Create('Save failed, see Book.LastDiagnostic');
  finally
    Book.Free;
  end;
end;

Cùng kỷ luật đó áp dụng cho bộ ghi XLS cổ điển, không chỉ riêng bộ ghi OOXML, và hai file tạm thậm chí còn chia sẻ một quy ước đặt tên: cả hai đều gọi API GetTempFileNameW của Windows với tiền tố hxl, nên một lần lưu bị gián đoạn trước khi dọn dẹp có thể để lại một file lạc lõng với tên như hxl4C2A.tmp nằm cạnh workbook của bạn. File đó không phải sự hỏng hóc, nó là bằng chứng cơ chế đã hoạt động đúng như thiết kế: lần ghi chưa hoàn tất dừng lại ở đó, và workbook thật của bạn chưa bao giờ được mở để ghi ngay từ đầu. Thấy một file như vậy sau một lần crash là an toàn để xóa và không có gì cần điều tra

Vì sao dàn dựng file tạm cạnh workbook thay vì trong %TEMP%?

Câu trả lời ngắn gọn là lệnh đổi tên của MoveFileExW chỉ nguyên tử khi nguồn và đích nằm trên cùng một volume, và cách chắc chắn nhất để đảm bảo điều đó mà không cần yêu cầu caller cấu hình gì là suy ra vị trí của file tạm từ chính đường dẫn đích. HotXLS tính thư mục riêng của đích và đưa thẳng thư mục đó cho GetTempFileNameW, nên file tạm luôn được tạo trên cùng ổ đĩa, cùng volume, với file nó sắp thay thế, tự động, cho mỗi lần lưu. Nếu thư viện thay vào đó dàn dựng việc ghi trong thư mục temp hệ thống, một đường dẫn đích trên một ổ khác hay một volume mạng đã ánh xạ sẽ biến bước cuối cùng thành một thao tác xuyên volume, thứ mà Windows API hoặc từ chối thẳng thừng, hoặc, nếu một caller chủ động bật với một cờ thêm mà HotXLS không đặt ở đây, âm thầm suy giảm thành một phép copy không nguyên tử theo sau bởi một lệnh xóa, mở lại chính xác khung cửa sổ gián đoạn mà toàn bộ cơ chế này tồn tại để đóng lại

Bước commit: MoveFileExW, write-through, và điều gì xảy ra khi thất bại

Bước cuối cùng của mỗi lần lưu là chính xác một lệnh gọi Windows API, MoveFileExW, mang hai cờ mỗi cờ làm một công việc riêng biệt. MOVEFILE_REPLACE_EXISTING là thứ cho phép lệnh đổi tên đáp xuống một file đã tồn tại; thiếu nó, một lệnh đổi tên nhắm vào một đường dẫn đã tồn tại sẽ đơn giản thất bại, điều này sẽ đánh bại toàn bộ mục đích của một lần lưu vốn dĩ để thay thế một workbook bạn đã có sẵn. MOVEFILE_WRITE_THROUGH bao phủ tính bền vững: nó nói với hàm không trả về cho đến khi việc di chuyển thực sự hoàn tất trên đĩa, thay vì trả về ngay khi lệnh đổi tên chỉ mới được xếp hàng, đóng lại một khung cửa sổ hẹp hơn nhưng có thật nơi một lần crash ngay sau khi SaveAs trả về vẫn có thể bắt gặp cuộc hoán đổi đang diễn ra. Nếu file tạm không thể tạo ra được, hay lệnh đổi tên cuối cùng thất bại vì bất kỳ lý do gì (một vấn đề quyền, một đích bị khóa, một sự không khớp volume), HotXLS tự xóa file tạm thay vì để lại rác, và file đích được để lại đúng như trước khi lệnh gọi diễn ra

Result := Book.SaveAs(TargetPath, xlsxOpenXMLWorkbook);
if Result <> 1 then
begin
  // TargetPath on disk is unchanged; safe to retry, alert, or
  // fall back to a different path without touching prior output
  LogWriter.Write(Format('SaveAs failed (%d): %s',
    [Book.LastDiagnostic.Code, Book.LastDiagnostic.Message]));
  Exit(False);
end;

Bản thân SaveAs giữ quy ước trả về chung của HotXLS, một khi thành công, một số âm khi thất bại, nhưng một số nguyên trần trụi không nói lên vì sao một lần lưu thất bại, và việc coi mọi kết quả âm như nhau vứt bỏ thông tin mà một chính sách thử lại thực sự có thể dùng đến. Thuộc tính LastDiagnostic, và tập hợp Diagnostics đầy đủ hơn đằng sau nó, mang theo thông điệp mà HotXLS sinh ra nội bộ, phân biệt một file tạm không thể tạo được với một lệnh đổi tên mà Windows từ chối. Một job hàng loạt ghi log Code và Message cho mỗi lần SaveAs thất bại xây dựng đúng loại bằng chứng bạn muốn có vào lần duy nhất một khách hàng báo cáo một lần lưu âm thầm không làm gì cả

XLS cổ điển trả giá bằng bộ nhớ, XLSX và ODS trả giá bằng đĩa

Hai engine lưu đạt đến cùng kết quả an toàn trước sự cố qua các con đường khác nhau, và sự khác biệt đó quan trọng nếu bạn đang tinh chỉnh một trong hai cho một job hàng loạt lớn. Bộ ghi XLS cổ điển dựng toàn bộ tài liệu ghép OLE trong bộ nhớ trước, dùng structured storage được hậu thuẫn bởi một handle bộ nhớ, và chỉ copy bộ đệm hoàn tất đó ra file tạm anh em trong một lần ghi; lý lẽ trong chính mã nguồn của HotXLS rất trực tiếp: dựng toàn bộ file trong bộ nhớ trước là điều ngăn một lần lưu thất bại hoặc bị hủy không bao giờ cắt cụt đích. Bộ ghi XLSX và ODS thay vào đó truyền các mục ZIP của nó vào file tạm khi chúng được tạo ra, cùng kiểu dàn dựng cấp file với một hồ sơ bộ nhớ khác. Nếu bạn đã dựa vào StreamingWrite để giữ các bản xuất XLSX lớn trong giới hạn bộ nhớ của một container, hãy biết rằng đòn bẩy tương đương cho xuất XLS cổ điển không tồn tại dưới cùng hình thức: cam kết an toàn trước sự cố là vô điều kiện trong cả hai trường hợp, nhưng một bản xuất .xls cũ rất lớn vẫn giữ toàn bộ đầu ra của nó trong RAM bất kể thế nào, một sự đánh đổi được nói sâu hơn trong bài viết của chúng tôi về ghi streaming cho các job hàng loạt trên server

Áp dụng cùng mẫu hình bên ngoài HotXLS, và giới hạn của cam kết này

Vay mượn mẫu hình này chủ yếu là vấn đề nối hai lệnh gọi Windows API giống hệt mà HotXLS dựa vào nội bộ. GetTempFileNameW đưa cho bạn một file rỗng, tên duy nhất, trong một thư mục bạn chọn, và MoveFileExW commit lần ghi hoàn tất của bạn đè lên đích thật trong một bước; một phiên bản tối giản của cùng thủ tục mà HotXLS chạy trước mỗi SaveAs trông như sau

function SaveFileAtomically(const Path: WideString; const Contents: TBytes): Boolean;
var
  Dir, TempName: WideString;
  Buffer: array[0..MAX_PATH] of WideChar;
  FS: TFileStream;
begin
  Result := False;
  Dir := ExtractFilePath(ExpandFileName(Path));
  FillChar(Buffer, SizeOf(Buffer), 0);
  if GetTempFileNameW(PWideChar(Dir), 'app', 0, @Buffer[0]) = 0 then
    Exit;
  TempName := PWideChar(@Buffer[0]);
  try
    FS := TFileStream.Create(TempName, fmCreate or fmShareExclusive);
    try
      FS.WriteBuffer(Contents[0], Length(Contents));
    finally
      FS.Free;
    end;
    Result := MoveFileExW(PWideChar(TempName), PWideChar(ExpandFileName(Path)),
      MOVEFILE_REPLACE_EXISTING or MOVEFILE_WRITE_THROUGH);
  finally
    if not Result then
      DeleteFileW(PWideChar(TempName));
  end;
end;

Cam kết này có những giới hạn thật đáng biết trước khi bạn tin tưởng nó một cách mù quáng. Việc dàn dựng một bản sao đầy đủ trước khi thay thế bản gốc có nghĩa là một lần lưu tạm thời cần dung lượng đĩa cho cả file cũ lẫn file mới, khoảng gấp đôi kích thước workbook trong suốt thời gian ghi, điều này ổn cho một báo cáo và đáng kiểm tra cho một bản xuất nhiều gigabyte chạy trên một volume gần đầy. File tạm cũng phải nằm trong cùng thư mục với đích, nên bất kỳ tài khoản nào HotXLS đang chạy dưới quyền cần quyền tạo file trên chính thư mục đó, không chỉ đơn thuần quyền ghi đè lên một file nó đã biết sẵn; một triển khai khóa một thư mục đích chỉ cho phép chỉnh sửa tại chỗ các tên file đã tồn tại cụ thể, thay vì quyền ghi cấp thư mục, sẽ thấy SaveAs thất bại ở bước file tạm dù cho lần ghi trực tiếp tương đương lẽ ra đã thành công

Có thêm hai ranh giới đáng nêu rõ. Một đích trên một ổ mạng hay bên trong một thư mục được đồng bộ bởi OneDrive hoặc một client tương tự có thể hoạt động khác với NTFS cục bộ dù Windows vẫn báo cáo nó như một volume đơn, vì driver hệ thống file đứng trước nó có thể không triển khai lệnh đổi tên theo cùng cách; nếu mục tiêu triển khai của bạn lưu qua một đường dẫn mạng, đáng để test một lần gián đoạn cưỡng bức ở đó cụ thể thay vì giả định hành vi đĩa cục bộ được mang theo. Và toàn bộ cơ chế này chỉ có phạm vi khi lưu vào một file có tên. Gọi SaveAs với một TStream thay vào đó, và HotXLS ghi trực tiếp vào bất cứ stream nào bạn đưa cho nó, không có file đích nào để dàn dựng hay bảo vệ, vì tính bền vững của stream đó (một bộ đệm bộ nhớ, một lượt tải lên mạng, một blob cơ sở dữ liệu) hoàn toàn là trách nhiệm của code bạn từ điểm đó trở đi

Một lượt xác minh sau đó có thể dựa hoàn toàn vào chính cam kết này, kể cả loại được xây dựng vào một xưởng kiểm toán và chuyển đổi workbook: một file mở lại bị thiếu hay ngắn là một vấn đề chuyển đổi thật cần truy tìm, không bao giờ là một lần lưu bị gián đoạn giữa chừng và để lại thứ gì đó mơ hồ trên đĩa. Việc ghi dàn dựng an toàn trước sự cố được xây dựng sẵn vào SaveAs cho mọi workbook XLSX, ODS, và XLS cổ điển do HotXLS Component dành cho Delphi và C++Builder tạo ra, không cần cấu hình gì để bật nó lên