IlmHamroh
JavaScript Full-stack/30-qism. NestJS39/65-dars22 daqiqa
Mundarija (32)

Rejalashtirish NestJS'da: @nestjs/schedule — @Cron, @Interval, @Timeout

Qisqacha: @nestjs/schedule — Nest'ning rejalashtirish moduli. ScheduleModule.forRoot() ni ulaysiz, keyin xizmat metodiga dekorator qo'yasiz: @Interval(60_000) — har daqiqada, @Timeout(5000) — bir marta, @Cron("0 30 3 * * *", { timeZone: "Asia/Tashkent" }) — har kuni 03:30 da. Nest taymerlarni ilova ishga tushganda yoqadi va app.close() da o'chiradi. Uchta tuzoq bor: vaqt zonasi, ustma-ust ishga tushish va ikki nusxa — har nusxa o'z jadvalini yurgizadi.

Bu darsda

  • ScheduleModule ni ulab, @Interval, @Timeout va @Cron bilan rejali ish yozasiz.
  • Nest cron ifodasidagi oltinchi maydon (soniya) va CronExpression ro'yxatini ishlatasiz.
  • SchedulerRegistry bilan ishlarni nomi bo'yicha topasiz, qo'shasiz va o'chirasiz.
  • Xato, ustma-ust ishga tushish va ikki nusxa tuzoqlarini haqiqiy chiqishda ko'rasiz.
  • vazifalar-api dagi eksport tozalashni @Interval ga o'tkazasiz.

Oldin bilishingiz kerak: Cron va rejali vazifalar, Dynamic modullar, Lifecycle hook'lar, timers va timers/promises chuqur.

Oldingi darsda CacheModule ni sinab, kanonga kiritmadik. Bu darsdan boshlab to'rt dars bitta loyiha bosqichini quradi: rejalashtirish, domen hodisalari, salomatlik tekshiruvi va SSE oqimi. Hammasi tayyor bo'lgach, to'rtinchi darsda bitta commit qilamiz.

1. Nega bu kerak?

vazifalar-api dagi eksportni eslang (Bulk va uzoq davom etuvchi operatsiyalar). Mijoz POST /v1/eksportlar yuboradi, server ishni fonda bajaradi va natijani bir soat xotirada saqlaydi. Bir soatdan keyin natija o'chishi kerak, aks holda xotira to'lib boradi.

Hozirgi kod buni qanday qiladi? EksportService.topilsin() har chaqiruvda butun Map ni aylanib, muddati o'tganlarni o'chiradi:

ts
topilsin(id: string): Ish {
  for (const [k, i] of this.#ishlar)
    if (i.tugash <= this.soat()) this.#ishlar.delete(k);
  // ...
}

Bu ishlaydi, lekin ikki kamchiligi bor. Birinchisi — tozalash faqat kimdir eksport holatini so'raganda bo'ladi. Hech kim so'ramasa, eski natijalar xotirada yotaveradi. Ikkinchisi — har o'qish so'rovi tozalash ishini ham bajaradi: o'qish va tozalash bir-biriga bog'lanib qolgan.

To'g'risi — tozalashni alohida, jadval bo'yicha bajarish: har daqiqada bir marta. Cron va rejali vazifalar darsida buni croner kutubxonasi bilan qo'lda qilgandik: jadvalni yaratish, ilova yopilganda to'xtatish — hammasini o'zimiz yozdik. Nest'da shu ishni bitta dekorator bajaradi.

O'xshatish: maktab qo'ng'irog'i. Har tanaffusni o'qituvchi soatga qarab o'zi e'lon qilsa — chalkashlik. Qo'ng'iroq esa jadval bo'yicha o'zi chalinadi. @nestjs/schedule — Nest ilovasining qo'ng'irog'i.

2. ScheduleModule va uch dekorator

2.1 O'rnatish

bash
npm i --save-exact @nestjs/schedule@12.0.2

Paket ichida cron kutubxonasi (4.4.0) ishlaydi. Cron jadvali — o'sha kutubxonaning CronJob obyekti. Interval va timeout esa Node'ning oddiy setInterval va setTimeout lari.

Ulash — ildiz modulda bitta qator. forRoot() — dinamik modul: u SchedulerRegistry va dekoratorlarni qidiruvchi xizmatni ilovaga qo'shadi.

ts
import { Module } from "@nestjs/common";
import { ScheduleModule } from "@nestjs/schedule";

@Module({
  imports: [ScheduleModule.forRoot()],
  providers: [Vazifalar],
})
export class AppModule {}

forRoot() ga sozlama ham berish mumkin: { cronJobs: false } — cron dekoratorlari e'tiborsiz qoladi (intervals, timeouts ham shunday). Bu testda yoki faqat API sifatida ishlaydigan nusxada qulay.

2.2 Uch dekorator

Biz demo loyihada (NestJS 12.2.0, @nestjs/schedule 12.0.2) uchala dekoratorni bitta xizmatga qo'ydik:

ts
import { Injectable } from "@nestjs/common";
import {
  Cron, CronExpression, Interval, Timeout,
} from "@nestjs/schedule";

@Injectable()
class Vazifalar {
  @Interval("tozalash", 100) // har 100 ms
  tozala() {
    bir("interval");
  }

  @Timeout("isinish", 250) // ishga tushgandan 250 ms keyin, bir marta
  isit() {
    bir("timeout");
  }

  @Cron(CronExpression.EVERY_SECOND, { name: "soniya" })
  soniya() {
    bir("cron");
  }
}

bir(nom) — sanagichni bittaga oshiradigan kichik yordamchi. Har dekoratorning birinchi argumenti — nom (ixtiyoriy). Nom berilsa, ishni keyin SchedulerRegistry dan topish mumkin.

Ilovani ishga tushirib, taxminan bir soniya kutdik va sanagichlarni chiqardik (Node 24.21.0, biz ishga tushirdik):

text
1) intervals: tozalash, xatoli, sekin
   timeouts: isinish
   cronJobs: soniya, sekin-cron, tungi, tungi-utc
   ~1 s ichida: {"interval":10,"timeout":1,"cron":1,...}

