IlmHamroh
JavaScript Full-stack/11-qism. Asinxron JavaScript va tarmoq15/32-dars27 daqiqa
Mundarija (35)

JavaScript asinxron naqshlar: retry, timeout, parallellik cheklovi va race condition

Qisqacha: Tarmoq ishonchsiz: so'rov sekinlashadi, server band bo'ladi, javoblar aralash tartibda keladi. Bunga beshta kundalik naqsh javob beradi. Timeout — "shuncha kutaman, keyin voz kechaman". Retry — vaqtinchalik xatoda qayta urinish, har safar ko'proq kutib (eksponensial kutish) va tasodifiy farq bilan (jitter). Parallellik cheklovi — bir vaqtda N tadan ortiq ish yo'q. Navbat — ishlar birin-ketin. "Oxirgi so'rov yutadi" — eskirgan javob yangisini bosib ketmasin.

Bu darsda

  • Promise'ga vaqt chegarasini qo'ya olasiz va Promise.race hamda AbortSignal.timeout farqini tushuntira olasiz.
  • Eksponensial kutish va jitter bilan qayta urinadigan funksiya yoza olasiz va qaysi xatoda qayta urinish mumkin emasligini bilasiz.
  • Bir vaqtdagi ishlar sonini cheklaydigan "hovuz" (pool) yasay olasiz.
  • Ishlarni navbat bilan, birin-ketin bajara olasiz.
  • Race condition'ni taniy olasiz va uni so'rov raqami yoki AbortController bilan tuzata olasiz.

Oldin bilishingiz kerak: Ketma-ket va parallel bajarish, Promise kombinatorlari, Bekor qilish: AbortController, Ushlanmagan xatolar.

1. Nega bu kerak?

Tushlik payti, soat 13:00. «Bahor» saytiga bir vaqtda yuzlab odam kiradi. Server og'irlashadi va ba'zi so'rovlarga "band" deb javob beradi. Mehmon menyuni ochmoqchi edi — ekranda xato. U sahifani yangilaydi va... menyu ochiladi. Bir soniya oldin server band edi, endi bo'sh.

Sardor kuzatib, beshta muammoni yozib oldi:

  1. Ba'zan server umuman javob bermaydi — "Yuklanmoqda..." abadiy aylanadi.
  2. Band server bir soniyadan keyin javob beradi, lekin sayt birinchi xatodayoq taslim bo'ladi.
  3. Galereyadagi 50 ta rasm bir vaqtda so'raladi va telefonda hammasi sekinlashadi.
  4. Bron formasidagi ikkita o'zgarish serverga teskari tartibda yetib boradi.
  5. Qidiruvga tez "osh" deb yozilsa, ba'zan "o" ning natijalari ko'rinib qoladi.

Har biri uchun tayyor naqsh (pattern) bor — tajribali dasturchilar har kuni ishlatadigan yechim shakli. Bugun beshtasini ham yozamiz. Serverni hozircha o'zimiz "o'ynaymiz" — fetch ni fetch asoslari darsida boshlaymiz. Lekin naqshlar fetch ga bog'liq emas: ular har qanday Promise bilan ishlaydi. Keyinroq API klient qatlami darsida ularni haqiqiy so'rovlarga ulaymiz.

2. Timeout: kutishga chegara

2.1 Promise.race bilan

Sekin server Promise'ni hech qachon bajarmasligi mumkin. await esa abadiy kutadi. Kutishga chegara qo'yishning birinchi yo'lini Promise kombinatorlari darsida yozgan edik — Promise.race bilan muddatBilan: ikki Promise "poyga"ga chiqadi — ish va taymer. Naqsh sifatida unga bitta narsa qo'shamiz — o'z xato klassi:

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

class VaqtTugadiXatosi extends Error {
  constructor(ms) {
    super(`${ms} ms ichida javob kelmadi`);
    this.name = "VaqtTugadiXatosi";
  }
}

function muddatBilan(promise, ms) {
  let taymer;
  const chegara = new Promise((_, reject) => {
    taymer = setTimeout(() => reject(new VaqtTugadiXatosi(ms)), ms);
  });
  return Promise.race([promise, chegara]).finally(() => {
    clearTimeout(taymer);
  });
}

async function sekinServer() {
  await kut(500);
  console.log("server: javob tayyor (hech kim kutmayapti)");
  return ["Osh", "Manti"];
}

async function menyuniOch() {
  try {
    const menyu = await muddatBilan(sekinServer(), 200);
    console.log(menyu);
  } catch (xato) {
    console.log(`${xato.name}: ${xato.message}`);
  }
}

menyuniOch();

Konsolda:

text
VaqtTugadiXatosi: 200 ms ichida javob kelmadi
server: javob tayyor (hech kim kutmayapti)

Kodni o'qiymiz:

  • VaqtTugadiXatosi — o'z xato klassimiz (O'z xato klasslaringiz). Chaqiruvchi uni instanceof yoki name bilan boshqa xatolardan ajrata oladi.
  • chegara — faqat rad etiladigan Promise. Birinchi parametr _ — "resolve kerak emas".
  • finally da clearTimeout — Promise kombinatorlari dagi tuzoq: ish oldin tugasa ham taymer tozalanadi.

Endi ikkinchi qatorga qarang. 200 ms da biz voz kechdik, lekin server ishlashda davom etdi va 500 ms da javob tayyorladi. Promise.race ishni to'xtatmaydi — u faqat natijani kutishni to'xtatadi. Haqiqiy tarmoqda bu so'rov ochiq qoladi, trafik va server resursini yeydi.

2.2 AbortSignal.timeout: ishni ham to'xtatish

Bekor qilish: AbortController darsidagi AbortSignal.timeout(ms) — ms dan keyin o'zi bekor bo'ladigan signal. Agar ish signalni tinglasa, vaqt tugaganda u o'zi to'xtaydi:

js
function menyuniOl({ signal }) {
  return new Promise((resolve, reject) => {
    const taymer = setTimeout(() => {
      console.log("server: javob tayyor");
      resolve(["Osh", "Manti"]);
    }, 500);
    signal.addEventListener("abort", () => {
      clearTimeout(taymer);
      reject(signal.reason);
    }, { once: true });
  });
}

async function menyuniOch() {
  try {
    const signal = AbortSignal.timeout(200);
    console.log(await menyuniOl({ signal }));
  } catch (xato) {
    console.log(`${xato.name}: ${xato.message}`);
  }
}

menyuniOch();

Konsolda:

text
TimeoutError: The operation was aborted due to timeout

"server: javob tayyor" chiqmadi — ish haqiqatan to'xtadi. Xato nomi — TimeoutError ("vaqt tugadi xatosi"), xabari: "Amal vaqt tugagani sababli to'xtatildi". Qo'lda abort() qilinganda esa nom AbortError bo'lardi. Shu farq bilan "foydalanuvchi bekor qildi" va "server sekin" holatlarini ajratasiz.

Qoida: ish signal qabul qilsa (fetch, Node'ning ko'p API'lari) — AbortSignal.timeout. Qabul qilmasa (eski kutubxona, callback API) — Promise.race o'rami. Ikkala chegara kerak bo'lsa — foydalanuvchi bekor qilishi va vaqt — ularni AbortSignal.any([boshqaruv.signal, AbortSignal.timeout(5000)]) bilan birlashtirasiz.

Tekshirib ko'ring: muddatBilan dan finally(() => clearTimeout(taymer)) ni olib tashlasak va ish 10 ms da tugasa, natija o'zgaradimi?

Javob

Natija — yo'q, ish baribir yutadi. Lekin taymer qoladi va 200 ms da chegara Promise'ini rad etadi. U poygada yutqazgan, shuning uchun xato hech qayerga chiqmaydi — Promise.race unga allaqachon "kuzatuvchi" ulagan. Muammo boshqa: Node dasturi taymer tugashini kutadi, ko'p chaqiruvda esa keraksiz taymerlar to'planadi.

3. Retry: qayta urinish

3.1 Eng oddiy shakl

Server vaqtinchalik band. Uni o'zimiz yasaymiz: birinchi xatolarSoni ta chaqiruvda xato beradi, keyin ishlaydi:

js
function serverYarat(xatolarSoni) {
  let chaqiruvlar = 0;
  return async function menyuniOl() {
    chaqiruvlar++;
    await kut(50);
    if (chaqiruvlar <= xatolarSoni) {
      throw new Error("503 — server band");
    }
    return ["Osh", "Lag'mon", "Manti"];
  };
}

503 — HTTP holat kodi: "xizmat vaqtincha mavjud emas" (holat kodlari — HTTP metodlari va status kodlari darsida). Closure chaqiruvlar ni eslab qoladi — har server o'z hisobiga ega.

Qayta urinish — sikl ichida try/catch:

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

function serverYarat(xatolarSoni) {
  let chaqiruvlar = 0;
  return async function menyuniOl() {
    chaqiruvlar++;
    await kut(50);
    if (chaqiruvlar <= xatolarSoni) {
      throw new Error("503 — server band");
    }
    return ["Osh", "Lag'mon", "Manti"];
  };
}

async function qaytaUrin(ish, urinishlar) {
  for (let urinish = 1; ; urinish++) {
    try {
      return await ish();
    } catch (xato) {
      console.log(`${urinish}-urinish: ${xato.message}`);
      if (urinish >= urinishlar) {
        throw xato;
      }
    }
  }
}

async function menyuniOch() {
  const menyuniOl = serverYarat(2);
  const menyu = await qaytaUrin(menyuniOl, 3);
  console.log("Menyu:", menyu.join(", "));
}

menyuniOch();

Konsolda:

text
1-urinish: 503 — server band
2-urinish: 503 — server band
Menyu: Osh, Lag'mon, Manti
  • for (let urinish = 1; ; urinish++) — shartsiz sikl: undan faqat return (muvaffaqiyat) yoki throw (urinishlar tugadi) bilan chiqiladi.
  • return await ish() — await shart. U bo'lmasa, rad etilish try dan qochib ketardi (Ushlanmagan xatolar darsidagi return await tuzog'i).
  • Oxirgi urinishda ham xato bo'lsa — asl xato qayta tashlanadi va chaqiruvchi uni ko'radi.

Bu kodda jiddiy kamchilik bor: urinishlar darhol ketma-ket ketadi. Server band — uni 50 ms ichida yana ikki marta bezovta qilish yordam bermaydi, aksincha, uni yanada og'irlashtiradi.

3.2 Eksponensial kutish

Har urinishdan oldin kutish kerak — va har safar ko'proq. Birinchi marta 100 ms, keyin 200, keyin 400 — har gal ikki baravar. Bunga eksponensial kutish (exponential backoff) deyiladi. Formula:

js
kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));

2 ** n — 2 ning n-darajasi (Arifmetik operatorlar). Math.min(maks, ...) — shift (cap): kechikish cheksiz o'smasin. Masalan asosiy = 100, maks = 2000 bo'lsa:

js
for (let urinish = 1; urinish <= 7; urinish++) {
  const kechikish = Math.min(2000, 100 * 2 ** (urinish - 1));
  console.log(`${urinish}-urinishdan keyin: ${kechikish} ms`);
}

Konsolda:

text
1-urinishdan keyin: 100 ms
2-urinishdan keyin: 200 ms
3-urinishdan keyin: 400 ms
4-urinishdan keyin: 800 ms
5-urinishdan keyin: 1600 ms
6-urinishdan keyin: 2000 ms
7-urinishdan keyin: 2000 ms

