Teknik Makale

HotXLS Çökmeye Dayanıklı Kayıtlar: Delphi'de Aşamalı Geçici Dosyalar

Zorla yeniden başlatma, sonlandırılan bir süreç veya yazma sırasında dolan bir disk yüzünden yarıda kalan bir kayıt işlemi, yerinde yazmaya dayalı bir format için geleneksel olarak tek bir şey anlamına gelmiştir: kesintiden önce diske ulaşan baytlar ne ise geri aldığınız odur ve kırpılmış bir çalışma kitabı bir daha açılmaz. HotXLS, yazdığı her XLSX, ODS ve klasik XLS dosyası için kullanılan çökmeye dayanıklı bir kayıt yoluyla bu hata modunu ortadan kaldırır. Her SaveAs çağrısı, tam yeni dosyayı hedefin yanında oluşturulan geçici bir dosyaya yazar, ardından Windows API'sinden tek bir atomik MoveFileExW yeniden adlandırmasıyla bunu işleme koyar; böylece yarıda kesilen bir kayıt yalnızca yeni dosyayı üretmede başarısız olabilir, elinizde zaten olan dosyaya asla zarar vermez. Aynı "aşamala sonra değiştir" disiplini, klasik XLS'in arkasındaki BIFF8 yazıcısı ile XLSX ve ODS'nin arkasındaki OOXML yazıcısı olmak üzere HotXLS'in her iki kayıt motorunda da tekdüze çalışır ve bu, kendi Delphi kodunuzun doğrudan üzerine yazdığı herhangi bir dosya için -elektronik tablo olsun olmasın- ödünç almaya değer bir örüntüdür

Bir çalışma kitabı kaydı yarıda kesilirse ne olur?

Doğrudan yanıt, tamamen yazıcının hedef dosyaya nasıl dokunduğuna bağlıdır ve yaygın uygulama olan hedef dosyayı açıp yeni içeriği doğrudan içine akıtmak, hiçbir şey yanlış gitmediği sürece sorunsuzdur. Bir şey yanlış gittiği an -bir çökme, zorla süreç sonlandırma, yazma sırasında düşen bir ağ paylaşımı- diskteki dosya, yazıcının ulaştığı ara durumda kalır: XLSX veya ODS için hiç eklenmemiş bir ZIP merkezi dizini, ya da klasik XLS için bir okuyucunun beklediği kayıtlardan yoksun bir BIFF akışı. Excel bunu zarif bir şekilde onarmaz ve eksiksiz bir dosya bekleyen başka hiçbir tüketici de onarmaz; bu yüzden pratik sonuç, dün sorunsuz açılan ama bugün açılmayı reddeden bir çalışma kitabıdır

HotXLS her kaydı tek bir atomik değişimin arkasında nasıl aşamalandırıyor

HotXLS, kaydettiği üç formattan hiçbiri için hedef dosyayı doğrudan yazmak üzere asla açmaz. Sıra her seferinde aynı şekildedir: tam çıktıyı kullanıcının diskte zaten sahip olduğu dosya olmayan bir yerde oluşturun ve bu yapım tamamen başarılı olduktan sonra ancak o zaman onu yerine taşıyın. Somut olarak, SaveAs hedef yolla aynı klasörde boş bir geçici dosya oluşturur, tüm yeni çalışma kitabını bu geçici dosyaya yazar ve bu yazma hatasız döndükten sonra ancak o zaman geçici dosyayı tek bir yeniden adlandırmayla hedefin üzerine işler. Bunun hiçbiri devreye almak için bir özellik gerektirmez; bu, düz bir dosya yolu için SaveAs'ın her çağrıda yaptığı şeydir, başka hiçbir şey değil

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;

Aynı disiplin yalnızca OOXML yazıcısına değil klasik XLS yazıcısına da uygulanır ve iki geçici dosya hatta bir adlandırma kuralı paylaşır: her ikisi de hxl önekiyle Windows GetTempFileNameW API'sini çağırır; bu yüzden temizlikten önce kesilen bir kayıt, çalışma kitabınızın yanında hxl4C2A.tmp gibi bir adı olan başıboş bir dosya bırakabilir. Bu dosya bozulma değildir, mekanizmanın tam olarak tasarlandığı gibi çalıştığının kanıtıdır: eksik yazma orada durmuştur ve gerçek çalışma kitabınız zaten hiç yazmak için açılmamıştır. Bir çökmeden sonra bunu görmek silmesi güvenlidir ve araştırılacak bir şey değildir