Xizmatda yana bir nechta ish bor edi (xatoli, sekin va boshqalar) — ularni keyingi bo'limlarda ko'ramiz. Hozircha uchtasiga qarang. Interval bir soniyada 10 marta ishladi, timeout — bir marta, cron — bir marta (har soniyada). Taymerlar vaqtga bog'liq, shuning uchun sizda 9 yoki 11 chiqishi mumkin.

2.3 Taymerlar qachon yonadi va o'chadi

Dekorator qo'yilgan metod o'zi ishga tushmaydi. ScheduleModule ilova to'liq yig'ilgandan keyin — onApplicationBootstrap bosqichida (Lifecycle hook'lar) — hamma provider'larni ko'rib chiqadi. Dekorator topsa, taymerni yoqadi. app.close() da esa hammasini o'chiradi.

Bu ikki narsani anglatadi:

  • onModuleInit ichida jadval hali ishlamaydi — taymerlar undan keyin yonadi.
  • Testda app.close() ni unutsangiz, setInterval jarayonni tirik ushlab turadi va test runner tugamaydi. Shuning uchun loyiha testida afterAll da app.close() bor.

Tekshirib ko'ring: @Timeout("isinish", 250) va setTimeout(isit, 250) ni constructor ichida yozish o'rtasida qanday farq bor?

Javob

Uchta farq bor. Birinchisi — vaqt: constructor provider yaratilayotganda ishlaydi, ilova hali yig'ilmagan bo'lishi mumkin; @Timeout esa ilova to'liq tayyor bo'lgach sanay boshlaydi. Ikkinchisi — boshqaruv: dekoratorli timeout SchedulerRegistry da isinish nomi bilan turadi, uni topib o'chirish mumkin. Uchinchisi — app.close() uni o'zi tozalaydi, qo'lda yozilgan setTimeout ni esa o'zingiz tozalashingiz kerak.

3. Cron ifodasi Nest'da

3.1 Oltinchi maydon — soniya

Cron darsida besh maydonni o'rgangandik: daqiqa, soat, kun, oy, hafta kuni. cron kutubxonasi oltinchi maydonni ham qabul qiladi — u boshida turadi va soniyani bildiradi:

Ifoda Ma'nosi
30 3 * * * har kuni 03:30 da (besh maydon)
0 30 3 * * * xuddi shu, soniya bilan (olti maydon)
*/10 * * * * * har 10 soniyada
0 0 9 * * 1-5 dushanbadan jumagacha 09:00 da

Ifodani qo'lda yozish o'rniga CronExpression ro'yxatidan olish mumkin. Paketda 80 dan ortiq tayyor ifoda bor. Ulardan bir nechtasi (biz paket ichidan ko'chirdik):

text
EVERY_SECOND            = "* * * * * *"
EVERY_10_SECONDS        = "*/10 * * * * *"
EVERY_HOUR              = "0 0-23/1 * * *"
EVERY_DAY_AT_3AM        = "0 03 * * *"
EVERY_DAY_AT_MIDNIGHT   = "0 0 * * *"
MONDAY_TO_FRIDAY_AT_9AM = "0 0 09 * * 1-5"

Qarang: EVERY_SECOND — olti maydonli, EVERY_DAY_AT_3AM esa besh maydonli. Kutubxona ikkalasini ham tushunadi. Maydonlar sonini sanab, birinchisi soniyami yoki daqiqami — shuni hal qiladi.

3.2 Vaqt zonasi

