برای جایگزینیِ خروجیِ یک پردازش، لازم نیست از ابتدا روی فایلِ نهایی بنویسیم. میتوان نسخهٔ تازه را در فایلِ مرحلهایِ همان پوشه ساخت، بست، دوباره خواند و پس از تأیید جایگزین کرد. اما موفقیتِ این مسیر را با دوام در قطعِ برق یا تراکنشِ چندفایلی یکی نگیریم. تمرین حاضر فقط چند فایلِ کوچکِ کاملاً تازه میسازد و دو شکستِ قبل از جایگزینی را واقعاً آزمایش میکند.
مرزِ تمرین: فقط فایلهایِ تازهٔ خودش
کد کنارِ خودش یک پوشهٔ مشخصِ آموزشی و زیرِ آن پوشهٔ تازه با نامِ تصادفی میسازد. فایلِ هدفِ state.json همانجا و از ابتدا توسطِ تمرین ایجاد میشود. ورودیِ مسیرِ دلخواه ندارد، فایلِ موجودِ کاربر را بازنویسی نمیکند و حذفِ پوشه یا دادهٔ دیگری انجام نمیدهد. نمونهها دادهٔ مشتری یا تنظیماتِ عملیاتی نیستند.
نسخهٔ قدیمی شامل versionِ ۱ و itemsِ[A] است؛ نسخهٔ جدید versionِ ۲ و itemsِ[A,B] دارد. بایتهایِ هر دو از قبل مشخصاند و SHA-256 مستقل ثبت میشود. موفقیت فقط به معنیِ رسیدنِ همان فایلِ تازهٔ آموزشی به نسخهٔ جدید است؛ هیچ انتشارِ سایت یا ذخیرهٔ مدل با آن انجام نمیشود.
نوشتن و بررسی را پیش از جابهجایی انجام دهیم
هر تلاش یک فایلِ stageِ تازه در همان پوشه دارد. با حالتِ xb باز میشود تا فایلِ ازپیشموجود جایگزین نشود. داده نوشته، buffer با flush خالی و سپس os.fsync روی descriptor فراخوانی میشود. فایل بسته میشود و دوباره از روی مسیر خوانده میشود؛ صرفِ برابربودنِ متغیرِ حافظه را شاهدِ فایلِ نوشتهشده نمیگیریم.
پس از خواندن، JSON باید دقیقاً با objectِ موردانتظارِ نسخهٔ۲ برابر باشد. این اعتبارسنجی کوچک، قراردادِ اصیلِ fixture است و جای schemaِ عمومیِ هر پروژه را نمیگیرد. اگر همهٔ کنترلها عبور کردند، os.replace فایلِ stage را به جای هدف مینشاند و هدف دوباره بازخوانی میشود.
دو شکست، قبل از مرزِ جایگزینی
تلاشِ اول متنِ JSONِ شکسته دارد و هنگامِ خواندن JSONDecodeError میدهد. تلاشِ دوم بایتِ درست دارد اما عمداً درست پیش از replace خطایِ RuntimeError تزریق میکنیم. پس از هر دو شکست، بایتهایِ هدف دقیقاً همان نسخهٔ۱ هستند؛ فقط فایلِ stageِ آموزشیِ همان تلاش باقی میماند.
| تلاش | رخدادِ واقعی | هدف پس از تلاش |
|---|---|---|
| JSONِ ناقص | JSONDecodeError | نسخهٔ۱، بدون تغییر |
| شکستِ تزریقشده | RuntimeError پیش از replace | نسخهٔ۱، بدون تغییر |
| داده و بررسیِ موفق | replace و بازخوانی | نسخهٔ۲ با همان بایتِ موردانتظار |
اینها خطاهایِ کنترلشدهاند، نه شبیهسازیِ قطعِ برق یا killِ سیستم. در تلاشِ سوم، فایلِ مرحلهای پس از replace دیگر در مسیرِ قبلی نیست و هدف با versionِ ۲ خوانده میشود. کد همهٔ این انتظارها را assert کرده است؛ نتیجه فقط از پیامِ «ذخیره شد» گرفته نشده است.
اتمیبودنِ نام با دوامِ ذخیره فرق دارد
مستنداتِ os.replace دربارهٔ جایگزینی و شرطِ موفقیت، و برای atomic rename به الزامِ POSIX اشاره میکند. اجرایِ ما روی Windows و Python۳٫۱۲٫۱۴ است. از یک اجرایِ موفق روی این محیط، تضمینِ عمومیِ crash safety برای هر فایلسیستم، دستگاه، خوانندهٔ همزمان یا قطعِ برق استخراج نمیکنیم.
flush وfsync انجام شدهاند، اما خودِ وجودِ این دو فراخوانی آزمونِ دوامِ کاملِ directory entry و سختافزار نیست. JSON صریحاً power_failure_tested=False وconcurrent_reader_tested=False دارد. این مرزها را با حدس یا نامِ «ذخیرهٔ امن» پنهان نکنید. همچنین دو فایلِ جدا که یکی manifest و دیگری داده است، صرفاً با دو replace به تراکنشِ واحد تبدیل نمیشوند.
این مسیر را با آپلود یا ثبتِ پایگاه یکی نکنیم
ادامهٔ آپلود با offset وضعیتِ انتقال در سرور و رقابتِ کلاینتها را پوشش میدهد؛ اینجا انتقالِ شبکه نداریم. Savepoint در SQLite مرزِ تراکنشِ پایگاه است؛ جایگزینیِ یک نامِ فایل، همان قرارداد را برای چند منبع نمیسازد.
در نقطهٔ ادامهٔ پردازش سند، نتیجه و وضعیتِ تکمیل باید سازگار بمانند. این تمرین فقط بخشِ محدودِ یک فایلِ خروجی را روشن میکند. برای ادامه به مبانی هوش مصنوعی بروید و پیش از استفادهٔ واقعی، دامنهٔ فایل، رفتارِ خطا و معیارِ بازخوانیِ موفق را بنویسید؛ وجودِ چنین سازوکاری در محصول اپرا از این مثال نتیجه نمیشود.
فایل تمرین و منبع
کد مستقلِ تمرینِ محدود · تمام خروجیِ واقعیِ همین تمرین؛ JSON
منابع در 2026-11-04 خوانده شدند؛ این تاریخ، تاریخ انتشار منبع نیست. مثالها و فایلهای این راهنما آموزشی و ساختهٔ تحریریهاند.