Grafikda doimiy va eksponensial kutishni taqqoslang:

Qayta urinishdan oldin kutish: doimiy va eksponensial
Kutish, ms
2 00010017Urinishdoimiy (100 ms): 1 → 100 msdoimiy (100 ms): 2 → 100 msdoimiy (100 ms): 3 → 100 msdoimiy (100 ms): 4 → 100 msdoimiy (100 ms): 5 → 100 msdoimiy (100 ms): 6 → 100 msdoimiy (100 ms): 7 → 100 mseksponensial (asosiy 100, maks 2000): 1 → 100 mseksponensial (asosiy 100, maks 2000): 2 → 200 mseksponensial (asosiy 100, maks 2000): 3 → 400 mseksponensial (asosiy 100, maks 2000): 4 → 800 mseksponensial (asosiy 100, maks 2000): 5 → 1 600 mseksponensial (asosiy 100, maks 2000): 6 → 2 000 mseksponensial (asosiy 100, maks 2000): 7 → 2 000 ms
  • doimiy (100 ms)
  • eksponensial (asosiy 100, maks 2000)
Qayta urinishdan oldin kutish: doimiy va eksponensial
Urinishdoimiy (100 ms)eksponensial (asosiy 100, maks 2000)
1100
2100
3100
4100
5100
6100
7100
1100
2200
3400
4800
51 600
62 000
72 000

Manba: darsdagi formula bo'yicha hisoblangan: Math.min(2000, 100 · 2^(urinish − 1))

Mantiq oddiy: server bir lahzaga band bo'lsa — tez qaytamiz. Uzoq band bo'lsa — uni tinch qo'yamiz va kamroq bezovta qilamiz. Shift esa foydalanuvchi minutlab kutib qolmasligi uchun.

3.3 Jitter: hamma bir vaqtda qaytmasin

Eksponensial kutishning yashirin muammosi bor. Server soat 13:00:00 da bir soniyaga "yiqildi". Shu paytda 1 000 ta mehmon xato oldi. Hammasining kodi bir xil: 100 ms kut, keyin qayta urin. Natijada 13:00:00.100 da serverga bir vaqtda 1 000 ta so'rov keladi — u yana yiqiladi. Keyin hammasi 200 ms kutadi va yana birga keladi. Bu hodisani "bo'kirgan poda" (thundering herd) deyishadi.

Yechim — jitter (tasodifiy tebranish): har mijoz kechikishni tasodifiy songa ko'paytiradi. Eng ko'p ishlatiladigan usul — "to'liq jitter" (full jitter): 0 dan kechikishgacha tasodifiy vaqt.

js
const kutish = Math.round(Math.random() * kechikish);

Endi 1 000 mijoz 0–100 ms oralig'iga "sochiladi" va server ularni navbat bilan qabul qiladi.

Lekin Math.random() har ishga tushirishda boshqa son beradi — darsdagi chiqishni ham, testni ham tekshirib bo'lmaydi. Shuning uchun tasodif manbasini parametr qilamiz: odatda Math.random, sinovda esa o'zimiz bergan funksiya. Bu usulni "bog'liqlikni tashqaridan berish" (dependency injection) deyishadi — test darslarida (node:test) ko'p uchraydi. Uchta mijozni o'ylab ko'ring, har birining "tasodifi" boshqacha:

js
function kutishniHisobla(urinish, { asosiy, maks, tasodif }) {
  const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
  return Math.round(tasodif() * kechikish);
}

const sozlama = { asosiy: 100, maks: 2000 };
const mijozlar = [0.9, 0.2, 0.6];

for (const son of mijozlar) {
  const tasodif = () => son;
  const jadval = [1, 2, 3].map((u) =>
    kutishniHisobla(u, { ...sozlama, tasodif }),
  );
  console.log(jadval.join(" → "));
}

Konsolda:

text
90 → 180 → 360
20 → 40 → 80
60 → 120 → 240

Jitter'siz uchalasi ham 100 → 200 → 400 bo'lardi — aynan bir xil lahzalarda. Endi ular sochilgan. Haqiqiy kodda tasodif berilmaydi va Math.random ishlaydi.

3.4 To'liq qaytaUrin

Uch g'oyani birlashtiramiz. Sozlamalar — parametrdagi destructuring va standart qiymatlar bilan:

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

function serverYarat(xatolarSoni) {
  let chaqiruvlar = 0;
  return async function menyuniOl() {
    chaqiruvlar++;
    await kut(50);
    if (chaqiruvlar <= xatolarSoni) {
      throw new Error("503 — server band");
    }
    return ["Osh", "Lag'mon", "Manti"];
  };
}

async function qaytaUrin(ish, {
  urinishlar = 3,
  asosiy = 100,
  maks = 2000,
  tasodif = Math.random,
} = {}) {
  for (let urinish = 1; ; urinish++) {
    try {
      return await ish();
    } catch (xato) {
      if (urinish >= urinishlar) {
        throw xato;
      }
      const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
      const kutish = Math.round(tasodif() * kechikish);
      const izoh = `${xato.message}, ${kutish} ms`;
      console.log(`${urinish}-urinish: ${izoh}`);
      await kut(kutish);
    }
  }
}

async function menyuniOch() {
  const menyuniOl = serverYarat(2);
  const menyu = await qaytaUrin(menyuniOl, {
    urinishlar: 4,
    tasodif: () => 0.5,
  });
  console.log("Menyu:", menyu.join(", "));
}

menyuniOch();

Konsolda:

text
1-urinish: 503 — server band, 50 ms
2-urinish: 503 — server band, 100 ms
Menyu: Osh, Lag'mon, Manti

tasodif: () => 0.5 — faqat darsdagi chiqish har safar bir xil bo'lishi uchun. Ilovangizda uni bermaysiz.

3.5 Har xatoda qayta urinish mumkin emas