Cron darsidagi birinchi tuzoq Nest'da ham o'sha: server UTC'da bo'lsa, "03:30" Toshkentda emas, UTC bo'yicha 03:30 bo'ladi. Ikki jadvalni solishtirdik — biri timeZone bilan, biri usiz. Jarayonni TZ=UTC bilan ishga tushirdik (ko'p serverlar shunday):

ts
@Cron("0 30 3 * * *", { name: "tungi", timeZone: "Asia/Tashkent" })
tungi() {}

@Cron("0 30 3 * * *", { name: "tungi-utc" })
tungiUtc() {}
text
   tungi (Asia/Tashkent) keyingi: 2026-11-02T22:30:00.000Z
   tungi (TZ yo'q) keyingi:      2026-11-03T03:30:00.000Z
   jarayon TZ: UTC

Birinchi jadval 22:30Z da ishlaydi — bu Toshkentda ertalab 03:30 (UTC+5). Ikkinchisi 03:30Z da — Toshkentda 08:30. Ya'ni "tungi tozalash" ish kuni boshlanganda, foydalanuvchilar eng ko'p bo'lgan paytda ishlaydi.

Shu jarayonni TZ=Asia/Tashkent bilan ishga tushirganimizda, ikkala qator ham 22:30:00.000Z chiqdi. Xato sizning kompyuteringizda ko'rinmaydi, faqat serverda chiqadi. Shuning uchun qoida: kun vaqtiga bog'liq har bir @Cron ga timeZone yozing.

nextDate() va nextDates(n) — jadval qachon ishlashini oldindan ko'rsatadi. Ular Luxon kutubxonasining DateTime obyektini qaytaradi (cron ichida ishlatiladi):

text
keyingi 3 marta: [
  '2026-11-02T22:30:00.000Z',
  '2026-11-03T22:30:00.000Z',
  '2026-11-04T22:30:00.000Z'
]

Bunday ro'yxatni testda tekshirish qulay: "jadval haqiqatan Toshkent vaqti bilan ishlaydimi?" savoliga 3 qatorlik javob.

Tekshirib ko'ring: @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) UTC'dagi serverda Toshkent vaqti bilan soat nechada ishlaydi?

Javob

05:00 da. EVERY_DAY_AT_MIDNIGHT — 0 0 * * *, ya'ni 00:00. timeZone berilmagani uchun jarayon zonasi — UTC — ishlatiladi. UTC 00:00 = Toshkentda 05:00. Toshkent yarim tuni kerak bo'lsa: { timeZone: "Asia/Tashkent" }.

4. SchedulerRegistry — ishlar ro'yxati

4.1 Nomi bilan topish va o'chirish

SchedulerRegistry — oddiy provider. Uni istalgan xizmatga constructor orqali olasiz. U uch ro'yxatni saqlaydi: cron ishlari, intervallar va timeout'lar. Biz yuqoridagi demoda ro'yxatni chiqargan edik: intervals: tozalash, xatoli, sekin. Endi o'chirib ko'ramiz:

ts
const r = app.get(SchedulerRegistry);
r.deleteInterval("tozalash");
// 300 ms kutamiz — sanagich o'zgardimi?
text
   deleteInterval dan keyin o'zgardimi: false

deleteInterval ichida clearInterval chaqiriladi — ish to'xtadi. Mavjud bo'lmagan nom so'ralsa, registr xato tashlaydi:

text
   getCronJob("yoq"): No Cron Job was found with the given name (yoq). Check that you created one with a decorator or with the create API.

Tarjimasi: "berilgan nom (yoq) bilan cron ishi topilmadi. Uni dekorator yoki yaratish API'si bilan yaratganingizni tekshiring". Xabardagi "create API" — keyingi bo'limdagi addCronJob, addInterval metodlari.

4.2 Dinamik ishlar

Ba'zan jadval kod yozilganda emas, ish vaqtida ma'lum bo'ladi: masalan, foydalanuvchi "har kuni 20:00 da eslat" deb sozladi. Bunday ishni registrga qo'lda qo'shasiz:

ts
import { CronJob } from "cron";

const ish = new CronJob("* * * * * *", () => { n++; });
r.addCronJob("hisobot", ish);
ish.start();
// 2.1 s kutamiz
ish.stop();
r.deleteCronJob("hisobot");
text
2.1 s da: 2 | isActive: false
cronJobs: []

Ikki narsaga e'tibor bering. addCronJob ishni faqat ro'yxatga qo'yadi — start() ni o'zingiz chaqirasiz. deleteCronJob esa ishni to'xtatadi va ro'yxatdan o'chiradi. Interval uchun ham xuddi shunday: r.addInterval("nom", setInterval(fn, 50)) — demoda 275 ms da 4 marta ishladi va deleteInterval dan keyin ro'yxat bo'sh qoldi.

Diqqat: Foydalanuvchi sozlagan jadval ilova xotirasida yashaydi. Server qayta ishga tushsa — yo'qoladi. Bunday jadvallarni bazada saqlang va ilova ishga tushganda qayta yarating. Yoki BullMQ job scheduler ga bering — u jadvalni Redis'da saqlaydi.

5. Tuzoqlar — haqiqiy chiqish bilan

5.1 Xato handler ichida

Demodagi xatoli ishi har 300 ms da xato tashlaydi:

ts
@Interval("xatoli", 300)
xatoli() {
  bir("xatoli");
  throw new Error("ombor javob bermadi");
}

Konsolda (izning at … qatorlarini olib tashladik):

text
ERROR [Scheduler] Error: ombor javob bermadi
ERROR [Scheduler] Error: ombor javob bermadi
ERROR [Scheduler] Error: ombor javob bermadi

Bir soniyada uch marta — ya'ni xatodan keyin ish to'xtamadi. @nestjs/schedule har metodni try/catch bilan o'raydi va xatoni Nest logger'iga yozadi. Ilova yiqilmaydi — bu yaxshi. Lekin xato faqat logda qoladi: hech kim javob olmaydi, HTTP xato filtri ham ishlamaydi. Monitoring logni kuzatmasa, tozalash haftalab ishlamay yotishi mumkin.

Amaliy qoida: rejali ish ichidagi muhim xatoni o'zingiz ushlang va aniq log yozing (nima, qaysi yozuv, nechta urinish). async metodda ham shunday — Nest await qiladi va rad etilgan Promise ni ham logga yozadi.

5.2 Ustma-ust ishga tushish

@Interval ichida oddiy setInterval. U oldingi chaqiruv tugashini kutmaydi. Demoda 100 ms li interval 250 ms ishlaydigan async metodni chaqirdi:

ts
@Interval("sekin", 100)
async sekin() {
  this.faol++;
  this.maksFaol = Math.max(this.maksFaol, this.faol);
  await kut(250);
  this.faol--;
}
text
   sekin interval bir vaqtda (maks): 3

Bir vaqtda uchta nusxa ishlagan. Bu bazaga ketma-ket 3 ta og'ir so'rov degani. Ish sekinlashgan sari ular ko'payadi va baza yanada sekinlashadi.

@Cron da himoya bor — waitForCompletion: true. Bayroq yoqilsa, oldingi chaqiruv tugamaguncha yangisi o'tkazib yuboriladi. Ikki cron'ni solishtirdik — ikkalasi har soniyada, ish 2.5 soniya davom etadi:

ts
@Cron(CronExpression.EVERY_SECOND, { name: "oddiy" })
async a() { oddiy++; await kut(2500); }

@Cron(CronExpression.EVERY_SECOND, {
  name: "kutuvchi", waitForCompletion: true,
})
async b() { kutuvchi++; await kut(2500); }
text
4) 2.5 s lik ish, har soniya, ~3 s: oddiy 3 marta, waitForCompletion 1 marta

@Interval da bunday bayroq yo'q. Uzoq davom etishi mumkin bo'lgan ishni @Cron + waitForCompletion bilan yozing yoki metod boshida o'zingiz "band" bayrog'ini tekshiring.

5.3 Bir xil nom — ilova ishga tushmaydi

Ikki xizmatga bir xil nomli interval qo'ydik:

ts
@Injectable()
class A { @Interval("tozalash", 1000) bir() {} }

@Injectable()
class B { @Interval("tozalash", 1000) ikki() {} }
text
Error: Interval with the given name (tozalash) already exists. Ignored.

Tarjimasi: "berilgan nomli (tozalash) interval allaqachon bor. E'tiborsiz qoldirildi". Lekin xabarga ishonmang: "Ignored" deyilgan bo'lsa ham, xato app.init() ichida tashlandi va jarayon yiqildi. Ilova umuman ishga tushmadi. Nomlarni modul nomi bilan boshlang (eksport-tozalash, sessiya-tozalash) — to'qnashuv ehtimoli kamayadi. Loyihada aynan shunday nom tanladik.

5.4 Ikki nusxa

Eng katta tuzoq. Ilova ikkita jarayonda ishlasa (yuk ko'p, yoki load balancer orqasida), har jarayon o'z ScheduleModule iga ega. Biz bitta jarayonda ikkita Nest ilovasini yaratdik — bu ikki server nusxasining modeli:

ts
const a = await NestFactory.createApplicationContext(HisobotModule);
const b = await NestFactory.createApplicationContext(HisobotModule);
// ikkalasida ham @Cron(EVERY_SECOND) hisobot++
text
2) ikki nusxa, ~2 s: hisobot 4 marta (kutilgani 2)

Har soniyada hisobot ikki marta yuborildi. Kunlik email bo'lsa — foydalanuvchi ikkita xat oladi. To'lov eslatmasi bo'lsa — ikki marta SMS (pul!). @nestjs/schedule buni hal qilmaydi va hal qilishga urinmaydi: u bitta jarayon ichidagi taymer.

Yechimlarni Cron darsida o'lchagandik: Postgres advisory lock yoki BullMQ job scheduler (jadval Redis'da bitta, har tikni bitta ishchi oladi). Nest'da ham o'sha yechimlar ishlaydi — @Cron metodi ichida avval qulf olinadi, olinmasa — chiqib ketiladi.

