IlmHamroh
JavaScript Full-stack/31-qism. Prodakshn backend va AI integratsiya29/34-dars30 daqiqa
Mundarija (33)

Resilience naqshlari: timeout, retry va jitter, circuit breaker, bulkhead, fallback

Qisqacha: Resilience (chidamlilik) — tashqi xizmat sekinlashsa yoki yiqilsa, sizning ilovangiz yiqilmasligi. To'rt asosiy vosita: timeout — cheksiz kutmaslik; retry — faqat vaqtinchalik xatoda, jitter bilan qayta urinish; circuit breaker — xizmat o'lganini sezib, uni bir muddat umuman chaqirmaslik; bulkhead — bir vaqtdagi chaqiruvlarga chegara. Kanon o'lchovida yiqilgan AI provayderi bilan "faqat retry" vaziyatni yomonlashtirdi: 100 so'rovga 300 chaqiruv, 32 soniya. Breaker bilan — 14 chaqiruv, 2 soniya.

Bu darsda

  • Timeout byudjetini hisoblaysiz: bitta so'rov eng yomon holatda qancha kutadi.
  • Retry'ni qachon qilish mumkin, qachon mumkin emasligini va jitter nega kerakligini tushuntirasiz.
  • Circuit breaker holatlarini model va kanon kodida kuzatasiz.
  • Bulkhead, fallback va graceful degradation'ni vazifalar-api misolida ajratasiz.
  • Kanondagi zaxira model va breaker birikmasida biz topgan tuzoqni ko'rasiz.
  • vazifalar-api ga ChidamliProvayder dekoratorini qo'shasiz.

Oldin bilishingiz kerak: Asinxron naqshlar, timers va timers/promises chuqur, DoS va resurs tugashidan himoya, Vercel AI SDK va provayderlarni almashtirish, AI xususiyatlarini prodakshnda ishlatish.

O'tgan darsda ilovaning ichini ko'rishni o'rgandik. Endi ko'rganimizga javob beramiz. Panelda "AI so'rovlari 1 soniyada timeout" ko'rindi — ilova bunga qanday munosabat bildiradi? Bu darsdagi barcha naqshlar 11-qismdagi Asinxron naqshlar darsining davomi: u yerda retry va timeout funksiyalarini yozgan edik, endi ularni prodakshn tizimiga qo'yamiz.

1. Nega bu kerak?

vazifalar-api da AI xususiyati bor: vazifani qadamlarga bo'lish (POST /v1/vazifalar/:id/qadamlar). U Gemini'ga so'rov yuboradi. Bir kuni Google tomonda muammo: har so'rov 1 soniya osilib, keyin xato beradi.

Himoyasiz ilovada nima bo'ladi? Har foydalanuvchi 1 soniya kutib, xato oladi. Yomon, lekin chidasa bo'ladi. Endi "aqlli" dasturchi retry qo'shgan bo'lsin: har xatoda ikki marta qayta urinish. Endi har foydalanuvchi 3 soniya kutadi, provayderga esa uch baravar ko'p so'rov ketadi. Provayder tiklanmoqchi bo'lsa ham, biz uni so'rovlar bilan bosib turibmiz.

Kanon buni o'lchadi. Provayder "yiqilgan": har chaqiruv 1 soniyadan keyin timeout. 100 ta foydalanuvchi so'rovi, bir vaqtda 10 tadan (soxta provayder, tarmoqsiz). Kanon chiqishi:

text
=== provayder yiqilgan (har chaqiruv 1 s, keyin timeout), 100 so'rov, 10 parallel
(a) himoyasiz                                jami  10066 ms | foydalanuvchi kutishi p50  1007 ms, p95  1011 ms | provayderga chaqiruv 100 | {"timeout":100}
(b) faqat retry 2 (jitter)                   jami  32026 ms | foydalanuvchi kutishi p50  3178 ms, p95  3305 ms | provayderga chaqiruv 300 | {"timeout":100}
(c) retry 2 + breaker 5/2 s                  jami   2063 ms | foydalanuvchi kutishi p50     0 ms, p95  1017 ms | provayderga chaqiruv  14 | {"timeout":10,"ochiq":90}
   breaker: ochiq, hisob {"chaqiruv":14,"qaytaUrinish":4,"breakerRad":90,"bulkheadRad":0}
=== provayder tiklandi, 2.1 s o'tdi — breaker: yarim
(c) tiklangandan keyin                       jami    278 ms | foydalanuvchi kutishi p50    28 ms, p95    31 ms | provayderga chaqiruv 100 | {"ok":100}
   breaker: yopiq
Yiqilgan provayderga 100 so'rovda nechta chaqiruv ketdi
  • himoyasiz100 chaqiruv
  • faqat retry 2300 chaqiruv
  • retry 2 + breaker14 chaqiruv

Manba: IlmHamroh kanon o'lchovi, 2026-11-03: kanon ChidamliProvayder kodi, soxta provayder (har chaqiruv 1 s, keyin timeout), 100 so'rov, 10 parallel, Node 24.21.0; chaqiruvlar soni — vaqtga bog'liq emas

Uchta xulosa:

  • Faqat retry — eng yomoni: jami vaqt 3.2 baravar, provayderga yuk 3 baravar. Bu — retry bo'roni (retry storm).
  • Breaker bilan — 14 chaqiruvdan keyin breaker "provayder o'lik" deb qaror qildi, qolgan 90 so'rov darhol rad etildi. Provayder dam oldi.
  • Provayder tiklangach, breaker o'zi sinab ko'rdi va yopildi — 100 so'rovning hammasi muvaffaqiyatli.

