چطور OLX جلوی آپلودهای تکراری بین تبها رو گرفت
خلاصهٔ کاملتر
سزار کونترراس، مهندس فرانتاند OLX، تو این مقاله تجربهٔ حل یک مسئلهٔ همروندی واقعی رو تعریف میکنه؛ خودش میگه از دنیای برنامهنویسی همروند با جاوا اومده و فکر نمیکرد تو وب دوباره با قفل و سمافور و دِدلاک روبهرو بشه. مسئله این بود: OLX باید آپلود فایلهای بزرگ تا ۲۰ گیگابایت رو پشتیبانی میکرد و نمیخواستن کاربر مجبور باشه تا آخر آپلود تو اپ بمونه. یعنی اگه مرورگر یا تب رو ببنده و باز کنه، آپلود باید بدون دخالت دستی ادامه پیدا کنه؛ پس سراغ مکانیزم ازسرگیری (resume) رفتن.
اینجا کار خراب میشه: اگه کاربر چند تب از اپ رو باز کنه، هر تب همون آپلود رو بازیابی و از سر میگیره. نتیجهاش آپلودهای تکراری روی باکت S3، مصرف بیخود پهنای باند، هزینهٔ پردازش اضافه و کاربریه که میبینه درصد پیشرفت آپلودش بیقاعده بالا و پایین میپره.
نویسنده اول یادآوری میکنه قفل چیه: مکانیزمی که تضمین میکنه یک منبع مشترک هر بار فقط دست یکیه و هر فراخوان تازهترین وضعیت رو میبینه. مثالش بانکیه — چند خودپرداز که همزمان روی یک حساب برداشت و واریز و ماندهگیری میکنن؛ بدون هماهنگی نتیجه فاجعهست، و با قفل، اولین کسی که قفل رو میگیره کارش رو تموم میکنه و بقیه صبر میکنن.
بعد دو گزینهٔ رایج هماهنگی بین تبها رو رد میکنه. اول localStorage بههمراه polling: عملیات خواندن و نوشتنش اتمی نیست پس شرط مسابقه (race condition) پیش میآد، اگه تبی کرش کنه قفل برای همیشه بیات میمونه، باید مدام وضعیت قفل رو چک کنی، و رویداد storage فقط تو تبهای دیگه شلیک میشه نه تب فعلی.
گزینهٔ دوم Broadcast Channel API ـه که با پیامرسانی بین تبها هماهنگ میکنه. مشکلش اینه که هیچ اولیهٔ mutex نداره و باید یک ماشین حالت از صفر بسازی، و heartbeat و تشخیص کرش و رفع تعارض رو خودت دستی مدیریت کنی — کاری مستعد خطا. به نظر نویسنده Web Locks API دقیقاً همون چیزیه که لازم بود: یک mutex واقعی و اتمی داخل خود مرورگر. گرفتن قفل اتمیه پس شرط مسابقه نداره، با بسته شدن یا کرش تب قفل خودکار آزاد میشه، و حالت غیرمسدودکننده داره تا UI فریز نشه.
پیادهسازیشون یک هوک ریاکت به اسم useUploadCoordinator ـه با دو راهبرد متفاوت. برای آپلود تازهای که خود کاربر شروع کرده، با ifAvailable: true قفل رو امتحان میکنن ولی در هر حالت آپلود ادامه پیدا میکنه — کار کاربر هیچوقت مسدود نمیشه. اما برای آپلود بازیابیشده فقط وقتی ادامه میدن که قفل رو گرفته باشن:
navigator.locks.request(LOCK_NAME, { ifAvailable: true }, async (lock) => {
if (lock) {
uploader.emit("restore-confirmed");
await holdLockUntilComplete(abortController);
}
// اگه قفل نگرفتیم، بیصدا رد میشیم؛ یک تب دیگه داره کارو انجام میده
});نگه داشتن قفل هم با یک promise انجام میشه که تا رسیدن یکی از رویدادهای پایانی — complete، error یا cancel-all — یا unmount شدن کامپوننت (از طریق AbortController) resolve نمیشه؛ و همون resolve شدن، آزاد شدن قفله. برای مرورگرهایی هم که locks رو ندارن یک بررسی سادهٔ وجود navigator.locks گذاشتن و در نبودش اجازه میدن بازیابی انجام بشه و ریسک تکراری شدن رو میپذیرن — به گفتهٔ خودشون افت آبرومندانه بهتر از قابلیت شکستهست.
نتیجهای که گزارش میکنه: صفر آپلود تکراری از بابت هماهنگی بین تبها، تجربهٔ بهتر چون منطق هماهنگی هیچوقت آپلود رو قفل نمیکنه، هزینهٔ زیرساخت کمتر و کدبیس سادهتر بدون ماشین حالت و polling. سه درسی هم که میگیره: هرگز کار کاربر رو مسدود نکن، امن شکست بخور، و ساده نگه دار — از اولیههای خود پلتفرم استفاده کن بهجای بازسازیشون. این الگو بهجز آپلود، برای همگامسازی پسزمینه و هر عملیات singleton که باید فقط تو یک تب اجرا بشه هم جواب میده.
نکات کلیدی:
- آپلود قابل ازسرگیری تا ۲۰ گیگابایت، با مشکل اجرای همزمان تو چند تب
- localStorage اتمی نیست و قفل بیات میسازه؛ Broadcast Channel هم mutex آماده نداره
- Web Locks API قفل اتمی میده و با کرش یا بسته شدن تب خودکار آزادش میکنه
- آپلود تازه با قفل غیرمسدودکننده پیش میره، آپلود بازیابیشده فقط با گرفتن قفل
- نتیجه: حذف آپلودهای تکراری، هزینهٔ کمتر و کد سادهتر بدون polling




