مقاله فنی

ذخیره‌های ایمن در برابر کرش در HotXLS: فایل‌های موقت مرحله‌ای در Delphi

ذخیره‌ای که در میانه‌ی راه می‌میرد، چه از یک راه‌اندازی مجدد اجباری، چه یک فرآیند کشته‌شده، چه یک دیسک که در میانه‌ی نوشتن پر می‌شود، سنتاً برای قالبی که حول نوشتن‌های درجا ساخته شده، به یک معنا بوده: هر بایتی که پیش از قطع‌شدن به دیسک رسیده همان چیزی است که پس می‌گیرید، و یک کاربرگ بریده‌شده دیگر باز نمی‌شود. HotXLS این حالت شکست را با یک مسیر ذخیره‌ی ایمن در برابر کرش که برای هر فایل XLSX، ODS، و XLS کلاسیک که می‌نویسد استفاده می‌شود، می‌بندد. هر فراخوانی SaveAs فایل جدید کامل را در یک فایل موقت که کنار مقصد ساخته شده می‌نویسد، سپس آن را با یک تغییرنام اتمی تکی MoveFileExW از API ویندوز commit می‌کند، پس یک ذخیره‌ی قطع‌شده فقط می‌تواند در تولیدکردن فایل جدید شکست بخورد، هرگز فایلی را که از پیش داشتید خراب نمی‌کند. همان انضباط مرحله‌سپس-جایگزینی به‌طور یکنواخت در سراسر هر دو موتور ذخیره‌ی HotXLS اجرا می‌شود، نویسنده‌ی BIFF8 پشت XLS کلاسیک و نویسنده‌ی OOXML پشت XLSX و ODS، و این یک الگو است ارزش قرض‌گرفتن برای هر فایلی که کد خودتان در Delphi مستقیماً بازنویسی می‌کند، صفحه‌گسترده یا نه

اگر یک ذخیره‌ی کاربرگ در میانه‌ی راه قطع شود چه اتفاقی می‌افتد؟

پاسخ مستقیم این است که کاملاً بستگی دارد به اینکه نویسنده چطور فایل مقصد را لمس می‌کند، و پیاده‌سازی رایج، بازکردن فایل هدف و جریان محتوای جدید مستقیم درون آن، تا زمانی که هیچ چیزی اشتباه نرود خوب است. همان لحظه‌ای که چیزی اشتباه برود، یک کرش، یک kill اجباری فرآیند، یک اشتراک شبکه‌ای که در میانه‌ی نوشتن قطع می‌شود، فایل روی دیسک در هر وضعیت میانی‌ای که نویسنده به آن رسیده رها می‌شود: یک directory مرکزی ZIP که هرگز برای XLSX یا ODS پیوست نشده، یا یک جریان BIFF که رکوردهایی را که یک خواننده انتظار دارد برای XLS کلاسیک ندارد. اکسل آن را با ظرافت تعمیر نمی‌کند، و هیچ مصرف‌کننده‌ی دیگری که یک فایل کامل انتظار دارد هم این کار را نمی‌کند، پس نتیجه‌ی عملی یک کاربرگ است که دیروز خوب باز می‌شد و امروز از بازشدن سر باز می‌زند

HotXLS چطور هر ذخیره را پشت یک جایگزینی اتمی مرحله‌بندی می‌کند

HotXLS هرگز فایل مقصد را برای هیچ‌کدام از سه قالبی که ذخیره می‌کند مستقیم برای نوشتن باز نمی‌کند. توالی هر بار همان شکل است: خروجی کامل را جایی بسازید که فایلی نیست که کاربر از پیش روی دیسک دارد، و فقط زمانی آن را به جایش منتقل کنید که آن ساخت کاملاً موفق شده باشد. مشخصاً، SaveAs یک فایل موقت خالی در همان پوشه‌ی مسیر هدف می‌سازد، کل کاربرگ جدید را درون آن فایل موقت می‌نویسد، و فقط پس از اینکه آن نوشتن بدون خطا برگردد، فایل موقت را با یک تغییرنام تکی روی مقصد commit می‌کند. هیچ‌کدام از این‌ها به یک ویژگی برای انصراف نیاز ندارد؛ این صرفاً کاری است که SaveAs برای یک مسیر فایل ساده، در هر فراخوانی انجام می‌دهد

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;