Qayta urinish faqat vaqtinchalik xatolarda ma'noli:

Qayta urinish mumkin Mumkin emas
tarmoq uzildi, timeout 400 — so'rov noto'g'ri
503 — server band 401/403 — ruxsat yo'q
429 — juda ko'p so'rov 404 — topilmadi
502, 504 — oraliq server xatosi 422 — ma'lumot xato

O'ng ustundagi xatolar qayta urinishdan tuzalmaydi: "Bunday taom yo'q" (404) degan server 400 ms dan keyin ham shuni aytadi. Ularga qayta urinish — foydalanuvchini bekorga kutdirish. Shuning uchun haqiqiy qaytaUrin "qayta urinsa bo'ladimi?" degan savolni beradigan funksiyani ham oladi — buni 3-mashqda qo'shasiz.

Ikkinchi xavf — takrorlanganda ziyon qiladigan so'rovlar. "Menyuni ber" ni ikki marta so'rash zararsiz. "Bron qil" ni ikki marta yuborish esa — ikki bron. Birinchi so'rov aslida serverga yetib borgan, faqat javob yo'lda yo'qolgan bo'lishi mumkin. Bir necha marta bajarilganda ham natijasi bir xil bo'ladigan amal idempotent deyiladi. Idempotent bo'lmagan so'rovni ko'r-ko'rona qayta yubormang. Server tomonda buning uchun maxsus "takrorlanmas kalit" (idempotency key) usuli bor — backend qismida o'rganamiz.

Maslahat: 429 ("juda ko'p so'rov") javobida server ko'pincha Retry-After sarlavhasini yuboradi: "N soniyadan keyin keling". Bunday holda o'z formulangiz emas, server aytgan vaqt ustun. Javob sarlavhalarini o'qishni Request, Response, Headers darsida o'rganamiz.