O'xshatish: do'stingizga qo'ng'iroq qilyapsiz, band. Ketma-ket 20 marta terish (retry bo'roni) yordam bermaydi. Aqllisi — 3 marta urinib, javob bo'lmasa, yarim soat qo'ng'iroq qilmaslik (breaker), keyin bir marta sinab ko'rish (yarim-ochiq).

2. Timeout har joyda

2.1 Kanondagi timeout'lar

Birinchi qoida: har tashqi chaqiruvning chegarasi bo'lsin. Timeout'siz fetch cheksiz kutishi mumkin — kanon 31-qism boshida buni o'z ko'zi bilan ko'rdi: Gemini'ga bitta so'rov javobsiz osilib qoldi va jarayonni 200 soniyada qo'lda o'ldirishga to'g'ri keldi. vazifalar-api dagi chegaralar:

Joy Timeout Qayerda
AI provayderi (har urinish) 20 s (AI_TIMEOUT_MS) chegaraliSignal — AbortSignal.any
SQL so'rovi 5 s (PG_STATEMENT_TIMEOUT) pool statement_timeout
Pool'dan ulanish olish 5 s connectionTimeoutMillis
Health: baza tekshiruvi 2 s Promise.race
Graceful shutdown 10 s yop(server, kutishMs)

chegaraliSignal ikki signalni birlashtiradi: o'z timeout'imiz (AbortSignal.timeout) va mijozning uzilishi. Foydalanuvchi sahifani yopsa, LLM so'rovi ham bekor bo'ladi — pul va kvota tejaladi (AbortController).

2.2 Timeout byudjeti

Timeout'lar ko'payadi. AI so'rovi: 20 soniyalik urinish × 3 (1 + 2 qayta) + kutishlar. Ustiga zaxira model ham shuncha. Eng yomon holat — 3-mashqda hisoblaymiz va natija sizni hayratga soladi. Qoida: ichki timeout'lar yig'indisi mijoz kutadigan vaqtdan kichik bo'lsin. Aks holda mijoz allaqachon ketgan, server esa hali ham urinyapti — behuda ish. Taqsimlangan tizimlarda buni deadline (oxirgi muddat) bilan hal qilishadi: so'rov bilan "shu vaqtgacha javob kerak" degan belgi yuboriladi va har qatlam qolgan vaqtga qarab ishlaydi.

3. Retry: qachon va qanday

3.1 Faqat vaqtinchalik xato

Retry yordam beradigan xatolar — vaqtinchalik: provayder band (503), limit (429), timeout, ichki xato (5xx). Yordam bermaydiganlari — doimiy: noto'g'ri so'rov (400), sxemaga mos kelmagan javob, kontent rad etildi. Ularni qayta yuborish natijani o'zgartirmaydi, faqat pul va vaqtni yeydi.

Kanonda bu farq xato klassining o'zida (src/ai/llm.ts, 31-qism AI darslaridan):

ts
export class LlmXatosi extends Error {
  // …
  /** qayta urinsa bo'ladimi (#29): faqat vaqtinchalik xatolar */
  get vaqtinchalik(): boolean {
    return this.tur === "limit" || this.tur === "band"
      || this.tur === "timeout"
      || (this.tur === "provayder" && (this.status ?? 0) >= 500);
  }
}

Ikkinchi shart — idempotentlik. LLM'dan qadamlar so'rash o'qishga o'xshaydi: ikki marta so'rasangiz, hech narsa ikki marta saqlanmaydi. POST /v1/vazifalar ni esa ko'r-ko'rona qayta yuborsangiz — ikki vazifa paydo bo'lishi mumkin. Bunday so'rovlarni faqat Idempotency-Key bilan qayta yuborish mumkin.

Uchinchi istisno — oqim (SSE). Javobning yarmi mijozga ketgan bo'lsa, qayta urinish uni ikki marta ko'rsatadi. Kanonda oqim uchun retry yo'q.

3.2 Eksponensial kutish va jitter

Retry orasida kutish ikki baravardan o'sadi (300, 600, 1200 ms …) — eksponensial kutish (exponential backoff). Lekin hamma mijoz bir xil formula bilan kutsa, ular bir vaqtda qaytadi va provayderni yana birdaniga bosadi. Yechim — jitter (tasodifiy siljish). Kanon "to'liq jitter"ni ishlatadi: kutish = tasodifiy(0, min(8 s, 300 · 2ⁿ)).

Brauzerda to'rt mijozni ko'ramiz. Math.random har safar boshqa natija berardi, shuning uchun takrorlanadigan oddiy "tasodif" generatori (Park–Miller) olamiz:

js
// Takrorlanadigan "tasodif" (Park–Miller) — har safar bir xil natija
function tasodif(urug) {
  let x = urug;
  return () => {
    x = (x * 48271) % 2147483647;
    return x / 2147483647;
  };
}

const ASOS = 300;
const MAKS = 8000;
// to'liq jitter: kutish = random(0, min(MAKS, ASOS · 2^n))
const kutish = (n, r) =>
  Math.round(r() * Math.min(MAKS, ASOS * 2 ** n));
const jittersiz = [0, 1, 2].map((n) => ASOS * 2 ** n);

for (let mijoz = 1; mijoz <= 4; mijoz++) {
  const r = tasodif(mijoz * 7919);
  const jadval = [0, 1, 2].map((n) => kutish(n, r));
  console.log(`mijoz ${mijoz}: ${jadval.join(", ")} ms`);
}
console.log(`jittersiz (hammasi): ${jittersiz.join(", ")} ms`);

Konsolda:

text
mijoz 1: 53, 223, 134 ms
mijoz 2: 107, 446, 267 ms
mijoz 3: 160, 70, 401 ms
mijoz 4: 214, 293, 534 ms
jittersiz (hammasi): 300, 600, 1200 ms

Jittersiz hamma mijoz aynan 300, 600, 1200 ms da qaytadi — uchta to'lqin. Jitter bilan qaytishlar vaqt bo'ylab sochiladi. Bizning generatorimiz juda oddiy (birinchi ustun teng qadam bilan o'sdi — ketma-ket urug'lar o'xshash son beradi), haqiqiy kodda Math.random() yetarli — kanondagi kabi.

3.3 Retry-After

Provayder "7 soniyadan keyin keling" desa (Retry-After sarlavhasi, 429/503 da), uni hurmat qilish kerak. Kanonda: Math.max(jitter, x.qaytaUrinishMs ?? 0) — jitter kichik chiqsa ham, provayder aytgan vaqtdan oldin qaytmaymiz. Kanon testida aynan shu tekshiriladi: ikkinchi kutish ≥ 2000 ms.

Tekshirib ko'ring: AI javobi Zod sxemasiga mos kelmadi (notogri turi). Kanon bu xatoda qayta urinadimi?

Javob

ChidamliProvayder — yo'q: notogri vaqtinchalik emas, breaker hisobiga ham qo'shilmaydi (provayder sog', javob yomon). Lekin bu xatoni bir qatlam yuqorida qadamlar.ts boshqacha hal qiladi: bitta tuzatish so'rovi yuboradi — xatolar ro'yxati bilan, "shu xatolarni tuzat" deb. Bu ko'r-ko'rona retry emas, yangi ma'lumotli boshqa so'rov. Ikkinchisi ham yomon bo'lsa — 502.

4. Circuit breaker

4.1 Uch holat

Circuit breaker (avtomatik o'chirgich) — uydagi elektr avtomati g'oyasi: qisqa tutashuvda tokni uzadi, uyni yong'indan saqlaydi. Dasturda: tashqi xizmat ketma-ket xato bersa, uni bir muddat umuman chaqirmaydi, darhol xato qaytaradi.

stateDiagram-v2
  [*] --> yopiq
  yopiq --> ochiq: N ketma-ket xato
  ochiq --> yarim: T vaqt o'tdi
  yarim --> yopiq: sinov muvaffaqiyatli
  yarim --> ochiq: sinov xato
  • Yopiq (closed) — oddiy ish, chaqiruvlar o'tadi, xatolar sanaladi.
  • Ochiq (open) — chaqiruv provayderga bormaydi, darhol rad. Foydalanuvchi 1 soniya kutish o'rniga darhol javob oladi (503).
  • Yarim-ochiq (half-open) — vaqt o'tgach bitta sinov so'roviga ruxsat. O'tsa — yopiq, yiqilsa — yana ochiq.

Nomlar teskari tuyuladi: "ochiq" — yomon holat. Elektr zanjirida shunday: zanjir ochiq bo'lsa, tok o'tmaydi.

4.2 Model

Soxta soat bilan breaker. Provayder avval o'lik, 60 soniyadan keyin tiklanadi:

js
function breakerYarat({ chegara, ochiqMs, soat }) {
  let holat = "yopiq";
  let xatolar = 0;
  let ochilgan = 0;
  const joriy = () => {
    if (holat === "ochiq" && soat() - ochilgan >= ochiqMs) {
      holat = "yarim"; // vaqt o'tdi — bitta sinovga ruxsat
    }
    return holat;
  };
  async function chaqir(fn) {
    if (joriy() === "ochiq") throw new Error("ochiq: darhol rad");
    try {
      const javob = await fn();
      xatolar = 0;
      holat = "yopiq";
      return javob;
    } catch (x) {
      xatolar++;
      if (holat === "yarim" || xatolar >= chegara) {
        holat = "ochiq";
        ochilgan = soat();
      }
      throw x;
    }
  }
  return { chaqir, joriy };
}

let t = 0;
let sog = false;
let chaqiruv = 0;
const provayder = async () => {
  chaqiruv++;
  if (!sog) throw new Error("503 band");
  return "ok";
};
const b = breakerYarat({
  chegara: 3, ochiqMs: 30_000, soat: () => t,
});

async function sorov() {
  try {
    return await b.chaqir(provayder);
  } catch (x) {
    return x.message;
  }
}

for (const [vaqt, tuzaldi] of [[0], [1], [2], [3], [4],
  [30_004], [60_004, true], [60_005]]) {
  t = vaqt;
  if (tuzaldi) sog = true;
  const javob = await sorov();
  const h = b.joriy();
  console.log(`t=${t}: ${javob} → ${h} (chaqiruv ${chaqiruv})`);
}

Konsolda:

text
t=0: 503 band → yopiq (chaqiruv 1)
t=1: 503 band → yopiq (chaqiruv 2)
t=2: 503 band → ochiq (chaqiruv 3)
t=3: ochiq: darhol rad → ochiq (chaqiruv 3)
t=4: ochiq: darhol rad → ochiq (chaqiruv 3)
t=30004: 503 band → ochiq (chaqiruv 4)
t=60004: ok → yopiq (chaqiruv 5)
t=60005: ok → yopiq (chaqiruv 6)

Qatorlarni kuzating. Uchinchi xatoda breaker ochildi. Keyingi ikki so'rov provayderga bormadi (chaqiruv 3 da qoldi). 30 soniyadan keyin bitta sinov — provayder hali o'lik, breaker yana 30 soniyaga ochildi. Keyingi sinovda provayder tiklangan — breaker yopildi va hammasi odatdagidek.

Soxta soat (soat: () => t) bilan 60 soniyalik stsenariy bir zumda o'tdi. Kanon testi ham shunday: soat: () => t va kut: () => Promise.resolve() — test 0 ms kutadi (timers chuqur darsidagi soxta soat g'oyasi).

4.3 Natijani halol o'qish

Kanon jadvalidagi (c) qatorida "foydalanuvchi kutishi p50 0 ms" yozilgan. Bu yaxshi xabar emas — bu foydalanuvchilarning yarmidan ko'pi darhol xato olganini bildiradi (90 ta ochiq). Breaker kutishni xatoga almashtiradi, muvaffaqiyat bermaydi. Lekin bu almashuv to'g'ri: 1 soniya kutib baribir xato olgandan ko'ra, darhol "AI vaqtincha mavjud emas, 25 soniyadan keyin urinib ko'ring" (503 + Retry-After) olgan yaxshi. Ilovaning qolgan qismi esa (vazifalar, ro'yxatlar) bu paytda to'liq ishlaydi.

Yana bir detal: tajribada breaker 2 soniyaga ochilgan (breaker 5/2 s) — tajriba tez tugashi uchun. Kanon kodining sukuti 30 soniya.

5. Bulkhead

5.1 G'oya

Bulkhead (to'siq devori) — kema korpusidagi suv o'tkazmaydigan bo'limlar. Bitta bo'limga suv kirsa, kema cho'kmaydi. Dasturda: bitta sekin bog'liqlik butun ilovaning resurslarini (ulanishlar, xotira, event loop) yemasligi uchun unga alohida chegara qo'yiladi.

Gemini 20 soniya osilib qolsa va har foydalanuvchi "qadamlarga bo'l" tugmasini bossa — yuzlab ochiq so'rov, har biri xotira va soket ushlab turadi. Kanonda: bir vaqtda ko'pi bilan 8 ta LLM chaqiruvi. 9-chisi darhol band oladi (503) — navbatda kutmaydi. 28-qismdagi semafor ham shunday chegara edi; u yerda navbatning ham chegarasi bor edi, bu yerda navbat umuman yo'q.

5.2 Kanon o'lchovi va uni o'qish

text
=== bulkhead: provayder SOG', lekin sekin emas (20 ms); maksParallel 4, 10 parallel mijoz — ortiqchasi darhol rad (navbatda kutmaydi)
(d) bulkhead 4                               jami     31 ms | foydalanuvchi kutishi p50     0 ms, p95     0 ms | provayderga chaqiruv   4 | {"band":96,"ok":4}

4 ta o'tdi, 96 ta rad. Ko'p tuyuladi — chegara 4, mijozlar 10, demak 6 tasi rad bo'lishi kerak edi-ku? Sabab o'lchov sxemasida: rad etilgan mijoz darhol (0 ms da) navbatdagi so'rovni oladi va u ham rad bo'ladi. 6 ta "bo'sh" mijoz 20 ms ichida qolgan 96 so'rovni rad etilish bilan "yeb" qo'ydi. Haqiqiy foydalanuvchilar bunday tez qayta bosmaydi. Saboq: o'lchov stsenariysi natijani shakllantiradi — raqamni o'qishdan oldin sxemani o'qing.

6. Fallback va graceful degradation

6.1 Kanonda qanday

Zaxira (fallback) — asosiy yo'l ishlamasa, boshqa javob (zaxira model, keshdagi natija yoki tayyor matn). Graceful degradation (ehtiyotkor sifat pasayishi) — butun ilova emas, faqat bitta xususiyat o'chadi. vazifalar-api da AI yiqilganda nima bo'ladi:

Qatlam Nima qiladi Qaysi darsdan
Kesh avval hisoblangan qadamlar keshdan, LLM'siz AI prodakshnda
Zaxira model asosiy model vaqtinchalik xatoda — boshqa model bilan AI SDK va provayderlar
Breaker va bulkhead darhol 503 + Retry-After shu dars
Feature flag administrator AI'ni butunlay o'chiradi — 404 xususiyat-yoq Feature flag'lar
Qolgan API vazifalar, ro'yxat, kirish — AI'ga bog'liq emas —

Oxirgi qator eng muhimi. O'tgan darslardagi health qoidasi ("readiness'ga tashqi API qo'shmang") va bu darsdagi naqshlar bitta maqsadga xizmat qiladi: tashqi xizmatning yiqilishi faqat o'sha xususiyatni o'chirsin.

6.2 Biz topgan tuzoq: zaxira va breaker tartibi

Kanonda o'ramlar tartibi: Zaxirali(Chidamli(asosiy)). Ya'ni zaxira model ham o'sha breaker va retry orqali chaqiriladi. Kanon zaxira.ts da zaxiraga o'tish bitta qator: this.asosiy.yarat({ ...s, model: zaxiraModel }) — bu yerdagi asosiy — ChidamliProvayder, ya'ni bitta umumiy breaker.

flowchart LR
  Z["ZaxiraliProvayder"] --> C["ChidamliProvayder: BITTA breaker"]
  C -->|"model: asosiy"| A["asosiy model (o'lik)"]
  C -->|"model: zaxira"| B["zaxira model (sog')"]

Diagrammaga qarang: ikkala model bitta eshikdan o'tadi. Breaker qaysi model xato qilganini bilmaydi — u faqat "provayder" xatolarini sanaydi. Bu nima beradi? Kichik tajriba yozdik: asosiy model doim 503, zaxira model sog'lom, 10 so'rov, kanon sozlamalari (2 qayta urinish, breaker chegarasi 5). Biz ishga tushirdik, Vitest, kanon kodi:

text
10 so'rov: asosiyga 30, zaxiraga 10, breaker yopiq, backoff kutishlari 20

Breaker hech qachon ochilmadi. Har so'rov: asosiy model 3 marta urinildi (2 ta backoff kutishi bilan), keyin zaxira muvaffaqiyatli bo'ldi — va bu muvaffaqiyat breaker hisoblagichini nolga qaytardi. Natija: o'lik model 30 marta chaqirildi, har foydalanuvchi zaxiraga yetguncha uch urinish va ikki kutishni kutdi. Haqiqiy timeout (20 s) bilan bu — bir daqiqa.

Tuzoqning ikkinchi yuzi ham bor — u yuk ostida chiqadi. Biz kanonning o'sha ikki faylini (chidamli.ts, zaxira.ts) o'zgartirmasdan oldik va soxta provayder bilan ikki stsenariyni yurgizdik: avval so'rovlar birma-bir, keyin 8 tadan parallel. Asosiy model doim 503, zaxira sog', kutishlar 0 ms (kanon testidagi kabi). Biz ishga tushirdik (Node 24.21.0, uch marta — natija bir xil):

text
1 parallel × 10: javob {"ok":10} | chaqiruv {"asosiy":30,"zaxira":10} | breaker yopiq
8 parallel × 2: javob {"ochiq":16} | chaqiruv {"asosiy":12,"zaxira":0} | breaker ochiq

Birinchi qator — yuqoridagi holat: hamma javob oldi, lekin o'lik model 30 marta chaqirildi. Ikkinchi qatorga diqqat. 8 ta so'rov bir vaqtda kelganda asosiy modelning xatolari zaxira muvaffaqiyatidan oldin 5 taga yetdi va breaker ochildi. Endi zaxiraga o'tish ham o'sha yopiq eshikka uriladi. Natija: 16 foydalanuvchining hammasi 503 oldi, sog' zaxira model bir marta ham chaqirilmadi.

Xulosa — bitta umumiy breaker ikki tomonlama xato qiladi:

  • yuk kam bo'lsa — o'lik modelni o'chirmaydi (zaxira muvaffaqiyati hisobni nolga qaytaradi);
  • yuk ko'p bo'lsa — sog' zaxirani ham o'chiradi (ochiq breaker ikkala modelni to'sadi).

Sabab bitta — breaker provayderga bitta, modelga emas. Ikki yo'l bor: har modelga alohida breaker (3-mashq) yoki zaxirani retry'dan oldin sinash. Testlar bu holatni qamramaydi — ZaxiraliProvayder va ChidamliProvayder alohida-alohida sinalgan.

Diqqat: Bu nuqson loyiha kodida ataylab qoldirildi — kanon shu bosqichda muzlatilgan, uning testlari aynan shu kodga yozilgan. Siz loyihaning shu bosqichini kanon bilan bir xil yozasiz (keyingi darslar unga tayanadi), tuzatishni esa 3-mashqda modelda sinaysiz. O'z ishingizda bunday kodni ko'rsangiz — har modelga alohida breaker qo'ying va "asosiy o'lik, zaxira sog'" stsenariysiga regressiya testi yozing.

7. Tayyor kutubxonalar

Kanon o'z ~100 qatorli dekoratorini yozdi (0 paket). Sabablar: LlmXatosi turlari bilan to'g'ridan ishlash, Retry-After ni hurmat qilish, oqim uchun alohida qoida. Lekin ish joyida tayyor kutubxonalar ko'p uchraydi. Kanon ularni sinamagan — biz alohida papkada sinab ko'rdik (opossum 10.0.0, p-retry 8.0.1, p-limit 7.3.3, 7 kundan eski versiyalar). 2026-11-02 da p-retry 9.0.0 chiqdi — uni sinamadik, yangilashda o'zgarishlar ro'yxatini o'qing.

opossum — Node uchun eng mashhur circuit breaker. U xatolar foizi bilan ishlaydi (kanondagi kabi "ketma-ket N" emas), volumeThreshold — kamida nechta chaqiruvdan keyin hisoblash:

js
import CircuitBreaker from "opossum";

let sog = false;
let chaqiruv = 0;
async function aiQadamlar(matn) {
  chaqiruv++;
  if (!sog) throw new Error("503 band");
  return `qadamlar: ${matn}`;
}

const breaker = new CircuitBreaker(aiQadamlar, {
  timeout: 1000,
  errorThresholdPercentage: 50,
  volumeThreshold: 5,
  resetTimeout: 200,
});
breaker.fallback(() => "AI hozir band — keyinroq urinib ko'ring");
for (const h of ["open", "halfOpen", "close"]) {
  breaker.on(h, () => console.log(`[holat] ${h}`));
}

for (let i = 1; i <= 7; i++) {
  console.log(`${i}: ${await breaker.fire("Non olish")}`);
}
console.log(`provayderga chaqiruv: ${chaqiruv}`);
sog = true;
await new Promise((ok) => setTimeout(ok, 250));
console.log(`8: ${await breaker.fire("Non olish")}`);
console.log(`provayderga chaqiruv: ${chaqiruv}`);
breaker.shutdown();

Biz ishga tushirdik (Node 24.21.0, ikki marta — natija bir xil):

text
1: AI hozir band — keyinroq urinib ko'ring
2: AI hozir band — keyinroq urinib ko'ring
3: AI hozir band — keyinroq urinib ko'ring
4: AI hozir band — keyinroq urinib ko'ring
[holat] open
5: AI hozir band — keyinroq urinib ko'ring
6: AI hozir band — keyinroq urinib ko'ring
7: AI hozir band — keyinroq urinib ko'ring
provayderga chaqiruv: 5
[holat] halfOpen
[holat] close
8: qadamlar: Non olish
provayderga chaqiruv: 6

fallback bilan foydalanuvchi xato emas, tayyor matn oladi. 5 chaqiruvdan keyin breaker ochildi, 6- va 7-so'rov provayderga bormadi. 250 ms dan keyin yarim-ochiq sinov o'tdi va breaker yopildi.

p-retry va p-limit — retry va parallellik chegarasi uchun kichik paketlar:

js
import pRetry, { AbortError } from "p-retry";
import pLimit from "p-limit";

let n = 0;
const natija = await pRetry(async () => {
  n++;
  if (n < 3) throw new Error(`503 band (${n})`);
  return "ok";
}, {
  retries: 3, minTimeout: 10, factor: 2, randomize: false,
  onFailedAttempt: ({ error, attemptNumber, retryDelay }) => {
    console.log(`${attemptNumber}-urinish: ${error.message}, `
      + `keyingisi ${retryDelay} ms dan keyin`);
  },
});
console.log(`natija: ${natija}, urinishlar: ${n}`);

let m = 0;
try {
  await pRetry(async () => {
    m++;
    throw new AbortError("422: matn bo'sh");
  }, { retries: 3 });
} catch (x) {
  console.log(`AbortError: ${x.message}, urinishlar: ${m}`);
}

const cheklov = pLimit(2); // bir vaqtda ko'pi bilan 2 ta
let faol = 0;
let eng = 0;
const ish = (i) => cheklov(async () => {
  faol++;
  eng = Math.max(eng, faol);
  await new Promise((ok) => setTimeout(ok, 20));
  faol--;
  return i;
});
const javoblar = await Promise.all([1, 2, 3, 4, 5].map(ish));
console.log(`javoblar: ${javoblar.join(", ")};`
  + ` eng ko'p parallel: ${eng}`);

Biz ishga tushirdik:

text
1-urinish: 503 band (1), keyingisi 10 ms dan keyin
2-urinish: 503 band (2), keyingisi 20 ms dan keyin
natija: ok, urinishlar: 3
AbortError: 422: matn bo'sh, urinishlar: 1
javoblar: 1, 2, 3, 4, 5; eng ko'p parallel: 2

AbortError — p-retry'da "bu doimiy xato, qayta urinma" belgisi. U kanondagi vaqtinchalik ning teskarisi. randomize: false ni ataylab qo'ydik (chiqish takrorlansin); p-retry jitter'i kutishni 1–2 baravarga ko'paytiradi — bu kanondagi "to'liq jitter" dan boshqacha. p-limit — navbatli chegara: ortiqchasi kutadi. Kanon bulkhead'i esa ortiqchasini darhol rad etadi. Navbat kerakmi yoki darhol rad — xizmatga bog'liq: AI uchun 20 soniya navbatda kutishdan darhol 503 yaxshiroq.

8. Ko'p uchraydigan xatolar

Xato Oqibat To'g'risi
Timeout'siz fetch so'rov abadiy osiladi AbortSignal.timeout
Har xatoda retry 400, sxema xatosi qayta-qayta faqat vaqtinchalik
Idempotent bo'lmagan so'rovni retry ikki marta saqlash Idempotency-Key
Jitter'siz backoff hamma birdaniga qaytadi to'liq jitter
Retry-After ga e'tibor bermaslik provayder bloklaydi max(jitter, Retry-After)
Breaker'siz retry retry bo'roni (3× yuk) breaker + retry
Cheksiz navbat xotira to'ladi bulkhead yoki navbat chegarasi
Ichki timeout'lar > mijoz kutishi behuda ish deadline byudjeti

9. Mashqlar

Mashqlar kurs/mashqlar/31/29-chidamlilik/ da. 2- va 3-mashq yechimlari Node 24.21.0 da tekshirilgan (biz ishga tushirdik).

1-mashq (oson): bo'shliqlarni to'ldiring

  • Breaker xizmatni chaqirmaydigan holat —
  • Vaqt o'tgach bitta sinov so'roviga ruxsat beradigan holat — -ochiq
  • Hamma mijoz bir vaqtda qaytmasligi uchun kutishga qo'shiladigan tasodif —
  • Faqat retry qo'shilganda kanon o'lchovida provayderga chaqiruvlar necha baravar ko'paydi —
  • Bir vaqtdagi chaqiruvlarga chegara qo'yadigan naqsh —
Yechim
  • ochiq — darhol rad, provayder dam oladi.
  • yarim — half-open; sinov o'tsa — yopiq, yiqilsa — ochiq.
  • jitter — kanonda to'liq jitter: random(0, asos·2ⁿ).
  • 3 — 100 dan 300 ga: har so'rov uch marta urinildi.
  • bulkhead — kanonda 8 ta, ortiqchasi darhol 503.

2-mashq (o'rta): eng yomon holat vaqti

engYomon(sozlama) funksiyasini yozing: provayder har urinishda timeout'gacha osilsa, foydalanuvchi eng ko'pi bilan necha soniya kutadi. Kirish: timeoutMs, qaytaUrinish, asosMs va ixtiyoriy zaxira (true bo'lsa — hammasi zaxira model bilan yana bir marta). Kutish — jitter'ning yuqori chegarasi: min(8000, asos · 2ⁿ). Kanon sozlamalarini va "8 s, 1 qayta" variantini hisoblang. Ishora: for sikli va spread.

Yechim
js
function engYomon({
  timeoutMs, qaytaUrinish, asosMs, zaxira = false,
}) {
  let jami = 0;
  for (let n = 0; n <= qaytaUrinish; n++) {
    jami += timeoutMs; // urinish timeout'gacha osildi
    if (n < qaytaUrinish) jami += Math.min(8000, asosMs * 2 ** n);
  }
  return (zaxira ? jami * 2 : jami) / 1000;
}

const kanon = { timeoutMs: 20_000, qaytaUrinish: 2, asosMs: 300 };
console.log("kanon:", engYomon(kanon), "s");
console.log("kanon + zaxira:",
  engYomon({ ...kanon, zaxira: true }), "s");
console.log("8 s, 1 qayta:",
  engYomon({ timeoutMs: 8000, qaytaUrinish: 1, asosMs: 300 }), "s");

Konsolda:

text
kanon: 60.9 s
kanon + zaxira: 121.8 s
8 s, 1 qayta: 16.3 s

Kanon sozlamalarida eng yomon holat — bir daqiqa, zaxira bilan — ikki daqiqa. Hech bir foydalanuvchi buncha kutmaydi, brauzer va balanser esa ancha oldin uzadi (ko'pchilik balanserlarda sukut 60 s). Breaker bu holatni faqat ketma-ket xatolardan keyin yumshatadi; birinchi foydalanuvchilar baribir to'liq vaqtni kutadi. Shuning uchun AI uchun timeout'ni kichikroq tanlash (kanondagi jonli o'lchovda javob 1–2 s da kelgan) yoki umumiy deadline qo'yish kerak.

3-mashq (qiyin): har modelga alohida breaker

«Biz topgan tuzoq» bo'limidagi muammoni modelda takrorlang. Keyin uni tuzating. breaker(chegara) — ketma-ket xatolarni sanaydigan oddiy o'chirgich (vaqt bo'yicha yarim-ochiq bu mashqda yo'q). zaxirali(breakerOl, hisob) — avval "asosiy", keyin "zaxira" modelni sinaydi, breaker'i ochiq modelni o'tkazib yuboradi. Asosiy model doim 503. 10 so'rovda ikki variantni solishtiring: bitta umumiy breaker va har modelga alohida (Map bilan).

Yechim
js
// Soddalashtirilgan breaker: ketma-ket `chegara` xatodan keyin ochiq
// (vaqt bo'yicha yarim-ochiq bu mashqda yo'q)
function breaker(chegara) {
  let xatolar = 0;
  return {
    ochiqmi: () => xatolar >= chegara,
    yoz: (ok) => { xatolar = ok ? 0 : xatolar + 1; },
  };
}

async function provayder(model, hisob) {
  hisob[model]++;
  if (model === "asosiy") throw new Error("503 band"); // asosiy o'lik
  return `javob (${model})`;
}

async function zaxirali(breakerOl, hisob) {
  for (const model of ["asosiy", "zaxira"]) {
    const b = breakerOl(model);
    if (b.ochiqmi()) continue; // bu model hozir o'tkazib yuboriladi
    try {
      const javob = await provayder(model, hisob);
      b.yoz(true);
      return javob;
    } catch {
      b.yoz(false);
    }
  }
  return "503";
}

async function sina(nom, breakerOl) {
  const hisob = { asosiy: 0, zaxira: 0 };
  for (let i = 0; i < 10; i++) await zaxirali(breakerOl, hisob);
  console.log(nom, hisob);
}

const umumiy = breaker(5);
await sina("umumiy breaker:", () => umumiy);

const breakerlar = new Map();
await sina("har modelga breaker:", (model) => {
  if (!breakerlar.has(model)) breakerlar.set(model, breaker(5));
  return breakerlar.get(model);
});

Konsolda:

text
umumiy breaker: { asosiy: 10, zaxira: 10 }
har modelga breaker: { asosiy: 5, zaxira: 10 }

Umumiy breaker'da zaxiraning muvaffaqiyati asosiy modelning xatolarini "o'chirib" turadi — o'lik model har safar chaqiriladi. Alohida breaker'da asosiy model 5 xatodan keyin chetlab o'tiladi: 6-so'rovdan boshlab foydalanuvchi to'g'ridan zaxiraga boradi. Haqiqiy kodda yarim-ochiq holat ham kerak — aks holda asosiy model tiklansa ham, unga hech qachon qaytilmaydi.

10. Loyiha qadami: ChidamliProvayder

10.1 src/ai/chidamli.ts

Yangi fayl — dekorator: LlmProvayder shartnomasini o'zi ham bajaradi va ichidagi provayderni o'raydi. Ilova kodi (qadamlar.ts, yordamchi.ts, qidiruv.ts) hech narsani sezmaydi. Kanon faylidagi uzun qatorlarni telefon uchun bo'ldik, izohlarni qisqartirdik, mantiq aynan:

ts
// chidamli.ts — 31-qism #29: resilience naqshlari LLM provayderi
// atrofida (dekorator — ilova kodi o'zgarmaydi):
//   timeout — provayderda, har urinishga;
//   retry — faqat vaqtinchalik xato; eksponensial + to'liq jitter;
//     Retry-After hurmat qilinadi;
//   circuit breaker — ketma-ket N xato → OCHIQ: T ms davomida darhol
//     `ochiq` xatosi; keyin YARIM-OCHIQ — bitta sinov so'rovi;
//   bulkhead — bir vaqtda ko'pi bilan K ta LLM chaqiruvi, qolganlari
//     darhol `band`: sekin provayder butun ilovani yemasin.
// Opossum — SINALMAGAN (o'z ~100 qatorli dekoratorimiz); 0 paket.
import { LlmXatosi } from "./llm.ts";
import type {
  EmbeddingTuri, LlmBolak, LlmJavob, LlmProvayder, LlmSorov,
} from "./llm.ts";

export type ChidamlilikSozlamasi = {
  /** vaqtinchalik xatoda qo'shimcha urinishlar (0 — o'chiq) */
  qaytaUrinish: number;
  /** backoff asosi (sukut 300 ms): kutish = random(0, asos·2^n),
   *  maks 8 s */
  asosMs?: number;
  /** ketma-ket xatolar (sukut 5) */
  breakerChegara?: number;
  /** ochiq turish vaqti (sukut 30 s) */
  breakerOchiqMs?: number;
  /** bulkhead (sukut 8) */
  maksParallel?: number;
  /** testlar uchun */
  kut?: (ms: number, signal?: AbortSignal) => Promise<void>;
  soat?: () => number;
};

export type BreakerHolati = "yopiq" | "ochiq" | "yarim";

const sukutKut = (ms: number, signal?: AbortSignal) =>
  new Promise<void>((ok, rad) => {
    const t = setTimeout(ok, ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(t);
      rad(signal.reason instanceof Error
        ? signal.reason
        : new DOMException("bekor", "AbortError"));
    }, { once: true });
  });

export class ChidamliProvayder implements LlmProvayder {
  readonly nom: string;
  readonly model: string;
  readonly #s: Required<ChidamlilikSozlamasi>;
  #holat: BreakerHolati = "yopiq";
  #xatolar = 0;
  #ochilgan = 0;
  #band = 0;
  /** metrikalar: urinishlar, qayta urinishlar, breaker va bulkhead
   *  rad etganlar */
  readonly hisob = {
    chaqiruv: 0, qaytaUrinish: 0, breakerRad: 0, bulkheadRad: 0,
  };

  constructor(
    private readonly ichki: LlmProvayder,
    s: ChidamlilikSozlamasi,
  ) {
    this.nom = ichki.nom;
    this.model = ichki.model;
    this.#s = {
      asosMs: 300, breakerChegara: 5, breakerOchiqMs: 30_000,
      maksParallel: 8, kut: sukutKut, soat: Date.now, ...s,
    };
  }

  get holat(): BreakerHolati {
    const otdi = this.#s.soat() - this.#ochilgan;
    if (this.#holat === "ochiq" && otdi >= this.#s.breakerOchiqMs) {
      this.#holat = "yarim";
    }
    return this.#holat;
  }

  #ruxsat(): void {
    const h = this.holat;
    if (h === "ochiq") {
      this.hisob.breakerRad++;
      const qoldi = this.#s.breakerOchiqMs
        - (this.#s.soat() - this.#ochilgan);
      throw new LlmXatosi("ochiq",
        "AI provayderi vaqtincha o'chirilgan (circuit breaker ochiq)",
        qoldi);
    }
    if (this.#band >= this.#s.maksParallel) {
      this.hisob.bulkheadRad++;
      const k = String(this.#s.maksParallel);
      throw new LlmXatosi("band",
        `bir vaqtda ${k} tadan ko'p AI so'rovi`, 1000);
    }
  }

  #muvaffaqiyat(): void {
    this.#xatolar = 0;
    this.#holat = "yopiq";
  }

  #xato(x: unknown): void {
    // foydalanuvchi bekor qilgani, sxema xatosi — provayder
    // sog'lig'iga aloqasi yo'q
    if (!(x instanceof LlmXatosi) || !x.vaqtinchalik) return;
    this.#xatolar++;
    const chegara = this.#xatolar >= this.#s.breakerChegara;
    if (this.#holat === "yarim" || chegara) {
      this.#holat = "ochiq";
      this.#ochilgan = this.#s.soat();
    }
  }

  async #urin<T>(fn: () => Promise<T>, signal?: AbortSignal)
    : Promise<T> {
    this.#ruxsat();
    this.#band++;
    try {
      for (let n = 0; ; n++) {
        this.hisob.chaqiruv++;
        try {
          const r = await fn();
          this.#muvaffaqiyat();
          return r;
        } catch (x) {
          this.#xato(x);
          const qayta = x instanceof LlmXatosi && x.vaqtinchalik
            && n < this.#s.qaytaUrinish && this.holat !== "ochiq"
            && signal?.aborted !== true;
          if (!qayta) throw x;
          this.hisob.qaytaUrinish++;
          const jitter = Math.random()
            * Math.min(8000, this.#s.asosMs * 2 ** n);
          const kutish = Math.max(jitter, x.qaytaUrinishMs ?? 0);
          await this.#s.kut(kutish, signal);
        }
      }
    } finally {
      this.#band--;
    }
  }

  yarat(s: LlmSorov): Promise<LlmJavob> {
    return this.#urin(() => this.ichki.yarat(s), s.signal);
  }

  /** oqim: qayta urinish yo'q (yarim javob mijozga ketgan bo'lishi
   *  mumkin); breaker va bulkhead — bor */
  async *oqim(s: LlmSorov): AsyncIterable<LlmBolak> {
    this.#ruxsat();
    this.#band++;
    try {
      for await (const b of this.ichki.oqim(s)) yield b;
      this.#muvaffaqiyat();
    } catch (x) {
      this.#xato(x);
      throw x;
    } finally {
      this.#band--;
    }
  }

  embedding(
    matnlar: string[], tur: EmbeddingTuri, signal?: AbortSignal,
  ): Promise<number[][]> {
    return this.#urin(
      () => this.ichki.embedding(matnlar, tur, signal), signal);
  }
}

O'qishda e'tibor bering:

  • # maydonlar (xususiy maydonlar) — breaker holatini tashqaridan o'zgartirib bo'lmaydi; faqat holat getter'i va hisob ochiq (metrika va test uchun).
  • holat getter'i vaqtni o'zi tekshiradi — alohida taymer yo'q. "Ochiq → yarim" o'tishi keyingi so'rov kelganda hisoblanadi.
  • #xato faqat vaqtinchalik xatoni sanaydi — foydalanuvchi bekor qilgani yoki sxema xatosi provayder sog'lig'iga aloqasi yo'q.
  • finally da #band-- — xato bo'lsa ham bulkhead joyi bo'shaydi.
  • Retry ichida holat !== "ochiq" — breaker o'rtada ochilsa, qolgan urinishlar qilinmaydi.
  • ochiq xatosi qoldi ni beradi — controller uni Retry-After ga aylantiradi: mijoz qachon qaytishni biladi.

10.2 Ulash: src/ai/ai.module.ts

Importga import { ChidamliProvayder } from "./chidamli.ts";. provayderYarat endi:

ts
export function provayderYarat(c: Config): LlmProvayder | null {
  const asosiy = asosiyProvayder(c);
  if (asosiy === null) return null;
  // #29: retry (vaqtinchalik xato, jitter, Retry-After) + circuit
  // breaker + bulkhead — provayder atrofida; #16: zaxira — tashqarida
  const p = new ChidamliProvayder(asosiy, {
    qaytaUrinish: c.AI_QAYTA_URINISH,
  });
  return c.AI_ZAXIRA_MODEL === undefined
    ? p
    : new ZaxiraliProvayder(p, c.AI_ZAXIRA_MODEL);
}

AI_QAYTA_URINISH (sukut 2) config'da AI darslaridan beri bor edi — endi ishlatiladi. Qolgan sozlamalar (breaker 5/30 s, bulkhead 8) — sukut. Zaxira eng tashqarida — «Biz topgan tuzoq» bo'limidagi oqibat shu tartibdan.

10.3 Test: test/chidamlilik.test.ts

Uchta test, tarmoqsiz: soxta provayder, soxta soat va soxta kutish (kut darhol qaytadi, faqat so'ralgan ms ni yozib oladi). Kanon testining sarlavha satrlarini telefon uchun bo'ldik:

ts
// chidamlilik.test.ts — 31-qism #29: ChidamliProvayder — retry
// (faqat vaqtinchalik, Retry-After), circuit breaker (yopiq → ochiq →
// yarim → yopiq), bulkhead. Soxta provayder va soat — tarmoqsiz.
import { describe, test } from "vitest";
import assert from "node:assert/strict";
import { SoxtaProvayder } from "../src/ai/soxta.ts";
import { LlmXatosi } from "../src/ai/llm.ts";
import type { LlmSorov } from "../src/ai/llm.ts";
import { ChidamliProvayder } from "../src/ai/chidamli.ts";

const S: LlmSorov = {
  tizim: "t", xabarlar: [{ rol: "user", matn: "x" }],
};
const band = () => new LlmXatosi("band", "503", undefined, 503);

describe("#29 ChidamliProvayder", () => {
  test("retry: 2 vaqtinchalik xato → 3-urinishda ok; kutish "
    + "≤ asos·2^n, Retry-After ustun; doimiy xato — qayta "
    + "urinilmaydi", async () => {
    const kutishlar: number[] = [];
    const p = new SoxtaProvayder({
      javob: () => "ok",
      xatolar: [band(), new LlmXatosi("limit", "429", 2000, 429)],
    });
    const c = new ChidamliProvayder(p, {
      qaytaUrinish: 2, asosMs: 100,
      kut: (ms) => { kutishlar.push(ms); return Promise.resolve(); },
    });
    assert.equal((await c.yarat(S)).matn, "ok");
    assert.equal(p.chaqiruvlar.length, 3);
    const k0 = kutishlar[0] ?? -1;
    assert.ok(k0 >= 0 && k0 <= 100);
    assert.ok((kutishlar[1] ?? 0) >= 2000); // Retry-After
    p.s.xatolar = [new LlmXatosi("notogri", "sxema")];
    await assert.rejects(c.yarat(S), /sxema/);
    assert.equal(p.chaqiruvlar.length, 4);
  });

  test("circuit breaker: 3 ketma-ket xato → ochiq (provayder "
    + "chaqirilmaydi); vaqt o'tgach yarim — ok yopadi, xato qayta "
    + "ochadi", async () => {
    let t = 0;
    const p = new SoxtaProvayder({
      javob: () => "ok", xatolar: [band(), band(), band()],
    });
    const c = new ChidamliProvayder(p, {
      qaytaUrinish: 0, breakerChegara: 3, breakerOchiqMs: 10_000,
      soat: () => t, kut: () => Promise.resolve(),
    });
    for (let i = 0; i < 3; i++) {
      await assert.rejects(c.yarat(S), /503/);
    }
    assert.equal(c.holat, "ochiq");
    await assert.rejects(c.yarat(S),
      (x: unknown) => x instanceof LlmXatosi && x.tur === "ochiq");
    assert.equal(p.chaqiruvlar.length, 3);
    t = 10_000;
    assert.equal(c.holat, "yarim");
    p.s.xatolar = [band()];
    await assert.rejects(c.yarat(S), /503/);
    // yarim-ochiqdagi bitta xato — yana ochiq
    assert.equal(c.holat, "ochiq");
    t = 20_000;
    assert.equal((await c.yarat(S)).matn, "ok");
    assert.equal(c.holat, "yopiq");
    assert.equal(c.hisob.breakerRad, 1);
  });

  test("bulkhead: maksParallel=2 — uchinchi parallel chaqiruv darhol "
    + "'band'; joy bo'shasa — yana o'tadi", async () => {
    const p = new SoxtaProvayder({
      javob: () => "ok", kechikishMs: 50,
    });
    const c = new ChidamliProvayder(p, {
      qaytaUrinish: 0, maksParallel: 2,
    });
    const natijalar = await Promise.allSettled(
      [c.yarat(S), c.yarat(S), c.yarat(S)]);
    assert.deepEqual(natijalar.map((n) => n.status),
      ["fulfilled", "fulfilled", "rejected"]);
    assert.equal(c.hisob.bulkheadRad, 1);
    assert.equal((await c.yarat(S)).matn, "ok");
  });
});

Ikkinchi test butun holat mashinasini (diagrammadagi to'rt o'q) bir chiziqda o'tadi — soxta soat t ni qo'lda suradi.

10.4 Tekshiruv

bash
npm run tip
npx vitest run test/chidamlilik.test.ts test/ai.test.ts \
  test/ai-qidiruv.test.ts

Biz loyiha nusxasida (Node 24.21.0, bazasiz): tsc --noEmit xatosiz, AI testlari o'zgarmay o'tdi (bazali qismlari o'tkazildi):

text
 ✓ |asosiy| test/chidamlilik.test.ts (3 tests) 117ms
 ✓ |asosiy| test/ai-qidiruv.test.ts (2 tests | 1 skipped) 10ms
 ✓ |asosiy| test/ai.test.ts (4 tests | 1 skipped) 17ms
 Test Files  3 passed (3)
      Tests  7 passed | 2 skipped (9)

Kanonning jonli tekshiruvida (haqiqiy Gemini, Postgres, Redis) ChidamliProvayder va ZaxiraliProvayder o'ramida qadamlar so'rovi HTTP 200, 1.622131 s bilan o'tdi — sog' provayderda o'ram sezilmaydi.

Hali commit qilmang — bosqichning oxirgi darsi keyingisi.

11. Real ishda

  • Qayerda uchraydi: to'lov tizimlari (Payme, Click), SMS provayderlari, AI API'lari, mikroservislar orasidagi har chaqiruv. Bitta servis yiqilib, butun tizimni tortib ketishi — yirik uzilishlarning klassik sababi.
  • Vositalar: Node'da opossum, cockatiel, p-retry, p-limit; Java'da Resilience4j; servis mesh (Istio, Linkerd) bu naqshlarni tarmoq darajasida beradi.
  • Intervyu savollari: "Circuit breaker holatlarini tushuntiring", "Retry qachon xavfli?", "Jitter nima uchun?", "Bulkhead va rate limit farqi?", "Graceful degradation'ga misol keltiring".
  • Monitoring: hisob (chaqiruv, qayta urinish, breaker va bulkhead rad etganlar) — o'tgan darsdagi /metrics ga chiqarishga tayyor raqamlar. Breaker ochilgani — alert uchun yaxshi simptom.

Xulosa

  • Har tashqi chaqiruvga timeout; ichki timeout'lar yig'indisi mijoz kutadigan vaqtdan kichik bo'lsin.
  • Retry — faqat vaqtinchalik xato va idempotent amal; eksponensial kutish, to'liq jitter, Retry-After.
  • Breakersiz retry — retry bo'roni: kanonda 3× yuk va 3.2× vaqt; breaker bilan — 14 chaqiruv.
  • Breaker: yopiq → ochiq (darhol 503) → yarim-ochiq (bitta sinov) → yopiq.
  • Bulkhead — sekin bog'liqlikka alohida chegara; ortiqchasini darhol rad etish navbatdan yaxshi bo'lishi mumkin.
  • Zaxira model bitta breaker ortida bo'lsa: yuk kam — o'lik model breaker'ni ochmaydi; yuk ko'p — ochiq breaker sog' zaxirani ham to'sadi. Breaker har modelga alohida bo'lishi kerak (loyiha kodida bu nuqson ataylab qoldirilgan).

Keyingi dars: Backend unumdorligini oshirish — o'lchab, eng sekin joyni topib, faqat o'shani tuzatamiz va loyihaning shu bosqichini commit qilamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Resilience naqshlari: timeout, retry va jitter, circuit breaker, bulkhead, fallback — IlmHamroh