دانشجو که بودم، برای استفاده از سلف غذا خوری دانشگاه باید همیشه از کارت دانشجویی موقع تحویل غذا استفاده میکردیم. کارت رو به دستگاه نزدیک میکردیم و اگر غذا، از قبل رزرو شده بود با یک چراغ سبز به مسئول سلف نشون میداد که باید به آقای فلانی غذا تحویل بشه. طبق قانون هم، با هر حساب کاربری فقط یک غذا میشد رزرو کرد. علاوه بر روش رایج استفاده از کارت دانشجویی، داخل لابی سلف غذاخوری، دستگاه دیگه ای وجود داشت که به دستگاه فیش فراموشی مشهور بود. این دستگاه برای مواقعی بود که شما فراموش کرده بودین کارت دانشجوییتون رو همراهتون بیارین. با وارد کردن شماره دانشجویی و رمز اکانتتون دستگاه به شما یک فیش کاغذی برای تحویل غذا میداد که جایگزین کارت الکترونیکی محسوب میشد.
اما هنوز به نکته جالب ماجرا نرسیدیم :)
ماجرا از اونجایی جذاب میشه که یکروز من و دوستم تصمیم گرفتیم این دستگاه رو در برابر یک موضوع امنیتی تست کنیم! قضیه از این قرار بود که وقتی شما دکمه شروع دستگاه رو میزدی مراحل زیر اتفاق میفتاد:
همونطور که گفتم، داخل لابی سلف غذاخوری دو تا از این دستگاه ها دقیقا کنار هم وجود داشت. ما یک ترفندی رو تست کردیم و کاملا هم جواب داد. ترفند ما این شکلی بود:
من و دوستم همزمان روبروی دو دستگاه می ایستادیم. هر دومون اطلاعات حساب یکی از ما (مثلا حساب من) رو وارد دستگاه میکردیم. وقتی من اطلاعاتم رو میزدم و سابمیت میکردم وارد صفحه چاپ فیش میشدم و دستگاه فقط منتظر این بود که دکمه چاپ فیش رو بزنم و فیش چاپ بشه (در واقع مرحله چک کردن رو پشت سر گذاشته بودم). در این مرحله دستگاه هنوز کانتر حساب من رو پر نکرده بود و در واقع من هنوز از نظر دستگاه غذام رو نگرفته بودم. در همین حالت رفیق من توی اون یکی دستگاه اطلاعات من رو وارد میکرد و چون هنوز تو دیتابیس غذای من به عنوان دریافت شده ثبت نشده بود اون هم مرحله چک رو رد میکرد و وارد صفحه چاپ فیش میشد. این طوری هر دو ما راحت با یک اکانت دو تا فیش غذا چاپ میکردیم و در واقع با یک حساب دو تا غذا میگرفتیم!
اون موقع نمیدونستم که اسم این باگ سیستم چیه و البته زیاد هم جز یکی دو بار که مهمون داشتیم و غذا کم آورده بودیم از این ترفند استفاده نکردیم :)
بعد از مدت ها فهمیدم که به این اتفاق (یا حمله) Race Condition گفته میشه.در واقع مشکل اونجا بود که دستگاه اجازه میداد دو نفر همزمان فرایند رو طی کنند در حالیکه هنوز تکلیف یکیشون مشخص نشده!
راه حل ساده ش هم در واقع این بود که الگوریتم دستگاه تغییر کنه و تا زمانی که یک یوزر در حال استفاده از دستگاه هست توی شبکه اجازه داده نشه که یک نفر با همون حساب کاربری وارد فرایند بشه، تا زمانیکه تکلیف اولی مشخص بشه.
همونطور که گفتم به این فرایند Race Condition گفته میشه و در این پست میخوام راه رفع و جلوگیری از اون رو پیشنهاد بدم. پس با من همراه باشین.
و حالا میرسیم به قسمت شیرین راه حل مسئله. فرض کنین کدی که برای چک کردن وضعیت رزرو دانشجو و چاپ در خروجی نوشته شده یه چیزی شبیه به کد پایین باشه:
همونطور که در کد بالا مشخصه وقتی دو درخواست همزمان برای یک حساب کاربری سمت سیستم بیاد، اتفاقی که میفته اینه که وقتی پردازش اولی هنوز به خط ۲۱ نرسیده و وضعیت غذای کاربر به استفاده شده تغییر نکرده درخواست بعدی میاد و هر دو فرایند چک کردن وضعیت رو با موفقیت طی میکنن. که طبیعتا ما میخوایم جلوی این اتفاق رو بگیریم.
به کمک این روش ما میایم و فرایند مورد نظر رو به ازای آیدی کاربر قفل میکنیم. به این صورت که وقتی یک درخواست با یک حساب کاربری در حال اجراس، اجازه نمیدیم که درخواست همزمان دیگه ای با همون حساب کاربری وارد سیستم بشه و فرایند مشابه رو طی کنه. وقتی کار یوزر اول تموم شد و نتیجه مشخص شد، بعد از اون قفل رو برای نفر بعدی باز میکنیم. به همین سادگی! یه چیزی تو مایه های صف به ازای هر کاربر!
توی Node، یکی از مشهورترین پکیج های Mutex پکیج async-mutex هست. برای استفاده از این پکیج میتونین مطابق کد پایین عمل کنین:
برای توضیح کد بالا باید بگم که ما اومدیم یک Map تعریف کردیم که بتونیم به ازای هر کاربر یک قفل جدید بسازیم و داخل اون ذخیره کنیم. به این صورت که کلید میشه آیدی کاربری و مقدارش میشه برابر با قفلی که برای اون کاربر ساختیم.
بعد از اون داخل متد getVoucher، اول چک میکنیم که اگر کاربر درخواست دهنده قفل فعال نداره یک قفل جدید براش بسازیم. بنابراین هر کاربری که درخواستش وارد متد getVoucher میشه، بر اساس آیدیش یک قفل براش ایجاد میشه و تا جایی که نتیجه نهایی مشخص نشه و بلاک finally اجرا نشه و قفل release نشه کاربر بعدی اجازه ورود به این فرایند (با همون حساب کاربری) رو نداره.
دقت کنین که تو نمونه کد بالا ما تابع release یا همون باز شدن قفل کاربر رو توی بلاک finally اجرا کردیم تا در هر صورت (چه موفقیت آمیز بودن فرایند و چه برخورد با خطا) اجرا بشه و قفل باز بشه تا کاربر بتونه دوباره فرایند رو انجام بده.
این باگ در خیلی از سیستم ها وجود داره و میشه با ارسال چند درخواست همزمان به یک سرویس شرط چک کردن اعتبار رو دور زد. و ساده ترین راهش هم استفاده از Mutex Lock هست که در بالا توضیح داده شد. دقت کنید که در مثال بالا ما برای ذخیره کردن Lock ها از مموری استفاده کردیم و باید دقت کنید که در سیستم های بزرگ ممکنه این روش (اگر کنترل شده نباشه) منجر به Memory Leak بشه.
امیدوارم که از این مقاله استفاده کرده باشین. اگر در متن مقاله و نمونه کدها اشتباهی دیدین ممنون میشم اطلاع بدین تا اصلاح بشه.