Lekin har rejali ish ham qulf talab qilmaydi. Savol bering: "bu ish umumiy ma'lumot bilan ishlaydimi yoki shu nusxaning xotirasi bilanmi?". Eksport natijalari har nusxaning o'z Map ida yashaydi. Demak har nusxa o'z xotirasini o'zi tozalashi kerak — bu yerda ikki nusxa muammo emas, aksincha to'g'ri xulq.

flowchart TB
  S["Rejali ish"] --> Q{"Qaysi ma'lumot?"}
  Q -->|"nusxa xotirasi"| L["@Interval / @Cron yetarli"]
  Q -->|"umumiy: baza, email"| U{"Bitta bajarilsinmi?"}
  U -->|"ha"| B["qulf yoki BullMQ scheduler"]
  U -->|"yo'q, takror zararsiz"| L

Diagrammani yuqoridan o'qing. Loyihadagi eksport tozalash chap tarmoqqa tushadi. Kunlik email yoki statistika — o'ng tarmoqqa.

Tekshirib ko'ring: Server 3 nusxada ishlayapti. Har kuni 03:30 da bazadagi 30 kundan eski loglarni DELETE bilan o'chiradigan @Cron bor. Bu zararmi?

Javob

Ma'lumot uchun deyarli zararsiz: birinchi DELETE eski qatorlarni o'chiradi, qolgan ikkitasi 0 qatorni o'chiradi. Lekin baza uch marta bir xil og'ir ishni bajaradi va qatorlarga qulf qo'yiladi. Takror bajarish xavfsiz (idempotent) bo'lsa ham, uni bitta nusxaga berish yaxshiroq. Email yoki to'lov kabi takrorlanishi zararli ishda esa qulf majburiy.