Geçici dosya neden %TEMP% yerine çalışma kitabının yanına aşamalandırılıyor?

Kısa yanıt, MoveFileExW'nin yeniden adlandırmasının yalnızca kaynak ve hedef aynı birimde otururken atomik olmasıdır ve çağırana hiçbir şey yapılandırmasını istemeden bunu garanti etmenin en kesin yolu, geçici dosyanın konumunu hedef yolun kendisinden türetmektir. HotXLS, hedefin kendi klasörünü hesaplar ve bu dizini doğrudan GetTempFileNameW'a verir; bu yüzden geçici dosya her zaman, her kayıt için otomatik olarak, değiştirmek üzere olduğu dosyayla aynı sürücüde, aynı birimde oluşturulur. Kütüphane bunun yerine yazmaları sistem geçici klasöründe aşamalandırsaydı, farklı bir sürücüdeki veya eşlenmiş bir ağ biriminde bir hedef yolu, son adımı birimler arası bir işleme dönüştürürdü; bu da Windows API'sinin ya doğrudan reddettiği ya da bir çağıranın HotXLS'in burada ayarlamadığı ekstra bir bayrakla açıkça devreye girmesi durumunda sessizce atomik olmayan bir kopyalama ve ardından bir silmeye düşürdüğü bir şeydir; bu da tüm bu mekanizmanın kapatmak için var olduğu kesinti penceresini tam olarak yeniden açar

İşleme (commit) adımı: MoveFileExW, write-through ve başarısızlıkta ne olur

Her kaydın son adımı tam olarak tek bir Windows API çağrısı olan MoveFileExW'dir ve her biri farklı bir iş yapan iki bayrak taşır. MOVEFILE_REPLACE_EXISTING, yeniden adlandırmanın zaten var olan bir dosyanın üzerine inmesine izin veren şeydir; bu olmadan, var olan bir yolu hedefleyen bir yeniden adlandırma basitçe başarısız olur ki bu da zaten sahip olduğunuz bir çalışma kitabını değiştirmek için tasarlanmış bir kaydın tüm amacını boşa çıkarır. MOVEFILE_WRITE_THROUGH dayanıklılığı kapsar: fonksiyona, taşıma yalnızca kuyruğa alındığında değil, diskte gerçekten tamamlandığında dönmesini söyler; bu da SaveAs döndükten hemen sonra gelen bir çökmenin değişimi hâlâ uçuşta yakalayabileceği daha dar ama gerçek bir yarışı kapatır. Geçici dosya oluşturulamazsa veya son yeniden adlandırma herhangi bir nedenle başarısız olursa (bir izin sorunu, kilitli bir hedef, bir birim uyuşmazlığı), HotXLS geçici dosyayı arkasında çöp bırakmak yerine kendisi siler ve hedef dosya çağrıdan önceki tam haliyle bırakılır

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;

SaveAs'ın kendisi HotXLS genelinde paylaşılan dönüş kuralını korur: başarıda bir, başarısızlıkta negatif bir sayı; ama çıplak bir tam sayı bir kaydın neden başarısız olduğunu söylemez ve her negatif sonucu aynı şekilde ele almak, bir yeniden deneme politikasının gerçekten kullanabileceği bilgiyi çöpe atar. LastDiagnostic özelliği ve arkasındaki daha dolu Diagnostics koleksiyonu, HotXLS'in dahili olarak ürettiği mesajı taşır; oluşturulamayan bir geçici dosyayı Windows'un reddettiği bir yeniden adlandırmadan ayırt eder. Her başarısız SaveAs'ta Code ve Message'ı kaydeden bir toplu iş, bir müşterinin sessizce hiçbir şey yapmayan bir kayıt bildirdiği o tek seferde ihtiyacınız olan kanıtı tam olarak biriktirir

Klasik XLS bedeli bellekle öder, XLSX ve ODS bedeli diskle öder

