دو چیز که وضعیت نامیده میشوند
یک چکپوینت در رانتایم یک عامل معمولاً مکانیکهای یک اجرا را ثبت میکند: کدام گره از گراف اجرا شد، کدام ابزار فراخوانی شد، ابزار چه چیزی برگرداند، کدام یال در انتظار است. این وضعیت اجراست. برای پاسخ به این پرسش کافی است: «ماشین کجا بود؟»
برای پاسخ به این پرسش کافی نیست: «ماشین در تلاش بود چه کاری انجام دهد، و تحت چه شرایطی؟» آن چیز دوم وضعیت قصد است: هدف آنگونه که اکنون فهمیده میشود، محدودیتهایی که اجرا پذیرفته است، فرضهایی که پیشتر به آنها متعهد شده، و تعهداتی که هنوز ایفا نکرده است. وضعیت اجرا یک مکاننماست. وضعیت قصد یک قرارداد است.
موتورهای اجرای پایدار در مکاننما خوب هستند. آنها ژورنالی از گامها را بازپخش میکنند، کارهای انجامشده را رد میکنند، و کنترل را در نخستین گام ناتمام بازمیگردانند. این رفتار درست برای گردشکاری است که معنایش در زمان تعریف ثابت شده است. یک عامل چنین نیست. معنایش در هر گام از تاریخچهای که میبیند بازساخته میشود، و تاریخچه همان چیزی نیست که آن را تولید کرده است. مکاننما را بدون قرارداد بازیابی کنید و اجرا کرش نمیکند. روان ادامه مییابد، در جهت اشتباه، با همان لحن مطمئنی که پیش از وقفه داشت.
بازبرنامهریزی از یک هدف کهنه
یک اجرا در ابتدا محدودیتی را میپذیرد: فقط-خواندنی عملکردن روی یک دیتاست تولیدی، یا محدودکردن دامنه به یک مستأجر واحد. در میانهی اجرا، اپراتور دامنه را گسترش میدهد و محدودیت در پیکربندی ارکستراتور بهروزرسانی میشود. فرایند پیش از گام بعدی از بین میرود. در بازگشت، چکپوینت شامل شاخصهای گام و خروجیهای ابزار است اما نه محدودیتی که در آن زمان حاکم بود. عامل برنامهاش را از تاریخچهی بازمانده و پیکربندی فعلی بازاستنتاج میکند، و این دو با هم اختلاف دارند. هیچچیز در ترنسکریپت آن درز را علامت نمیزند. برنامهی حاصل بهطور درونی سازگار و بهطور بیرونی غلط است.
اثرات جانبی تکراری
مورد کلاسیک: آخرین فراخوانی ابزار موفق شد اما تأییدیه گم شد. موتور اجرای پایداری که در نخستین گام ناتمام بازمیگردد، فراخوانی را دوباره صادر میکند. اگر فراخوانی charge_card یا send_email یا create_ticket باشد، اجرا اکنون دو نسخه دارد. این شکست استدلال نیست؛ شکست قرارداد چکپوینت است. ژورنال قصد فراخوانی را ثبت کرده، نه واقعیت اثر را، و این دو تنها در صورتی قابل تفکیکاند که خود اثر کلیدی داشته باشد که سیستم پاییندستی آن را بشناسد.

رانش فرض
با تاریخچههای یکسان، یک عامل همیشه برنامههای یکسانی بازاستنتاج نمیکند. نمونهگیری، برش کانتکست، و تغییرات نسخهی مدل همگی توزیع را جابهجا میکنند. بازگشتی که تاریخچه را بازپخش میکند و از مدل میخواهد ادامه دهد، برنامهای تولید میکند که با توجه به تاریخچه محتمل است اما همان برنامهای نیست که در جریان بود. رانش فرض نسخهی خاموش شکست هدف کهنه است: هیچ پیکربندیای تغییر نکرده، فقط بازسازی. اجرا سالم به نظر میرسد و واگرا میشود.
یک قرارداد چکپوینت کارآمد
راهحل این است که چکپوینت را بهعنوان سریالسازی قصد ببینیم، با وضعیت اجرا بهعنوان یکی از چندین فیلد. چکپوینتی که بتوان با اطمینان بازگرداند باید، بهطور همزمان و اتمیک، حامل اینها باشد:
- هدف، به شکلی که اجرا اکنون به آن متعهد است، نه شکلی که با آن راهاندازی شد.
- محدودیتهای پذیرفتهشده، هرکدام با نقطهای در اجرا که در آن پذیرفته شد و منبعی که آن را اظهار کرد.
- نتایج ابزار، کلیدشده با یک کلید idempotency بهجای ترتیب فراخوانی.
- تعهدات حلنشده: آنچه اجرا وعده داده انجام دهد اما هنوز انجام نداده است.
- یک نسخهی اسکیما، تا چکپوینتی که توسط رانتایم قدیمیتر نوشته شده بهعنوان چنین چیزی قابل تشخیص باشد.
مکانیزمی که این را در عمل کارآمد میکند یک کلید idempotency در دامنهی اجراست که از ورودیهای پایدار مشتق میشود، نه از ساعت دیواری یا توالی:
key = hash(run_id, step_name, canonical_args)
هر فراخوانی ابزار با اثر جانبی این کلید را حمل میکند. سیستم پاییندستی، یا یک آداپتور نازک در مقابل آن، کلیدهایی را که پیشتر اجرا کرده ثبت میکند و در بازپخش، نتیجهی پیشین را برمیگرداند بهجای اینکه اثر را دوباره انجام دهد. کلید باید در طول تلاشهای مجدد پایدار و در میان قصدهای متمایز یکتا باشد؛ مشتقکردن آن از نام گام و آرگومانهای کانونیکال بهجای یک شمارنده چیزی است که بازپخش را پس از ریاستارتی که گامها را بازآرایی یا رد میکند ایمن میسازد.
مکانیزم دوم امتناع است. یک چکپوینت نسخهی اسکیما را حمل میکند، و رانتایم از بازگشت در میان نسخههای ناسازگار امتناع میکند بهجای تلاش برای مهاجرت با بهترین تلاش. مهاجرت قصد یک بازبرنامهریزی است، و بازبرنامهریزی باید یک عملیات صریح و لاگ