6. JS modelida rejalovchi

Nest'ning rejalovchisi ichida nima bo'layotganini kichik modelda ko'raylik. Haqiqiy vaqt o'rniga soxta soat ishlatamiz — har chaqiruvda 100 ms oldinga yuradi. Shunda natija har safar bir xil chiqadi. Blok brauzerda ishlaydi:

js
class Rejalovchi {
  #vaqt = 0; // soxta soat, ms
  #ishlar = new Map(); // nom → ish

  qosh(nom, har, fn, { kutsin = false } = {}) {
    if (this.#ishlar.has(nom)) {
      throw new Error(`${nom} nomli ish allaqachon bor`);
    }
    this.#ishlar.set(nom, {
      har, keyingi: har, fn, kutsin, bandGacha: 0,
    });
  }

  ochir(nom) {
    this.#ishlar.delete(nom);
  }

  oldinga(ms) {
    const oxiri = this.#vaqt + ms;
    while (this.#vaqt < oxiri) {
      this.#vaqt += 100; // soat 100 ms qadam bilan yuradi
      for (const [nom, ish] of this.#ishlar) {
        if (this.#vaqt < ish.keyingi) continue;
        ish.keyingi += ish.har;
        const t = this.#vaqt;
        if (ish.kutsin && t < ish.bandGacha) {
          console.log(`${t}: ${nom} — o'tkazildi, hali band`);
          continue;
        }
        try {
          ish.bandGacha = t + (ish.fn(t) ?? 0);
        } catch (e) {
          console.log(`${t}: ERROR [${nom}] ${e.message}`);
        }
      }
    }
  }
}

const r = new Rejalovchi();
r.qosh("hisobot", 200, (t) => {
  console.log(`${t}: hisobot boshlandi (300 ms davom etadi)`);
  return 300;
}, { kutsin: true });
r.qosh("xatoli", 400, () => {
  throw new Error("ombor javob bermadi");
});
r.oldinga(800);
r.ochir("hisobot");
r.oldinga(400);
console.log("tugadi");

Konsolda:

text
200: hisobot boshlandi (300 ms davom etadi)
400: hisobot — o'tkazildi, hali band
400: ERROR [xatoli] ombor javob bermadi
600: hisobot boshlandi (300 ms davom etadi)
800: hisobot — o'tkazildi, hali band
800: ERROR [xatoli] ombor javob bermadi
1200: ERROR [xatoli] ombor javob bermadi
tugadi

Modelda Nest'ning to'rt xulqi bor:

  1. Nomlar ro'yxati — #ishlar Map i SchedulerRegistry ning o'rnida; bir xil nom — xato (Nest'da ham).
  2. kutsin — waitForCompletion ning o'zi: 400 va 800 da hisobot hali band, shuning uchun o'tkazildi.
  3. try/catch — xato logga yoziladi va ish davom etadi (400, 800, 1200).
  4. ochir — deleteInterval: 1000 dan keyin hisobot chiqmadi.

Soddalashtirilgan joyi: haqiqiy rejalovchida fn asinxron, "band" holati await tugaguncha davom etadi. Modelda esa fn davomiylikni o'zi qaytaradi.

7. Express'da qanday edi → Nest'da qanday

Express (29-qism) Nest
Jadval yaratish new Cron(...) qo'lda @Cron / @Interval dekorator
To'xtatish yopish funksiyasida qo'lda app.close() o'zi
Ro'yxat o'z Map ingiz SchedulerRegistry
Xato o'zingiz ushlaysiz try/catch + Nest log

Nima yutdik: yopishni unutib bo'lmaydi, ishlar registrda nomi bilan turadi, xizmat metodining ustida jadvali yozilgan — kod o'qiganda darrov ko'rinadi. Nima yo'qotdik: bitta paket qo'shildi (prod bog'liqliklar 19 tadan 20 taga) va jadval faqat Nest konteksti ichida ishlaydi.

