المقدمة

ال Race Condition حالة شائعة في عالم الـ Software تحدث عند تزاحم ال Api Request لعرض البيانات اعتمادًا على سرعة استجابة السيرفر وليس على الترتيب الزمني لإرسال الـ Api Request مما يؤدي إلى عرض البيانات بشكل خاطئ.

في المقال دا هنفهم ما هي الـ Race Conditions وكيف نتغلب عليها .


تعريف الـ Race Conditions

علشان نفهم امتى بتحصل ال Race Condition تخيل تطبيق لتحويل العملات بيسمح للمستخدمين بإضافة مبلغ واختيار عملتين لتحويلهم. وعند تغيير تغير المبلغ المراد تحويله بيتم إرسال طلب إلى API لتحويل العملة وعرض النتيجة.

تخيل مع السيناريو الاتي:

  1. المستخدم دخل المبلغ: المستخدم دخل مبلغ من المال واختار عُملة المصدر.
  2. تبدأ عملية التحويل: بيتم إرسال Request الى السرفر لتحويل المبلغ.
  3. المستخدم قام بتغير العملة الهدف: أثناء التحميل وقبل أن يصل رد من السيرفر علي ال Request الأول، قام المستخدم بتغير قيمة المبلغ المراد تحويله .
  4. يتم استقبال Response ل Request الثاني: لسبب ما في الBackend تم تأخير الرد علي ال Request الأول وتم استقبال Response من ال Request الثاني.
  5. يتم استقبال Response ل Request الأول: يصل Response على ال Request الأول الي بيحسب قيمة التحويل القديمة . 
  6. يتم عرض النتيجة الخاطئة: يتم عرض نتيجة التحويل الخاطئة للمستخدم، لأنها تعتمد على ال Request الاول وليس ال Request الثاني.

من هنا بنلاحظ إنه البيانات تم عرضها بشكل خاطئ نظرًا لأنه ال Request الأول عمل override  علي ال Request الثاني رغم إنه ال Request التاني سابقه في التسلسل الزمني .


مثال عملي علي ال Race Condition

في الأول هنلقي نظرة على الكود علشان نفهم أكثر ازاي ممكن نتفادى مشكلة زي دي, عندنا  أربع ملفات كما في الصورة

ملفات مشروع تحويل العملات

الملف الأول Index.html file

<!DOCTYPE html>
<html>


<head>
 <title>محول العملات</title>
 <link rel="stylesheet" href="/styles.css">
</head>
<body>
 <div class="container">
   <h2>محول العملات</h2>
   <input type="number" id="amount" placeholder="المبلغ">
   <select id="fromCurrency">
     <option value="USD">دولار</option>
   </select>
   <select id="toCurrency">
     <option value="EGP">جنيه مصري</option>
   </select>
   <button id="convert">تحويل</button>
   <div id="result"></div>
   <div id="loader" class="loader" style="display: none;"></div>
 </div>
 <script src="/script.js">
 </script>
</body>


</html>

الملف الثاني styles.css



.container {
 text-align: center;
 margin: 20px;
}
.loader {
 border: 16px solid #f3f3f3;
 border-radius: 50%;
 border-top: 16px solid #3498db;
 width: 50px;
 height: 50px;
 margin: auto;
 animation: spin 2s linear infinite;
}
@keyframes spin {
 0% {
   transform: rotate(0deg);
 }
 100% {
   transform: rotate(360deg);
 }
}

الملف الثالث currency.json


{
 "rates": {
   "EGP": 50
 }
}

الملف الرابع فيه أكواد ال Js  ومكون من اثنين من ال Functions

الأولي هي  ()convertCurrency ودورها أن تستجيب ل Click Event

والثانية هي ()backendApiCall ومن خلالها بنعمل Simulate ل ال delay الي بيحصل في الBackend واللي بسببه بتحصل مشكلة الـ Race Condition .

const amountInput = document.getElementById("amount");
const fromCurrencySelect = document.getElementById("fromCurrency");
const toCurrencySelect = document.getElementById("toCurrency");
const convertButton = document.getElementById("convert");
const resultDiv = document.getElementById("result");


let tryNumber = 0;
const backendApiCall = (api) => {
 return new Promise((accept) => {
   setTimeout(
     () => {
       fetch(api)
         .then((response) => response.json())
         .then((data) => {
           const amount = amountInput.value;
           const toCurrency = toCurrencySelect.value;
           const rate = data.rates[toCurrency];
           const result = amount * rate;
           return accept(result);
         });
     },
     tryNumber === 1 ? 5000 : 0
   );
 });
};