Tekshirib ko'ring: asosiy = 200, maks = 1000. To'rtinchi urinishdan keyingi kechikish (jitter'siz) necha ms? Javob:

Javob

1000. Formula: 200 * 2 ** 3 = 1600, lekin Math.min(1000, 1600) — shift ishladi. Jadval: 200 → 400 → 800 → 1000.

4. Parallellik cheklovi

4.1 Hovuz (pool)

Galereyada 50 ta rasm. Promise.all(rasmlar.map(yukla)) hammasini bir vaqtda boshlaydi (Ketma-ket va parallel bajarish). Server uchun bu — 50 ta bir vaqtdagi so'rov. Sekin internetli telefonda esa 50 ta yuklash bitta tarmoqni bo'lishadi va birinchi rasm ham kech ko'rinadi.

Ketma-ket yuklash ham yomon — juda sekin. O'rtacha yo'l: bir vaqtda ko'pi bilan N ta. Buni "parallellik cheklovi" (concurrency limit) deyishadi. Usuli — hovuz (pool): N ta "ishchi" bitta umumiy navbatdan ish oladi. Ishchi o'z ishini tugatgach, navbatdagisini oladi — xuddi oshxonadagi ikkita oshpaz buyurtmalar ilgichidan navbatdagi varaqni olgandek.

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

async function cheklanganParallel(ishlar, chegara) {
  const natijalar = new Array(ishlar.length);
  let keyingi = 0;

  async function ishchi() {
    while (keyingi < ishlar.length) {
      const i = keyingi++;
      natijalar[i] = await ishlar[i]();
    }
  }

  const ishchilar = [];
  for (let n = 0; n < Math.min(chegara, ishlar.length); n++) {
    ishchilar.push(ishchi());
  }
  await Promise.all(ishchilar);
  return natijalar;
}

async function galereyaniYukla() {
  const vaqtlar = [300, 100, 150, 100];
  const ishlar = vaqtlar.map((ms, i) => async () => {
    console.log(`${i + 1}-rasm boshlandi`);
    await kut(ms);
    console.log(`${i + 1}-rasm tugadi`);
    return `rasm${i + 1}.webp`;
  });
  console.log(await cheklanganParallel(ishlar, 2));
}

galereyaniYukla();

Konsolda:

text
1-rasm boshlandi
2-rasm boshlandi
2-rasm tugadi
3-rasm boshlandi
3-rasm tugadi
4-rasm boshlandi
1-rasm tugadi
4-rasm tugadi
[ 'rasm1.webp', 'rasm2.webp', 'rasm3.webp', 'rasm4.webp' ]

Asosiy joylar:

  • ishlar — Promise'lar emas, Promise qaytaradigan funksiyalar. Bu muhim: Promise yaratilishi bilan ish boshlanadi. Funksiya esa ishni "keyinga" qoldiradi — ishchi chaqirgandagina boshlanadi.
  • const i = keyingi++ — ishchi navbatdan raqam oladi va hisoblagichni oshiradi. Bu ikki ish bitta sinxron qatorda — oraliqda boshqa ishchi aralasha olmaydi. JavaScript bir oqimli, shuning uchun bu xavfsiz (Sinxron va asinxron kod).
  • natijalar[i] — natija o'z joyiga yoziladi. Rasmlar har xil tartibda tugadi, lekin massiv asl tartibda — Promise.all dagidek.

4.2 Qadamma-qadam

Hovuzning ichki ishini soddalashtirilgan kodda kuzating. O'ngda — hozir ishlayotgan va navbatda turgan rasmlar:

E'tibor bering: bo'shagan ishchi darhol keyingisini oladi. Bu "partiyalab" (avval 1–2, keyin 3–4) bajarishdan tezroq: partiyada 2-rasm tugagach, 1-rasmni bekorga 200 ms kutish kerak bo'lardi.

Tekshirib ko'ring: cheklanganParallel(ishlar, 10) — 4 ta ish uchun. Nechta ishchi yaratiladi va natija Promise.all dan farq qiladimi?

Javob

To'rtta: Math.min(10, 4). Har ishchi bittadan ish oladi va hammasi birdan boshlanadi — xulqi Promise.all bilan bir xil. Cheklov faqat ishlar soni chegaradan ko'p bo'lganda ta'sir qiladi.

5. Navbat: birin-ketin

Dilshod aka telefonda bronini tahrirlayapti: avval odam sonini 4 ga, darhol keyin 6 ga o'zgartirdi. Ikkita "saqla" so'rovi ketdi. Birinchisi sekin, ikkinchisi tez yetib bordi — serverda 4 qoldi. Oxirgi o'zgarish yo'qoldi.

Bunday holatda ishlar navbat bilan bajarilishi kerak: keyingisi oldingisi tugagandan keyin boshlansin. Promise zanjiri aynan shunday ishlaydi — then oldingi Promise'ni kutadi:

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

function navbatYarat() {
  let oxirgi = Promise.resolve();
  return function navbatgaQoy(ish) {
    const natija = oxirgi.then(ish);
    oxirgi = natija.catch(() => {});
    return natija;
  };
}

let serverda = null;
async function odamSoniniSaqla(soni, ms) {
  await kut(ms);
  serverda = soni;
  console.log(`saqlandi: ${soni}`);
}

const navbatgaQoy = navbatYarat();
navbatgaQoy(() => odamSoniniSaqla(4, 300));
navbatgaQoy(() => odamSoniniSaqla(6, 100));
setTimeout(() => console.log("Serverda:", serverda), 600);

Konsolda:

text
saqlandi: 4
saqlandi: 6
Serverda: 6
  • oxirgi — navbatdagi oxirgi ishning Promise'i. Har yangi ish unga then bilan "ulanadi".
  • oxirgi = natija.catch(() => {}) — bitta ish xato bersa, navbat to'xtab qolmasin. Xatoni esa chaqiruvchi natija orqali o'zi oladi.
  • Navbatsiz (ikkalasini to'g'ridan-to'g'ri chaqirsak) konsolda saqlandi: 6, keyin saqlandi: 4 va Serverda: 4 bo'lardi.

Navbat — chegarasi 1 bo'lgan hovuzning o'zi. Farqi: hovuzga ishlar ro'yxati bir marta beriladi, navbatga esa vaqt o'tib, birma-bir qo'shiladi.

6. Race condition: "oxirgi so'rov yutadi"

6.1 Muammo

Qidiruv maydoniga "osh" deb yozilyapti. Har harfda so'rov ketadi: "o", "os", "osh". Qisqa so'z ko'proq natija beradi va server uni sekinroq qaytaradi. Javoblar teskari tartibda keladi:

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

const javobVaqti = { o: 300, os: 200, osh: 100 };
function qidir(soz) {
  return kut(javobVaqti[soz], `«${soz}» natijalari`);
}

let ekranda = "";
async function qidiruvniYangila(soz) {
  const natija = await qidir(soz);
  console.log("keldi:", natija);
  ekranda = natija;
}

qidiruvniYangila("o");
setTimeout(() => qidiruvniYangila("os"), 50);
setTimeout(() => qidiruvniYangila("osh"), 100);
setTimeout(() => console.log("Ekranda:", ekranda), 500);

Konsolda:

text
keldi: «osh» natijalari
keldi: «os» natijalari
keldi: «o» natijalari
Ekranda: «o» natijalari

Foydalanuvchi "osh" deb yozdi, ekranda esa "o" ning natijalari. Eng eski javob eng oxirida kelib, to'g'ri natijani bosib ketdi.

Natija ishlar qaysi tartibda tugashiga bog'liq bo'lgan holat race condition (poyga holati) deyiladi. Uni topish qiyin: kompyuteringizda internet tez — hammasi to'g'ri; mehmonning telefonida — xato. Bir oqimli JavaScript'da ham race condition bo'ladi: kod parallel ishlamaydi, lekin asinxron ishlar har xil tartibda tugaydi.

6.2 1-yechim: so'rov raqami

Har so'rovga raqam beramiz va javob kelganda tekshiramiz: "men hali ham oxirgisimanmi?"

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

const javobVaqti = { o: 300, os: 200, osh: 100 };
function qidir(soz) {
  return kut(javobVaqti[soz], `«${soz}» natijalari`);
}

let ekranda = "";
let oxirgiRaqam = 0;
async function qidiruvniYangila(soz) {
  const raqam = ++oxirgiRaqam;
  const natija = await qidir(soz);
  if (raqam !== oxirgiRaqam) {
    console.log("eskirgan, tashlandi:", natija);
    return;
  }
  ekranda = natija;
}

qidiruvniYangila("o");
setTimeout(() => qidiruvniYangila("os"), 50);
setTimeout(() => qidiruvniYangila("osh"), 100);
setTimeout(() => console.log("Ekranda:", ekranda), 500);

Konsolda:

text
eskirgan, tashlandi: «os» natijalari
eskirgan, tashlandi: «o» natijalari
Ekranda: «osh» natijalari

oxirgiRaqam — closure'dagi umumiy hisoblagich, raqam — har chaqiruvning o'z nusxasi. Javob kelganda ular teng bo'lmasa, demak orada yangi so'rov ketgan — bu javob eskirgan.

6.3 2-yechim: oldingi so'rovni bekor qilish

1-yechimda eski so'rovlar baribir oxirigacha ishladi — biz faqat natijasini tashladik. Yaxshiroq: yangi so'rov ketishi bilan eskisini bekor qilish. Brauzerda sinang: maydonga o'zingiz ham yozib ko'ring. Sahifa ochilganda skript "tez yozish"ni o'zi taqlid qiladi — o, os, osh:

html
<style>
  body { font: 1rem/1.5 system-ui, sans-serif; margin: 1rem; }
  input { font: inherit; padding: 0.3rem 0.5rem; }
</style>
<label for="qidiruv">Menyudan qidirish</label>
<input id="qidiruv" autocomplete="off">
<p id="natija" aria-live="polite">—</p>
<script>
  const MENYU = ["Osh", "Lag'mon", "Manti", "Ko'k choy"];
  const qidiruv = document.getElementById("qidiruv");
  const natijaEl = document.getElementById("natija");
  let joriy = null;

  // Server o'rinbosari: qisqa so'z — sekinroq javob
  function qidir(soz, signal) {
    return new Promise((resolve, reject) => {
      const taymer = setTimeout(() => {
        resolve(MENYU.filter((t) => t.toLowerCase().includes(soz)));
      }, Math.max(100, 400 - soz.length * 100));
      signal.addEventListener("abort", () => {
        clearTimeout(taymer);
        reject(signal.reason);
      }, { once: true });
    });
  }

  qidiruv.addEventListener("input", async () => {
    const soz = qidiruv.value.trim().toLowerCase();
    joriy?.abort();
    const boshqaruv = new AbortController();
    joriy = boshqaruv;
    try {
      const topildi = await qidir(soz, boshqaruv.signal);
      natijaEl.textContent = topildi.join(", ") || "Topilmadi";
      console.log(`«${soz}»: ${natijaEl.textContent}`);
    } catch (xato) {
      if (xato.name !== "AbortError") {
        throw xato;
      }
      console.log(`«${soz}» bekor qilindi`);
    }
  });

  // Tez yozishni taqlid qilamiz: o → os → osh
  [[0, "o"], [50, "os"], [100, "osh"]].forEach(([ms, matn]) => {
    setTimeout(() => {
      qidiruv.value = matn;
      qidiruv.dispatchEvent(new Event("input"));
    }, ms);
  });
</script>

Konsolda:

text
«o» bekor qilindi
«os» bekor qilindi
«osh»: Osh

Har input hodisasida: avvalgi so'rovni abort() qilamiz (?. — birinchi marta joriy hali null), yangisi uchun yangi AbortController yaratamiz. Bekor qilingan so'rovning await i AbortError bilan rad etiladi — biz uni "kutilgan" deb jim o'tkazamiz. Boshqa har qanday xatoni esa qayta tashlaymiz: u global tinglovchiga borsin (Ushlanmagan xatolar).

Haqiqiy fetch da bekor qilish so'rovni tarmoq darajasida uzadi — trafik ham, server ishi ham tejaladi. Qidiruvda odatda yana bir naqsh qo'shiladi — har harfda emas, yozish to'xtagach so'rash (Debounce va throttle).

6.4 Ikki marta bosish: bitta jarayon

Race condition'ning teskari ko'rinishi: Otabek kassadagi «Bron qilish» tugmasini shoshib ikki marta bosdi. Ikki so'rov — ikki bron. Bu yerda "oxirgisi" emas, birinchisi yutishi kerak: jarayon ketayotgan bo'lsa, yangisini boshlamaslik.

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

function birMartalik(ish) {
  let jarayon = null;
  return function () {
    if (jarayon === null) {
      jarayon = ish().finally(() => {
        jarayon = null;
      });
    }
    return jarayon;
  };
}

let bronlarSoni = 0;
async function bronYubor() {
  await kut(200);
  bronlarSoni++;
  return `Bron №${bronlarSoni}`;
}

const bronQil = birMartalik(bronYubor);
const birinchi = bronQil();
const ikkinchi = bronQil();
console.log(birinchi === ikkinchi);
birinchi.then((javob) => console.log(javob, "| jami:", bronlarSoni));

Konsolda:

text
true
Bron №1 | jami: 1

Ikkinchi bosish yangi so'rov yubormadi — u o'sha Promise'ni oldi. Jarayon tugagach (finally) jarayon yana null bo'ladi va keyingi bosish yangi bronni boshlaydi. Interfeysda bunga qo'shimcha ravishda tugma ham disabled qilinadi — lekin kod darajasidagi himoya ishonchliroq: tugmadan boshqa yo'llar (Enter tugmasi, ikkinchi tab) ham bor.

6.5 Sardorning beshta muammosi — beshta javob

«Nega bu kerak?» dagi ro'yxatga qaytamiz. Endi har muammoning o'z naqshi bor.

Muammo Naqsh
"Yuklanmoqda..." abadiy aylanadi timeout (AbortSignal.timeout)
band server — birinchi xatoda taslim retry + eksponensial kutish + jitter
50 ta rasm bir vaqtda hovuz (parallellik cheklovi)
ikki o'zgarish teskari tartibda navbat
"o" natijasi "osh" ni bosib ketadi so'rov raqami yoki abort()

Ikki marta bosish — race condition'ning oltinchi, "birinchisi yutadi" ko'rinishi.

7. Og'ir ishni bo'laklash

Sinxron va asinxron kod darsida va'da bergan edik: og'ir ishni bo'laklash usullarini ko'ramiz. Avval Event loop darsining 3-mashqida bo'laklarni callback va setTimeout bilan yozgan edingiz. Keyin Microtask va macrotask chuqur darsida await kut(0) nega davomni task qilishini ko'rdik. Endi ikkalasini naqsh qilib birlashtiramiz: har bo'lakdan keyin await kut(0) — "event loop'ga nafas ber":

js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

async function bolaklabHisobla(jami, bolak) {
  let summa = 0;
  for (let i = 0; i < jami; i++) {
    summa += i % 100;
    if ((i + 1) % bolak === 0) {
      console.log(`${(i + 1) / bolak}-bo'lak tugadi`);
      await kut(0);
    }
  }
  return summa;
}

setTimeout(() => console.log("Yangi buyurtma qabul qilindi"), 0);
bolaklabHisobla(3_000_000, 1_000_000).then((summa) => {
  console.log("Jami summa:", summa);
});

Konsolda:

text
1-bo'lak tugadi
Yangi buyurtma qabul qilindi
2-bo'lak tugadi
3-bo'lak tugadi
Jami summa: 148500000

Natija Event loop darsidagi mashq bilan bir xil, lekin kod oddiy for sikliga o'xshaydi: i va summa closure orqali emas, oddiy o'zgaruvchi sifatida yashaydi. Har bo'lakdan keyin await kut(0) siklni to'xtatadi, task navbatidagi "Yangi buyurtma" bajariladi, keyin sikl davom etadi.

Diqqat: await null yoki await Promise.resolve() bilan bo'laklash ishlamaydi — ular faqat microtask beradi (Microtask va macrotask chuqur dagi microtask ochligi). Event loop'ga nafas berish uchun — task kerak. Brauzerlarda buning uchun maxsus scheduler.yield() ham bor — Asosiy oqimni bo'shatish darsida.

8. Ko'p uchraydigan xatolar

8.1 Promise'lar massivini hovuzga berish

cheklanganParallel(rasmlar.map((r) => yukla(r)), 2) — map hammasini darhol chaqirdi, 50 ta yuklash allaqachon boshlangan. Hovuz endi hech narsani cheklamaydi. Tuzatish: funksiyalar bering: rasmlar.map((r) => () => yukla(r)).

8.2 Kutishsiz qayta urinish

for siklida darhol qayta urinish — band serverni yanada bosadi. Tuzatish: eksponensial kutish + jitter + shift.

8.3 Hamma xatoda qayta urinish

404 ga uch marta qayta urinish — foydalanuvchini bekorga uch marta kutdirish. "Bron qil" ni qayta yuborish — ikki bron. Tuzatish: faqat vaqtinchalik xatolar va idempotent amallar.

8.4 Promise.race timeout'i ishni to'xtatadi deb o'ylash

U faqat kutishni to'xtatadi — ish davom etadi. Tuzatish: imkon bo'lsa AbortSignal.timeout va signal qabul qiladigan API.

8.5 Javob tartibiga ishonish

"Men avval «o» ni, keyin «osh» ni so'radim — demak javoblar ham shu tartibda keladi". Yo'q: tarmoqda tartib kafolatlanmaydi. Tuzatish: so'rov raqami yoki oldingisini bekor qilish; ketma-ket saqlash uchun — navbat.

9. Mashqlar

1-mashq (oson): Jadvalni to'ldiring

asosiy = 50, maks = 300, jitter yo'q. Har urinishdan keyingi kechikishni yozing:

  • 1-urinishdan keyin: ms
  • 2-urinishdan keyin: ms
  • 3-urinishdan keyin: ms
  • 4-urinishdan keyin: ms
Yechim

50 → 100 → 200 → 300. To'rtinchida formula 50 * 2 ** 3 = 400 beradi, lekin Math.min(300, 400) — shift. Bundan keyingi har urinish ham 300.

2-mashq (o'rta): Zaxira bilan kutish

Menyu 300 ms ichida kelmasa, ekranda bo'sh joy emas, kechagi saqlangan menyu ko'rinsin. zaxiraBilan(promise, ms, zaxira) funksiyasini yozing: promise ms ichida bajarilsa — uning qiymatini, aks holda zaxira ni qaytarsin. Taymerni tozalashni unutmang. Ishora: muddatBilan dagi Promise.race ni oling, faqat chegara Promise'i rad etilmasin, zaxira bilan bajarilsin.

Sinang: tez server (100 ms) va sekin server (500 ms), zaxira — ["Osh (kechagi menyu)"].

Yechim
js
function kut(ms, qiymat) {
  return new Promise((resolve) => setTimeout(resolve, ms, qiymat));
}

function zaxiraBilan(promise, ms, zaxira) {
  let taymer;
  const chegara = new Promise((resolve) => {
    taymer = setTimeout(resolve, ms, zaxira);
  });
  return Promise.race([promise, chegara]).finally(() => {
    clearTimeout(taymer);
  });
}

async function menyularniOch() {
  const kechagi = ["Osh (kechagi menyu)"];
  const tez = kut(100, ["Osh", "Manti"]);
  const sekin = kut(500, ["Osh", "Manti"]);
  console.log(await zaxiraBilan(tez, 300, kechagi));
  console.log(await zaxiraBilan(sekin, 300, kechagi));
}

menyularniOch();

Konsolda:

text
[ 'Osh', 'Manti' ]
[ 'Osh (kechagi menyu)' ]

setTimeout(resolve, ms, zaxira) — Taymerlar darsidagi uchinchi argument: resolve(zaxira) ni ms dan keyin chaqiradi. Bu naqsh "eskirgan bo'lsa ham ko'rsat" deb ataladi — ilovalarda keshdagi ma'lumot shunday ko'rsatiladi, yangisi kelgach almashtiriladi. Bunday keshni yakuniy loyihada o'zingiz yasaysiz.

3-mashq (qiyin): Aqlli qaytaUrin

«To'liq qaytaUrin» bo'limidagi funksiyaga ikki imkoniyat qo'shing:

  1. shart(xato) sozlamasi — true qaytarsa qayta urinadi, false bo'lsa xatoni darhol tashlaydi. Standart qiymati — har doim true.
  2. signal sozlamasi — bekor qilinsa, kutish paytida ham darhol to'xtasin.

Sinov uchun server xatoga holat xususiyatini qo'shsin: Object.assign(new Error("..."), { holat: 503 }). shart — (xato) => [429, 502, 503, 504].includes(xato.holat). Ikki holatni sinang: 503 ni ikki marta beradigan server (muvaffaqiyat) va 404 beradigan server (bitta urinish, xato). Ishora: signal bilan kutish uchun Bekor qilish darsidagi signal tinglaydigan kutish funksiyasini yozing. Faylni kurs/mashqlar/11/15-asinxron-naqshlar/qayta-urin.mjs ga saqlang.

Yechim
js
function signalliKut(ms, signal) {
  return new Promise((resolve, reject) => {
    signal?.throwIfAborted();
    const taymer = setTimeout(resolve, ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(taymer);
      reject(signal.reason);
    }, { once: true });
  });
}

async function qaytaUrin(ish, {
  urinishlar = 3,
  asosiy = 100,
  maks = 2000,
  tasodif = Math.random,
  shart = () => true,
  signal,
} = {}) {
  for (let urinish = 1; ; urinish++) {
    signal?.throwIfAborted();
    try {
      return await ish();
    } catch (xato) {
      if (urinish >= urinishlar || !shart(xato)) {
        throw xato;
      }
      const kechikish = Math.min(maks, asosiy * 2 ** (urinish - 1));
      const kutish = Math.round(tasodif() * kechikish);
      const izoh = `${xato.message}, ${kutish} ms`;
      console.log(`${urinish}-urinish: ${izoh}`);
      await signalliKut(kutish, signal);
    }
  }
}

function serverYarat(holatlar) {
  let chaqiruv = 0;
  return async function menyuniOl() {
    const holat = holatlar[chaqiruv++] ?? 200;
    if (holat !== 200) {
      throw Object.assign(new Error(`${holat} xato`), { holat });
    }
    return ["Osh", "Manti"];
  };
}

const VAQTINCHALIK = [429, 502, 503, 504];
const vaqtinchalikmi = (xato) => VAQTINCHALIK.includes(xato.holat);
const sozlama = {
  urinishlar: 4,
  tasodif: () => 0.5,
  shart: vaqtinchalikmi,
};

async function sina() {
  const band = serverYarat([503, 503]);
  console.log("Menyu:", await qaytaUrin(band, sozlama));
  try {
    await qaytaUrin(serverYarat([404]), sozlama);
  } catch (xato) {
    console.log("Taslim bo'ldik:", xato.message);
  }
}

sina();

Konsolda:

text
1-urinish: 503 xato, 50 ms
2-urinish: 503 xato, 100 ms
Menyu: [ 'Osh', 'Manti' ]
Taslim bo'ldik: 404 xato

404 da birorta "urinish" qatori yo'q: shart false qaytardi va xato darhol tashlandi. signal?. — signal berilmagan bo'lsa (undefined), chaqiruv o'tkazib yuboriladi (?? va ?.). holatlar[chaqiruv++] ?? 200 — ro'yxat tugagach server "200 — hammasi yaxshi" deb javob beradi.

bash
git add 11/15-asinxron-naqshlar/qayta-urin.mjs
git commit -m "11/15: shart va signal bilan qaytaUrin"

10. Real ishda

  • TanStack Query (React va Vue'dagi eng mashhur so'rov kutubxonasi) — standart sozlamasi bilan brauzerda xato bergan so'rovni 3 marta qayta uradi, kutish: Math.min(1000 * 2 ** urinish, 30000) ms (5.104 versiyasi manba kodidan). Bugungi formulaning o'zi.
  • npm paketlari. p-limit (7.3 versiyasi) — hovuz, p-retry 8.0-bob — qayta urinish. Ichida — bugungi kod. Paketlarni npm bilan o'rnatish 16-qismda chuqurlashadi.
  • To'lov va bulut API'lari. Katta xizmatlarning rasmiy kutubxonalari xatoda jitter bilan qayta urinadi va pul o'tkazmalari uchun idempotency key talab qiladi — ikki marta yechib olmaslik uchun.
  • Telegram botlar. Telegram Bot API juda ko'p so'rovga 429 va "N soniya kut" javobini beradi — bot yozganda shu raqamni hurmat qiladigan qayta urinish kerak.
  • Intervyu. "Exponential backoff nima va nega jitter kerak?", "N ta so'rovni bir vaqtda ko'pi bilan 3 tadan qanday bajarasiz?", "Qidiruvdagi race condition'ni qanday tuzatasiz?" — o'rta va yuqori darajadagi intervyularda tez-tez beriladi.

Xulosa

  • Timeout: Promise.race faqat kutishni to'xtatadi; AbortSignal.timeout ishni ham to'xtatadi (TimeoutError).
  • Retry: faqat vaqtinchalik xatolarda va idempotent amallarda; kechikish Math.min(maks, asosiy * 2 ** (n - 1)), ustiga jitter.
  • Tasodif — parametr: sinovda aniq funksiya, ilovada Math.random.
  • Hovuz: N ta ishchi bitta navbatdan funksiyalarni oladi; natijalar asl tartibda.
  • Navbat: oxirgi = oxirgi.then(ish) — ishlar birin-ketin, xato navbatni to'xtatmaydi.
  • Race condition: javoblar tartibiga ishonmang — so'rov raqami yoki oldingisini abort(); ikki marta bosishga — bitta umumiy jarayon.

Keyingi dars: Dinamik import() va top-level await — modulni kerak bo'lgandagina yuklash, await ni modulning eng yuqori qatorida yozish va vazifalar ilovasida eksport modulini bosilganda yuklash.

Manbalar

  • Marc Brooker, "Exponential Backoff And Jitter" (AWS Architecture Blog, 2015) — aws.amazon.com/blogs/architecture
  • MDN: "AbortSignal.timeout()", "AbortSignal.any()", "Promise.race()" — developer.mozilla.org
  • TanStack Query 5.104.1 manba kodi: packages/query-core/src/retryer.ts (defaultRetryDelay, retry) — github.com/TanStack/query
  • RFC 9110 "HTTP Semantics": 429, 503, Retry-After, idempotent metodlar — rfc-editor.org
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
JavaScript asinxron naqshlar: retry, timeout, parallellik cheklovi va race condition — IlmHamroh