Kanon qarori. Loyihada faqat bitta rejali ish bor — eksport tozalash, @Interval. Statistika materialized view'ini Toshkent vaqti bilan har kecha yangilaydigan @Cron kanonga kirmadi. Sabab — arxitektura: xizmat bazani VazifaOmbori interfeysi orqali ko'radi va unda ulanish hovuzi (pool) yo'q. MV yangilash — Postgres'ga xos ish, uni interfeys orqali o'tkazish abstraksiyani buzadi. 29-qismdagi rejali ishlar esa ishchi jarayonida, BullMQ job scheduler bilan ishlaydi.

8. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
ScheduleModule.forRoot() unutildi dekoratorlar jim ishlamaydi ildiz modulga qo'shing
timeZone yo'q serverda boshqa soatda ishlaydi timeZone: "Asia/Tashkent"
Uzoq ish @Interval da bir vaqtda bir necha nusxa @Cron + waitForCompletion
Bir xil nom already exists. Ignored. va ilova yiqiladi modul nomi bilan nomlang
Ikki server nusxasi email/SMS ikki marta qulf yoki BullMQ scheduler
Testda app.close() yo'q test runner tugamaydi afterAll da yoping

Yana biri: rejali metod singleton provider'da bo'lishi kerak. @nestjs/schedule dekoratorlarni qidirganda provider'ning isDependencyTreeStatic() ini tekshiradi. Request scope'li provider (Provider scope'lari) uchun jadval ro'yxatga olinmaydi — taymer yaratadigan "so'rov" yo'q.

9. Mashqlar

Mashqlar kurs/mashqlar/30/39-rejalashtirish/ da.

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

  • Bir marta, ishga tushgandan keyin ishlaydigan dekorator —
  • 0 30 3 * * * ifodasidagi birinchi maydon nimani bildiradi? —
  • Oldingi chaqiruv tugamaguncha yangisini o'tkazib yuboradigan @Cron bayrog'i —
  • Ishlarni nomi bo'yicha topish va o'chirish uchun provider —
  • Demoda ikki nusxa ~2 soniyada hisobotni necha marta yubordi? —
Yechim
  • @Timeout — @Timeout("isinish", 250); @Interval — takroriy, @Cron — jadval bo'yicha.
  • soniya — olti maydonli ifodada birinchi maydon soniya; besh maydonlida — daqiqa.
  • waitForCompletion — faqat @Cron da bor; @Interval da yo'q.
  • SchedulerRegistry — getIntervals(), getCronJob(nom), deleteInterval(nom).
  • 4 — har nusxa o'z cron'ini yurgizdi: 2 soniya × 2 nusxa.

2-mashq (o'rta): bir martalik ish

Darsdagi Rejalovchi ga @Timeout ning o'xshashini qo'shing: qosh(nom, har, fn, { birMarta: true }) — ish bir marta bajarilib, ro'yxatdan o'zi o'chsin. nomlar() metodi ham kerak — u Map kalitlarini massiv qilib qaytarsin. Ishora: [...this.#ishlar.keys()] (Map); for...of ichida Map dan joriy kalitni o'chirish xavfsiz. Soddalik uchun kutsin va try/catch ni olib tashlang.

Yechim
js
class Rejalovchi {
  #vaqt = 0;
  #ishlar = new Map();

  qosh(nom, har, fn, { birMarta = false } = {}) {
    if (this.#ishlar.has(nom)) {
      throw new Error(`${nom} nomli ish allaqachon bor`);
    }
    this.#ishlar.set(nom, { har, keyingi: har, fn, birMarta });
  }

  nomlar() {
    return [...this.#ishlar.keys()];
  }

  oldinga(ms) {
    const oxiri = this.#vaqt + ms;
    while (this.#vaqt < oxiri) {
      this.#vaqt += 100;
      for (const [nom, ish] of this.#ishlar) {
        if (this.#vaqt < ish.keyingi) continue;
        ish.keyingi += ish.har;
        ish.fn(this.#vaqt);
        if (ish.birMarta) this.#ishlar.delete(nom);
      }
    }
  }
}

const r = new Rejalovchi();
r.qosh("tozalash", 200, (t) => console.log(`${t}: tozalash`));
r.qosh("isinish", 300, (t) => console.log(`${t}: kesh isitildi`), {
  birMarta: true,
});
console.log(r.nomlar());
r.oldinga(600);
console.log(r.nomlar());

Konsolda:

text
[ 'tozalash', 'isinish' ]
200: tozalash
300: kesh isitildi
400: tozalash
600: tozalash
[ 'tozalash' ]

isinish 300 da bir marta ishladi va ro'yxatdan chiqdi. Bu yerda modelimiz Nest'dan farq qiladi. Demoda @Timeout("isinish", 250) bajarilganidan keyin ham getTimeouts() uni ko'rsatdi (timeouts: isinish qatori 1 soniyadan keyin chiqqan). Nest'da nom registrda qoladi, faqat taymer tugaydi.

3-mashq (qiyin): ikki nusxa va umumiy qulf

Ikki server nusxasini modellang: har biri har 1000 ms da tik(t) oladi va "hisobot" ishini bajarmoqchi bo'ladi. Ular umumiy Qulf obyektini ko'radi (Redis yoki Postgres o'rnida). ol(kalit, kim, t, ttl) — qulf bo'sh yoki muddati o'tgan bo'lsa, uni kim ga ttl ms ga beradi va true qaytaradi; aks holda false. 1000 dan 5000 gacha 5 tikda ikki nusxa jami 5 marta bajarsin (10 emas). Ishora: qulf Map da { kim, tugash } saqlaydi; TTL 500 ms — keyingi tikkacha muddati o'tadi. Kim oldin uyg'onishini navbatma-navbat qiling.

Yechim
js
// Umumiy qulf: ikki nusxa bitta "Redis"ni ko'radi
class Qulf {
  #egalar = new Map(); // kalit → { kim, tugash }
  ol(kalit, kim, t, ttl) {
    const joriy = this.#egalar.get(kalit);
    if (joriy && joriy.tugash > t && joriy.kim !== kim) return false;
    this.#egalar.set(kalit, { kim, tugash: t + ttl });
    return true;
  }
}

class Nusxa {
  constructor(nom, qulf) {
    this.nom = nom;
    this.qulf = qulf;
    this.bajardi = 0;
  }
  tik(t) {
    // har nusxa o'z taymeri bilan keladi, lekin qulfni bittasi oladi
    if (this.qulf.ol("hisobot", this.nom, t, 500)) this.bajardi++;
  }
}

const qulf = new Qulf();
const a = new Nusxa("A", qulf);
const b = new Nusxa("B", qulf);
for (let t = 1000; t <= 5000; t += 1000) {
  // kim oldin uyg'onishi — tasodif; bu yerda navbatma-navbat
  const tartib = t % 2000 === 0 ? [a, b] : [b, a];
  for (const n of tartib) n.tik(t);
}
console.log(`A: ${a.bajardi}, B: ${b.bajardi}`);
console.log(`jami: ${a.bajardi + b.bajardi} (5 tik)`);

Konsolda:

text
A: 2, B: 3
jami: 5 (5 tik)

Har tikda qulfni birinchi kelgan oladi, ikkinchisi false oladi va chiqib ketadi. TTL muhim: u ish davomiyligidan uzun, lekin jadval oralig'idan qisqa bo'lsin. Haqiqiy Redis'da bu SET kalit kim NX PX 500 buyrug'i — tekshirish va yozish bitta atomar amalda. Modelimizda JS bitta oqimda ishlagani uchun "atomar" o'z-o'zidan; ikki jarayonda esa aynan atomarlik qulfning butun ma'nosi.

10. Loyiha qadami: eksport tozalash @Interval ga

10.1 Paket va modul

vazifalar-api papkasida (C:/Users/…/vazifalar-api):

bash
npm i --save-exact @nestjs/schedule@12.0.2

src/app.module.ts — importlar orasiga:

ts
import { ScheduleModule } from "@nestjs/schedule";

imports massivida ClsModule.forRoot(...) dan keyin:

ts
        // @Interval, @Cron, @Timeout dekoratorlarini yoqadi
        ScheduleModule.forRoot(),

10.2 EksportService

src/eksport/eksport.service.ts — importlarga import { Interval } from "@nestjs/schedule"; qo'shing. Eski topilsin ni olib tashlab, o'rniga ikki metod yozing:

ts
  /**
   * Muddati o'tgan ishlar har daqiqada o'chiriladi (avval — har
   * topilsin() chaqiruvida butun Map aylanardi). Bitta jarayon
   * ichida: N nusxa (instans) bo'lsa, har biri o'z xotirasini
   * tozalaydi — ishlarning o'zi ham nusxaga bog'liq (umumiy navbat
   * — 29-qism, BullMQ)
   */
  @Interval("eksport-tozalash", 60_000)
  tozala(): number {
    let n = 0;
    for (const [k, i] of this.#ishlar) {
      if (i.tugash <= this.soat()) {
        this.#ishlar.delete(k);
        n++;
      }
    }
    return n;
  }

  topilsin(id: string): Ish {
    const ish = this.#ishlar.get(id);
    // interval kelgunicha muddati o'tgan ish ham ko'rinmasin
    if (ish === undefined || ish.tugash <= this.soat()) {
      throw new IlovaXatosi(
        404,
        `Eksport ${id} topilmadi (yoki muddati o'tgan)`,
        { kod: "eksport-topilmadi", sarlavha: "Eksport topilmadi" },
      );
    }
    return ish;
  }

Ikki joyga qarang:

  • tozala() o'chirilganlar sonini qaytaradi. Nest bu qiymatni ishlatmaydi, lekin test uchun qulay: "bitta ish o'chdimi?" deb tekshiramiz.
  • topilsin da muddat ham tekshiriladi. Interval har daqiqada ishlaydi. Muddat 12:00:05 da tugasa va interval 12:01:00 da kelsa, orada 55 soniya bor. Bu vaqtda eskirgan ish ko'rinmasligi kerak — shuning uchun ish.tugash <= this.soat() sharti.

this.soat — xizmatdagi tikuv (seam): sukut bo'yicha Date.now, testda esa soxta soat beriladi. Shu sababli test 2 soat kutmaydi.

10.3 Test

Yangi fayl test/hodisa.test.ts. Keyingi ikki darsda unga yana testlar qo'shamiz — nomi shundan:

ts
// hodisa.test.ts — rejalashtirilgan tozalash (@Interval)
import { afterAll, beforeAll, expect, test } from "vitest";
import { SchedulerRegistry } from "@nestjs/schedule";
import { ishgaTushir as ishga } from "../src/ishga-tushir.ts";
import type { Ishlayotgan } from "../src/ishga-tushir.ts";
import { yop } from "../src/yopish.ts";
import { XotiraOmbori } from "../src/vazifalar/xotira-ombor.ts";
import { EksportService } from "../src/eksport/eksport.service.ts";

let i: Ishlayotgan;
beforeAll(async () => {
  i = await ishga({
    port: 0,
    bogliqliklar: { ombor: new XotiraOmbori() },
  });
});
afterAll(async () => {
  await yop(i.server, 1000);
  await i.app.close(); // @Interval taymerlari to'xtasin
});

test("@Interval: eksport-tozalash ro'yxatda; muddati o'tgan ish "
  + "tozalanadi va 404", async () => {
  const reestr = i.app.get(SchedulerRegistry);
  expect(reestr.getIntervals()).toContain("eksport-tozalash");
  const eksport = i.app.get(EksportService);
  const r = await fetch(`${i.manzil}/v1/eksportlar`, {
    method: "POST",
  });
  const joy = String(r.headers.get("location"));
  expect((await fetch(i.manzil + joy)).status).toBe(200);
  const asl = eksport.soat;
  // 2 soat o'tdi (natija 1 soat saqlanadi)
  eksport.soat = () => asl() + 2 * 60 * 60 * 1000;
  expect((await fetch(i.manzil + joy)).status).toBe(404);
  expect(eksport.tozala()).toBe(1);
  eksport.soat = asl;
});

Test interval 60 soniya o'tishini kutmaydi. U uch narsani tekshiradi: interval registrda bor (dekorator ishladi), muddati o'tgan ish darhol 404 beradi (topilsin sharti) va tozala() uni o'chiradi. Taymerning o'zini — "60 soniyada bir chaqiriladimi" — Nest'ning o'z testlari tekshiradi, biz o'z mantiqimizni tekshiramiz.

Kanon testida test nomi bitta qatorda; biz uni telefon uchun + bilan bo'ldik. Faylning qolgan qismi ham faqat format jihatidan bo'lingan, ma'nosi aynan.

10.4 Tekshiruv

bash
npm run tip
npm run lint
npm test

Biz bu holatni kanon nusxasida tekshirdik (Node 24.21.0, bazasiz — Postgres testlari SKIP):

text
> tsc --noEmit
> eslint . --max-warnings=0
 Test Files  20 passed | 5 skipped (25)
      Tests  78 passed | 6 skipped (84)

Bitta test qo'shildi (77 dan 78 ga). Postgres bilan ishga tushirsangiz, SKIP qilinganlar ham o'tadi. Hali commit qilmang — keyingi uch darsda domen hodisalari, salomatlik va SSE qo'shiladi, commit Server-Sent Events NestJS'da darsida.

11. Real ishda

  • Qayerda uchraydi: eskirgan sessiya va tokenlarni tozalash, kunlik hisobot, kesh isitish, tashqi API'dan valyuta kursini soatda bir olish. Kichik loyihalarda @nestjs/schedule yetarli.
  • Masshtab: server ikki nusxaga chiqqanda jamoalar rejali ishlarni alohida "worker" jarayoniga yoki BullMQ'ga ko'chiradi. Buni oldindan bilish — keyin "nega har kuni ikkita xat keladi?" muammosidan saqlaydi.
  • Intervyu savollari: "@Cron ko'p nusxada qanday ishlaydi?", "Vaqt zonasini qanday berasiz?", "Uzoq ish ustma-ust ishga tushmasligi uchun nima qilasiz?".
  • Kod review'da: timeZone siz @Cron("0 9 * * *") — eng ko'p uchraydigan izoh.

Xulosa

  • ScheduleModule.forRoot() + @Interval, @Timeout, @Cron — rejali ishlar xizmat metodida, dekorator bilan.
  • Taymerlar onApplicationBootstrap da yonadi, app.close() da o'chadi.
  • Nest cron'ida oltinchi maydon — soniya (boshida); kun vaqtiga bog'liq har jadvalga timeZone.
  • SchedulerRegistry — ishlar ro'yxati: topish, qo'shish, o'chirish; bir xil nom ilovani yiqitadi.
  • Xato logga yoziladi va ish davom etadi; @Interval ustma-ust ishlaydi, @Cron da waitForCompletion bor.
  • Har nusxa o'z jadvalini yurgizadi: umumiy ish uchun qulf yoki BullMQ, nusxa xotirasi uchun — oddiy @Interval.

Keyingi dars: Hodisalar: @nestjs/event-emitter — xizmat webhook'ni to'g'ridan chaqirmay, "vazifa bajarildi" hodisasini chiqaradi; tinglovchilar esa alohida klasslarda yashaydi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Rejalashtirish NestJS'da: @nestjs/schedule — @Cron, @Interval, @Timeout — IlmHamroh