همان انضباط روی نویسنده‌ی XLS کلاسیک هم اعمال می‌شود، نه فقط نویسنده‌ی OOXML، و آن دو فایل موقت حتی یک قرارداد نام‌گذاری را به اشتراک می‌گذارند: هر دو API ویندوز GetTempFileNameW را با پیشوند hxl فرا می‌خوانند، پس یک ذخیره که پیش از پاک‌سازی قطع شود می‌تواند یک فایل سرگردان با نامی مثل hxl4C2A.tmp کنار کاربرگ شما به‌جا بگذارد. آن فایل خرابی نیست، شواهدی است که سازوکار دقیقاً همان‌طور که طراحی شده کار کرده: نوشتن ناقص همان‌جا متوقف شده، و کاربرگ واقعی شما اصلاً برای نوشتن باز نشده بود. دیدن یکی پس از یک کرش، ایمن برای حذف است و چیزی برای بررسی نیست

چرا فایل موقت را کنار کاربرگ مرحله‌بندی کنیم نه در %TEMP%؟

پاسخ کوتاه این است که تغییرنام MoveFileExW فقط زمانی اتمی است که منبع و مقصد روی همان جلد بنشینند، و مطمئن‌ترین راه تضمین آن بدون اینکه از فراخواننده بخواهید چیزی پیکربندی کند، این است که موقعیت فایل موقت را از خودِ مسیر مقصد مشتق کنید. HotXLS پوشه‌ی خودِ هدف را محاسبه می‌کند و آن دایرکتوری را مستقیم به GetTempFileNameW می‌سپارد، پس فایل موقت همیشه روی همان درایو، همان جلد، فایلی که در شرف جایگزینی آن است، برای هر ذخیره‌ای به‌طور خودکار ساخته می‌شود. اگر کتابخانه به‌جای آن نوشتن‌ها را در پوشه‌ی موقت سیستم مرحله‌بندی می‌کرد، یک مسیر هدف روی یک درایو متفاوت یا یک جلد شبکه‌ی نگاشته‌شده، گام نهایی را به یک عملیات بین-جلدی تبدیل می‌کرد، که API ویندوز یا کاملاً آن را رد می‌کند یا، اگر فراخواننده صریحاً با یک پرچم اضافی که HotXLS اینجا تنظیم نمی‌کند انتخاب کند، بی‌سروصدا به یک کپی غیراتمی به‌دنبال یک حذف تنزل می‌یابد، که دقیقاً همان پنجره‌ی قطع‌شدنی را دوباره باز می‌کند که کل این سازوکار برای بستنش وجود دارد

گام commit: MoveFileExW، write-through، و اینکه در شکست چه اتفاقی می‌افتد