function convertCurrency() {
 tryNumber++;
 const loader = document.getElementById("loader");
 loader.style.display = "block";
 result.style.display = "none";
 backendApiCall("/currency.json")
   .then((result) => {
     resultDiv.textContent = `  جنية مصري ${result} = ${
       document.getElementById("amount").value
     } دولار`;
   })
   .catch((error) => {
     console.error("Error fetching exchange rate:", error);
   })
   .finally(() => {
     loader.style.display = "none";
     result.style.display = "block";
   });
}


convertButton.addEventListener("click", convertCurrency);

كيف نتغلب على ال Race Condition ؟

في أكثر من طريقة للتغلب علي ال Race Condition الحل الأول وهو  إننا نعمل Block لأي request لحين اكتمال ال Pending Requests رغم إن الحل بسيط وسهل للوهلة الأولى إلا إنه  سئ على مستوى تجربة المستخدم لأنه لو المستخدم عنده bandwidth ضعيف و latency عالية لأي سبب كان زي مثلاً بيستخدم موبايل 3G أو في مكان النت فيه ضعيف هنا أنا بجبره في كل مرة ينتظر وقت إضافي لحين انتهاء كل Requests بيتم إرساله ودا مش أحسن حاجة على مستوى تجربة المستخدم و النقطة الثانية إنه المستخدم استقبل Response علي Request هو أصلاً مش مهتم بيه وبالتالي حملت على السيرفر Band Width زيادة أنا في غنى عنه .الحل الثاني وهو الأفضل مقارنة بالحل الأول وهو إننا نعمل block لكل ال Pending Request في كل مرة بيحصل Request جديد ونستقبل بس ال Response ل ال  Request الأخير بس وهنعمل دا عن طريق ال  AbortController : 

  1. نقوم بتهيئة ال AbortController  .
let abortController = new AbortController();
  1. نقوم بعمل Call ل ال  ()abortController.abort قبل اي Api Request .
abortController.abort();
  1. ارسال ال  AbortController.signal ك option ل ال  fetch function .
fetch(api, { signal: abortController.signal })

الكود بعد التعديل

const amountInput = document.getElementById("amount");
const fromCurrencySelect = document.getElementById("fromCurrency");
const toCurrencySelect = document.getElementById("toCurrency");
const convertButton = document.getElementById("convert");
const resultDiv = document.getElementById("result");


let tryNumber = 0;
let abortController = null;                                                    
const backendApiCall = (api, controller) => {
 return new Promise((accept, reject) => {
   setTimeout(() => {
     fetch(api, { signal: controller.signal })
       .then((response) => {
         if (!response.ok) {
           throw new Error("Network response was not ok");
         }
         return response.json();
       })
       .then((data) => {
         const amount = amountInput.value;
         const toCurrency = toCurrencySelect.value;
         const rate = data.rates[toCurrency];
         const result = amount * rate;
         accept(result);
       })
       .catch((error) => {
         if (error.name === "AbortError") {
           console.log("Request aborted");
         } else {
           reject(error);
         }
       });
   }, tryNumber === 1 ? 5000 : 0);
 });
};


function convertCurrency() {
 tryNumber++;


 // Abort any ongoing requests
 if (abortController) {  
 abortController.abort();
 }


 // Create a new AbortController
 abortController = new AbortController();


 const loader = document.getElementById("loader");
 loader.style.display = "block";
 resultDiv.style.display = "none";


 backendApiCall("/currency.json", abortController)
   .then((result) => {
     resultDiv.textContent = `  جنية مصري ${result} = ${
       amountInput.value
     } دولار`;
   })
   .catch((error) => {
     console.error("Error fetching exchange rate:", error);
   })
   .finally(() => {
     loader.style.display = "none";
     resultDiv.style.display = "block";
   });
}
convertButton.addEventListener("click", convertCurrency);

و بكده يبقي تجنبنا حدوث حالة ال Race Condition بشكل قطعي .


في الختام

في هذا المقال تناولنا ال Race Condition وحالات حدوثها وتأثيرها على تجربة المستخدم وعرفنا ايه أحسن الطرق للتغلب عليها و إلي اللقاء في مقال قادم ودمتم سالمين👋 .