İki kayıt motoru aynı çökmeye dayanıklı sonuca farklı yollardan ulaşır ve bu fark, ikisinden birini zaten büyük bir toplu iş için ayarlıyorsanız önemlidir. Klasik XLS yazıcısı, bir bellek tutamacına dayanan yapılandırılmış depolama kullanarak tüm OLE bileşik belgesini önce bellekte oluşturur ve yalnızca o bitmiş arabelleği kardeş geçici dosyaya tek bir yazmayla kopyalar; HotXLS'in kendi kaynak kodundaki gerekçe doğrudandır: tüm dosyayı önce bellekte oluşturmak, başarısız veya iptal edilmiş bir kaydın hedefi asla kırpmamasını sağlayan şeydir. XLSX ve ODS yazıcısı ise bunun yerine ZIP girdilerini üretildikçe geçici dosyaya akıtır; bu, farklı bir bellek profiliyle aynı dosya düzeyinde aşamalandırmadır. Büyük XLSX dışa aktarımlarını bir konteynerin bellek sınırları içinde tutmak için zaten StreamingWrite'a dayanıyorsanız, klasik XLS dışa aktarımı için eşdeğer bir kolun aynı biçimde var olmadığını bilin: çökmeye dayanıklı garanti her iki durumda da koşulsuzdur, ama çok büyük bir eski .xls dışa aktarımı ne olursa olsun tam çıktısını RAM'de tutar; bu ödünleşim sunucu toplu işleri için akışlı yazma üzerine makalemizde daha derinlemesine ele alınmıştır

Aynı örüntüyü HotXLS dışında uygulamak ve garantinin bittiği yer

Örüntüyü ödünç almak büyük ölçüde HotXLS'in dahili olarak dayandığı aynı iki Windows API çağrısını bağlamaktan ibarettir. GetTempFileNameW size seçtiğiniz bir klasörde benzersiz adlandırılmış, boş bir dosya verir ve MoveFileExW bitmiş yazmanızı gerçek hedefin üzerine tek bir adımda işler; HotXLS'in her SaveAs'tan önce çalıştırdığı aynı rutinin minimal bir sürümü şöyle görünür

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;

Bu garantinin, körü körüne güvenmeden önce bilmeye değer gerçek sınırları vardır. Orijinalin yerine geçirmeden önce tam bir kopyayı aşamalandırmak, yazma süresince eski dosya ve yeni dosya için, yaklaşık olarak çalışma kitabının boyutunun iki katı kadar disk alanına kısaca ihtiyaç duyulması demektir; bu bir rapor için sorun değildir ama neredeyse dolu bir birime karşı çalışan çok gigabaytlık bir dışa aktarım için kontrol etmeye değer. Geçici dosya da hedefle aynı klasöre inmek zorundadır; bu yüzden HotXLS'in çalıştığı hesap ne olursa olsun, yalnızca zaten bildiği tek dosyanın üzerine yazma iznine değil, özellikle o klasörde dosya oluşturma iznine ihtiyaç duyar; bir hedef klasörü klasör düzeyinde yazma erişimi yerine belirli mevcut dosya adlarının yerinde düzenlenmesine kilitleyen bir dağıtım, eşdeğer doğrudan yazma başarılı olacakken bile SaveAs'ın geçici dosya adımında başarısız olduğunu görecektir

Açıkça belirtilmeye değer iki sınır daha vardır. Bir ağ paylaşımındaki veya OneDrive ya da benzeri bir istemci tarafından senkronize edilen bir klasördeki bir hedef, Windows onu hâlâ tek bir birim olarak bildirse bile yerel NTFS'den farklı davranabilir, çünkü önündeki dosya sistemi sürücüsü yeniden adlandırmayı aynı şekilde uygulamayabilir; dağıtım hedefiniz bir ağ yolu üzerinden kaydediyorsa, yerel disk davranışının aynen geçerli olduğunu varsaymak yerine özellikle orada zorlanmış bir kesintiyi test etmeye değer. Ve tüm mekanizma adlandırılmış bir dosyaya kaydetmeyle sınırlıdır. SaveAs'ı bunun yerine bir TStream'e karşı çağırın, HotXLS size verdiğiniz akışa doğrudan yazar, aşamalandıracak veya koruyacak bir hedef dosya olmadan; çünkü o akışın dayanıklılığı (bir bellek arabelleği, bir ağ yüklemesi, bir veritabanı blob'u) o noktadan itibaren tamamen sizin kodunuzun sorumluluğundadır

Bir doğrulama geçişi bundan sonra tam olarak bu garantiye dayanabilir; bir çalışma kitabı denetimi ve dönüştürme çalışma tezgahına yerleşik olanı da dahil: yeniden açıldığında eksik veya kayıp çıkan bir dosya, kovalanacak gerçek bir dönüştürme sorunudur, asla yarıda kesilip diskte belirsiz bir şey bırakan bir kayıt değildir. Çökmeye dayanıklı aşamalı yazmalar, Delphi ve C++Builder için HotXLS Bileşeni'nin ürettiği her XLSX, ODS ve klasik XLS çalışma kitabı için SaveAs'a yerleşiktir, açmak için hiçbir yapılandırma gerekmez