گام نهایی هر ذخیره دقیقاً یک فراخوانی API ویندوز است، MoveFileExW، که دو پرچم حمل می‌کند که هرکدام کار متمایزی انجام می‌دهند. MOVEFILE_REPLACE_EXISTING چیزی است که اجازه می‌دهد تغییرنام روی فایلی که از پیش وجود دارد بنشیند؛ بدون آن، یک تغییرنام که یک مسیر موجود را هدف می‌گیرد به‌سادگی شکست می‌خورد، که کل هدف یک ذخیره را که قرار است یک کاربرگی را که از پیش دارید جایگزین کند، شکست می‌داد. MOVEFILE_WRITE_THROUGH دوام را پوشش می‌دهد: به تابع می‌گوید تا زمانی که انتقال واقعاً روی دیسک کامل نشده برنگردد، به‌جای اینکه به‌محض صف‌بندی‌شدن صرف تغییرنام برگردد، که یک race باریک‌تر اما واقعی را می‌بندد که در آن یک کرش بلافاصله پس از برگشتن SaveAs همچنان می‌توانست جایگزینی را در حال پرواز بگیرد. اگر فایل موقت نتواند ساخته شود، یا تغییرنام نهایی به هر دلیلی شکست بخورد (یک مسئله‌ی مجوز، یک مقصد قفل‌شده، یک عدم‌تطابق جلد)، HotXLS خودش فایل موقت را حذف می‌کند به‌جای اینکه زباله پشت سر بگذارد، و فایل مقصد دقیقاً همان‌طور که پیش از فراخوانی بود رها می‌شود

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 قرارداد بازگشتی مشترک در سراسر HotXLS را نگه می‌دارد، یک در موفقیت، یک عدد منفی در شکست، اما یک عدد صحیح ساده نمی‌گوید چرا یک ذخیره شکست خورده، و رفتار یکسان با هر نتیجه‌ی منفی، اطلاعاتی را که یک سیاست تلاش‌مجدد واقعاً می‌تواند استفاده کند دور می‌ریزد. ویژگی LastDiagnostic، و مجموعه‌ی کامل‌تر Diagnostics پشت آن، پیامی را حمل می‌کند که HotXLS داخلاً تولید کرده، و یک فایل موقت که نتوانسته ساخته شود را از یک تغییرنامی که ویندوز رد کرده تفکیک می‌کند. یک job دسته‌ای که Code و Message را روی هر SaveAs شکست‌خورده لاگ می‌کند، دقیقاً همان شواهدی را می‌سازد که آن یک‌بار که مشتری‌ای گزارش می‌دهد یک ذخیره بی‌سروصدا هیچ کاری نکرده، می‌خواهید

XLS کلاسیک با حافظه هزینه می‌پردازد، XLSX و ODS با دیسک

دو موتور ذخیره از مسیرهای متفاوتی به همان نتیجه‌ی ایمن در برابر کرش می‌رسند، و این تفاوت وقتی اهمیت دارد که از پیش دارید هرکدام را برای یک job دسته‌ای بزرگ تنظیم می‌کنید. نویسنده‌ی XLS کلاسیک ابتدا کل سند مرکب OLE را در حافظه می‌سازد، با استفاده از storage ساختاریافته پشتیبانی‌شده با یک handle حافظه، و فقط آن بافر تمام‌شده را در یک نوشتن به فایل موقت خواهر منتقل می‌کند؛ استدلال در سورس خودِ HotXLS مستقیم است: ابتدا ساختن کل فایل در حافظه چیزی است که یک ذخیره‌ی شکست‌خورده یا لغوشده را از بریدن مقصد باز می‌دارد. نویسنده‌ی XLSX و ODS در عوض ورودی‌های ZIP خودش را همان‌طور که تولید می‌شوند درون فایل موقت جریان می‌دهد، همان مرحله‌بندی در سطح فایل با یک نمای حافظه‌ی متفاوت. اگر از پیش برای نگه‌داشتن exportهای بزرگ XLSX درون محدودیت حافظه‌ی یک کانتینر به StreamingWrite تکیه می‌کنید، بدانید اهرم معادل برای export XLS کلاسیک به همان شکل وجود ندارد: تضمین ایمن در برابر کرش در هر دو حالت بی‌قید و شرط است، اما یک export بسیار بزرگ .xls قدیمی صرف‌نظر از هرچیزی خروجی کامل خودش را در RAM نگه می‌دارد، معاوضه‌ای که با عمق بیشتری در مقاله‌ی ما درباره‌ی نوشتن جریانی برای jobهای دسته‌ای سرور پوشش داده شده

اعمال همان الگو خارج از HotXLS، و جایی که تضمین متوقف می‌شود

