همه به‌دنبال به‌روزرسانی‌های آنی هستند تا زمانی که متوجه شوند «سریع» و «درست» یکی نیستند. در یک سیستم توزیع‌شده، رویدادها می‌توانند با سرعت نور حرکت کنند و باز هم با ترتیب اشتباه برسند. WebSocketها قطع و وصل می‌شوند. Message brokerها بسته‌ها را دوباره ارسال می‌کنند. ورکر‌های پس‌زمینه با لغو شدن به دلیل اتمام زمان (timeout) مسابقه می‌گذارند. نتیجه؟ کلاینت ممکن است رویداد ۴۲ را ببیند، سپس رویداد ۴۰، و بعد از یک snapshot مطلع شود که سیستم در حال حاضر در رویداد ۴۵ است. اگر در حال ساخت گردش‌کارهای عامل (agent workflows) طولانی‌مدت هستید، این آشفتگی یک مورد استثنایی نیست، بلکه وضعیت پایه است. قبل از اینکه نگران صرفه‌جویی در میلی‌ثانیه‌های ارسال باشید، ترتیب رویدادهای خود را اصلاح کنید.

واقعیت آشفته‌ی «در لحظه» (Real-Time)

«در لحظه» بودن یک ویژگی انتقال (transport property) است. این ویژگی توصیف می‌کند که یک بسته با چه سرعتی روی سیم حرکت می‌کند، نه اینکه داستانی که روایت می‌کند منطقی است یا خیر. وظایف طولانی‌مدت هر ناهماهنگی را تشدید می‌کنند زیرا در طول زمان گسترده شده‌اند. یک وظیفه آموزش مدل، یک جریان تأیید چند مرحله‌ای، یا یک خط لوله رندر ویدیو ممکن است طی دقایق یا ساعت‌ها ده‌ها رویداد صادر کنند. در طول این بازه، هر چیزی ممکن است اشتباه پیش برود.

یک broker ممکن است یک پیام را دوباره تلاش (retry) کند چون یک تأییدیه (acknowledgement) گم شده است. یک load balancer ممکن است دو رویداد را از مسیرهای شبکه متفاوتی هدایت کند و اجازه دهد رویداد جدیدتر زودتر برسد. یک فرآیند ورکر ممکن است پس از نوشتن در پایگاه داده اما قبل از انتشار رویداد موفقیت، از کار بیفتد، و سپس ورکر دومی وظیفه را بردارد و پیشرفت خودش را اعلام کند. اگر فرانت‌اند شما فرض کند که آخرین پیام، معتبرترین پیام است، وضعیتی را نمایش می‌دهد که هرگز وجود نداشته است. کاربران شاهد چشمک زدن نشان «تکمیل شده» و بازگشت آن به حالت «در حال پردازش» خواهند بود، یا بدتر از آن، یک وظیفه لغو شده ناگهان دوباره احیا می‌شود. سرعت بدون ترتیب، چیزی جز سردرگمی با نرخ فریم بالاتر نیست.

شماره‌های توالی، ساعت واقعی هستند

راه حل، استفاده از شماره‌های توالی صعودی (monotonic) و سخت‌گیرانه‌ای است که توسط تولیدکننده (producer) ایجاد می‌شود. هر عملیاتی که وضعیت را تغییر می‌دهد، شماره‌ای دریافت می‌کند که دقیقاً یک واحد افزایش می‌یابد، بدون هیچ شکاف یا بازگشتی (rollback). این شماره باید در همان تراکنشی (transaction) ذخیره شود که خودِ رویداد ذخیره می‌شود. اگر ردیف پایگاه داده به‌روزرسانی شود اما ثبت (commit) توالی با شکست مواجه شود، باید هر دو را به حالت قبل برگردانید (roll back). این کار باعث می‌شود خط زمانی منطقی با تغییر وضعیت، اتمیک (atomic) باقی بماند.

شناسه‌های رویداد (Event IDs) هنوز مفید هستند، اما آن‌ها مشکل متفاوتی را حل می‌کنند. یک Event ID یک payload خاص را شناسایی می‌کند تا بتوانید هنگام ارسال دوباره همان پیام توسط broker، از تکرار آن جلوگیری کنید (deduplicate). از سوی دیگر، یک شماره توالی به شما می‌گوید که آن payload در زنجیره علّی کجا قرار دارد. شماره توالی شکاف‌ها را آشکار می‌کند. ترتیب را آشکار می‌کند. یک برچسب زمانی (timestamp) هیچ‌کدام از این کارها را انجام نمی‌دهد. ساعت‌ها دچار انحراف (drift) می‌شوند، NTP به عقب برمی‌گردد و ماشین‌های مجازی متوقف می‌شوند. از برچسب‌های زمانی فقط برای مقاصد نمایشی استفاده کنید، چیزی شبیه به «۳ دقیقه پیش شروع شد»، و هرگز از آن‌ها به عنوان کلید مرتب‌سازی برای منطق تجاری (business logic) استفاده نکنید.

کلاینت چگونه باید استریم را مدیریت کند

زمانی که تولیدکننده یک توالی صعودی را تضمین کرد، مصرف‌کننده (consumer) قوانین ساده و سختی خواهد داشت. اگر شماره توالی ورودی کمتر یا مساوی با آخرین شماره اعمال‌شده بود، آن را رها کنید (drop). آن پیام یا تکراری است یا یک پیام قدیمی و دیررس. اگر توالی دقیقاً یک واحد بزرگتر از آخرین شماره اعمال‌شده بود، بلافاصله آن را اعمال کنید. این همان مسیر ایده‌آل (happy path) است. اگر توالی جهش کرد، مثلاً انتظار ۱۲ را داشتید اما ۱۵ دریافت کردید، چیزی مفقود شده است. رویداد جدید را بافر (buffer) کنید و از سرور بخواهید بازپخش (replay) را از توالی بعدی مورد انتظار شروع کند. حدس نزنید. با امید به اینکه شکاف اهمیتی ندارد، از آن عبور نکنید.

وضعیت‌های نهایی (Terminal states) باید غیرقابل بازگشت تلقی شوند. وقتی یک وظیفه با برچسب «تکمیل شده»، «شکست خورده» یا «لغو شده» مشخص شد، کلاینت باید هرگونه تغییر وضعیت بعدی برای آن عملیات را رد کند. این موضوع بدیهی به نظر می‌رسد تا زمانی که با...