Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. ScheduleModule va uch dekorator
- 2.1 O'rnatish
- 2.2 Uch dekorator
- 2.3 Taymerlar qachon yonadi va o'chadi
- 3. Cron ifodasi Nest'da
- 3.1 Oltinchi maydon — soniya
- 3.2 Vaqt zonasi
- 4. SchedulerRegistry — ishlar ro'yxati
- 4.1 Nomi bilan topish va o'chirish
- 4.2 Dinamik ishlar
- 5. Tuzoqlar — haqiqiy chiqish bilan
- 5.1 Xato handler ichida
- 5.2 Ustma-ust ishga tushish
- 5.3 Bir xil nom — ilova ishga tushmaydi
- 5.4 Ikki nusxa
- 6. JS modelida rejalovchi
- 7. Express'da qanday edi → Nest'da qanday
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): bo'shliqlarni to'ldiring
- 2-mashq (o'rta): bir martalik ish
- 3-mashq (qiyin): ikki nusxa va umumiy qulf
- 10. Loyiha qadami: eksport tozalash @Interval ga
- 10.1 Paket va modul
- 10.2 EksportService
- 10.3 Test
- 10.4 Tekshiruv
- 11. Real ishda
- Xulosa
- Manbalar
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 vaapp.close()da o'chiradi. Uchta tuzoq bor: vaqt zonasi, ustma-ust ishga tushish va ikki nusxa — har nusxa o'z jadvalini yurgizadi.
Bu darsda
ScheduleModuleni ulab,@Interval,@Timeoutva@Cronbilan rejali ish yozasiz.- Nest cron ifodasidagi oltinchi maydon (soniya) va
CronExpressionro'yxatini ishlatasiz. SchedulerRegistrybilan ishlarni nomi bo'yicha topasiz, qo'shasiz va o'chirasiz.- Xato, ustma-ust ishga tushish va ikki nusxa tuzoqlarini haqiqiy chiqishda ko'rasiz.
vazifalar-apidagi eksport tozalashni@Intervalga 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:
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
npm i --save-exact @nestjs/schedule@12.0.2Paket 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.
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:
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):
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:
onModuleInitichida jadval hali ishlamaydi — taymerlar undan keyin yonadi.- Testda
app.close()ni unutsangiz,setIntervaljarayonni tirik ushlab turadi va test runner tugamaydi. Shuning uchun loyiha testidaafterAlldaapp.close()bor.
Tekshirib ko'ring:
@Timeout("isinish", 250)vasetTimeout(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):
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):
@Cron("0 30 3 * * *", { name: "tungi", timeZone: "Asia/Tashkent" })
tungi() {}
@Cron("0 30 3 * * *", { name: "tungi-utc" })
tungiUtc() {} tungi (Asia/Tashkent) keyingi: 2026-11-02T22:30:00.000Z
tungi (TZ yo'q) keyingi: 2026-11-03T03:30:00.000Z
jarayon TZ: UTCBirinchi 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):
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:
const r = app.get(SchedulerRegistry);
r.deleteInterval("tozalash");
// 300 ms kutamiz — sanagich o'zgardimi? deleteInterval dan keyin o'zgardimi: falsedeleteInterval ichida clearInterval chaqiriladi — ish to'xtadi. Mavjud bo'lmagan nom so'ralsa, registr xato tashlaydi:
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:
import { CronJob } from "cron";
const ish = new CronJob("* * * * * *", () => { n++; });
r.addCronJob("hisobot", ish);
ish.start();
// 2.1 s kutamiz
ish.stop();
r.deleteCronJob("hisobot");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:
@Interval("xatoli", 300)
xatoli() {
bir("xatoli");
throw new Error("ombor javob bermadi");
}Konsolda (izning at … qatorlarini olib tashladik):
ERROR [Scheduler] Error: ombor javob bermadi
ERROR [Scheduler] Error: ombor javob bermadi
ERROR [Scheduler] Error: ombor javob bermadiBir 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:
@Interval("sekin", 100)
async sekin() {
this.faol++;
this.maksFaol = Math.max(this.maksFaol, this.faol);
await kut(250);
this.faol--;
} sekin interval bir vaqtda (maks): 3Bir 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:
@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); }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:
@Injectable()
class A { @Interval("tozalash", 1000) bir() {} }
@Injectable()
class B { @Interval("tozalash", 1000) ikki() {} }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:
const a = await NestFactory.createApplicationContext(HisobotModule);
const b = await NestFactory.createApplicationContext(HisobotModule);
// ikkalasida ham @Cron(EVERY_SECOND) hisobot++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"| LDiagrammani 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
DELETEbilan o'chiradigan@Cronbor. 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:
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:
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
tugadiModelda Nest'ning to'rt xulqi bor:
- Nomlar ro'yxati —
#ishlarMapiSchedulerRegistryning o'rnida; bir xil nom — xato (Nest'da ham). kutsin—waitForCompletionning o'zi: 400 va 800 da hisobot hali band, shuning uchun o'tkazildi.try/catch— xato logga yoziladi va ish davom etadi (400, 800, 1200).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
@Cronbayrog'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
@Cronda bor;@Intervalda 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
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:
[ '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
// 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:
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):
npm i --save-exact @nestjs/schedule@12.0.2src/app.module.ts — importlar orasiga:
import { ScheduleModule } from "@nestjs/schedule";imports massivida ClsModule.forRoot(...) dan keyin:
// @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:
/**
* 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.topilsinda 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 uchunish.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:
// 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
npm run tip
npm run lint
npm testBiz bu holatni kanon nusxasida tekshirdik (Node 24.21.0, bazasiz — Postgres testlari SKIP):
> 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/scheduleyetarli. - 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: "
@Cronko'p nusxada qanday ishlaydi?", "Vaqt zonasini qanday berasiz?", "Uzoq ish ustma-ust ishga tushmasligi uchun nima qilasiz?". - Kod review'da:
timeZonesiz@Cron("0 9 * * *")— eng ko'p uchraydigan izoh.
Xulosa
ScheduleModule.forRoot()+@Interval,@Timeout,@Cron— rejali ishlar xizmat metodida, dekorator bilan.- Taymerlar
onApplicationBootstrapda 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;
@Intervalustma-ust ishlaydi,@CrondawaitForCompletionbor. - 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
- NestJS hujjati — Task Scheduling: https://docs.nestjs.com/techniques/task-scheduling
cronkutubxonasi (CronJob, timeZone, waitForCompletion): https://github.com/kelektiv/node-cron@nestjs/schedulemanba kodi (12.0.2): https://github.com/nestjs/schedule- IANA vaqt zonalari bazasi: https://www.iana.org/time-zones
- Kanon (IlmHamroh, 2026-10-28): eksport tozalash
@Interval, NestJS 12.2.0,@nestjs/schedule12.0.2. Demo loyihadagi dekoratorlar, vaqt zonasi, registr, xato, ustma-ust, bir xil nom va ikki nusxa tajribalari va loyiha nusxasidagi tekshiruv — biz ishga tushirdik (2026-11-02, Node 24.21.0, NestJS 12.2.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!