قرض‌گرفتن این الگو اغلب مسئله‌ی سیم‌کشی همان دو فراخوانی API ویندوز است که HotXLS داخلاً به آن‌ها تکیه می‌کند. GetTempFileNameW یک فایل خالی با نام یکتا در پوشه‌ای که انتخاب می‌کنید به شما می‌دهد، و MoveFileExW نوشتن تمام‌شده‌ی شما را در یک گام روی مقصد واقعی commit می‌کند؛ یک نسخه‌ی حداقلی از همان روالی که HotXLS پیش از هر SaveAs اجرا می‌کند این‌طور به‌نظر می‌رسد

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;

این تضمین لبه‌های واقعی‌ای دارد ارزش دانستن پیش از اینکه کورکورانه به آن اعتماد کنید. مرحله‌بندی یک کپی کامل پیش از جایگزین‌کردن اصلی یعنی یک ذخیره برای مدتی به فضای دیسک برای هم فایل قدیمی و هم فایل جدید نیاز دارد، تقریباً دو برابر اندازه‌ی کاربرگ در طول نوشتن، که برای یک گزارش خوب است و ارزش بررسی‌کردن دارد برای یک export چند-گیگابایتی که در برابر یک جلد تقریباً پر اجرا می‌شود. فایل موقت هم باید در همان پوشه‌ی مقصد فرود بیاید، پس هر حسابی که HotXLS زیر آن اجرا می‌شود به مجوز ساخت-فایل روی همان پوشه به‌طور مشخص نیاز دارد، نه صرفاً مجوز بازنویسی همان یک فایلی که از پیش می‌شناسد؛ یک استقراری که یک پوشه‌ی مقصد را به ویرایش‌های درجای نام‌های فایل موجود مشخص محدود می‌کند، نه دسترسی نوشتن در سطح پوشه، خواهد دید که SaveAs روی گام فایل موقت شکست می‌خورد حتی اگر نوشتن مستقیم معادل موفق می‌شد

دو مرز دیگر ارزش پرچم‌گذاری صریح دارند. یک مقصد روی یک اشتراک شبکه یا درون پوشه‌ای که با OneDrive یا کلاینتی مشابه همگام‌سازی می‌شود، می‌تواند متفاوت از NTFS محلی رفتار کند حتی اگر ویندوز همچنان آن را به‌عنوان یک جلد تکی گزارش دهد، چون درایور فایل‌سیستم جلوی آن ممکن است تغییرنام را به همان شکل پیاده‌سازی نکند؛ اگر هدف استقرار شما در سراسر یک مسیر شبکه ذخیره می‌کند، ارزش دارد یک قطع‌شدگی اجباری را آنجا به‌طور مشخص تست کنید به‌جای فرض‌کردن اینکه رفتار دیسک محلی منتقل می‌شود. و کل سازوکار به ذخیره‌کردن درون یک فایل نام‌دار محدود است. به‌جای آن SaveAs را در برابر یک TStream فراخوانی کنید، و HotXLS مستقیم درون هر جریانی که به آن سپرده‌اید می‌نویسد، بدون هیچ فایل مقصدی برای مرحله‌بندی یا محافظت، چون دوام آن جریان (یک بافر حافظه، یک آپلود شبکه، یک blob پایگاه داده) از آن نقطه به بعد کاملاً مسئولیت کد شماست

یک پاس اعتبارسنجی بعداً می‌تواند دقیقاً به همین تضمین تکیه کند، از جمله نوعی که در یک کارگاه ممیزی و تبدیل کاربرگ ساخته شده: یک فایل دوباره‌بازشده که کوتاه یا گم برمی‌گردد یک مسئله‌ی تبدیل واقعی است برای پیگیری، هرگز یک ذخیره‌ای نیست که در میانه‌ی راه قطع شده و چیزی مبهم روی دیسک باقی گذاشته. نوشتن‌های مرحله‌ای ایمن در برابر کرش درون SaveAs برای هر کاربرگ XLSX، ODS، و XLS کلاسیک تولیدشده توسط کامپوننت HotXLS برای Delphi و C++Builder ساخته شده، بدون هیچ پیکربندی لازم برای روشن‌کردنش