Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 1.1 Express'da qanday edi
- 2. Besh hook
- 2.1 Jadval
- 2.2 Kanon izi: tartib
- 2.3 JS modelida tartib
- 3. Bajarilayotgan so'rov va app.close()
- 3.1 Kanon o'lchovi
- 3.2 Yechim — 25-qismdagi yop()
- 3.3 Kanon qarori
- 3.4 Qolgan hook'lar nimaga kerak
- 4. enableShutdownHooks()
- 5. abortOnError: fabrika yiqilsa
- 5.1 Sukut xulq
- 5.2 abortOnError: false
- 6. Ko'p uchraydigan xatolar
- 7. Mashqlar
- 1-mashq (oson): qaysi hook?
- 2-mashq (o'rta): idempotent yopuvchi
- 3-mashq (qiyin): onModuleInit da tekshiruv
- 8. Loyiha qadami: ombor Nest ichida yaratiladi va yopiladi
- 8.1 Reja
- 8.2 ombor.module.ts: SaqlashYopuvchi
- 8.3 ilova.ts va main.ts
- 8.4 Test: fabrika xatosi va pool yopilishi
- 8.5 Tekshiruv: avval va keyin
- 9. Real ishda
- Xulosa
- Manbalar
NestJS lifecycle hook'lar: onModuleInit, onApplicationShutdown va to'g'ri yopish
Qisqacha: Lifecycle hook (hayot sikli ilgagi) — Nest provider'ning o'zi chaqiradigan maxsus metod. Ishga tushishda:
onModuleInit(modul tayyor),onApplicationBootstrap(hamma modul tayyor).app.close()da:onModuleDestroy→beforeApplicationShutdown→ HTTP server yopiladi →onApplicationShutdown. Async hook'lar navbat bilan kutiladi. Resurslarni (baza pool'i, Redis) yopish uchun eng to'g'ri joy —onApplicationShutdown: boshlangan so'rovlar tugagandan keyin.enableShutdownHooks()Nest'ga signallarni tinglatadi;vazifalar-apibuni ishlatmaydi — signal va drenaj 25-qismdagi o'z kodimizda.
Bu darsda
- Besh hook'ning tartibini va har biri qachon chaqirilishini kanon izida ko'rasiz.
app.close()bajarilayotgan so'rov bor paytda nega 3 sekund kutganini tushuntirasiz.enableShutdownHooks()nima qilishini va nega kanonda ishlatilmasligini bilasiz.abortOnErrorsozlamasi testni qanday "o'ldirishini" haqiqiy chiqishda ko'rasiz.vazifalar-apida ombor ulanishlarini Nest yopadigan qilasiz vamain.tsfabrika yo'liga o'tadi.
Oldin bilishingiz kerak: Dynamic modullar, Bootstrap: main.ts, Server timeout'lari va keep-alive, Process darajasidagi xatolar va crash strategiyasi, Connection pooling.
O'tgan darsda OmborModule ikki yo'lli bo'ldi, lekin main.ts hali omborni o'zi yaratadi va o'zi yopadi. Fabrika yo'liga o'tish uchun bitta savolga javob kerak: Nest yaratgan pool'ni kim va qachon yopadi? Javob — hayot sikli ilgaklari.
1. Nega bu kerak?
25-qismda graceful shutdown ni o'zimiz yozgandik: signal kelsa — yangi ulanish qabul qilinmaydi, boshlangan so'rovlar tugaydi, keyin pool yopiladi. Tartib muhim edi: so'rov hali bazadan o'qiyotganda pool'ni yopsangiz — so'rov xato bilan tugaydi.
Nest ilovasida resurslar provider'lar ichida yashaydi. Ularni ochish va yopish ham provider'ning o'zida bo'lishi tabiiy: "kim yaratgan bo'lsa, o'sha yopadi". Lekin provider "hozir yopish vaqti" ekanini qayerdan biladi? Nest unga xabar beradi — maxsus nomli metodni chaqirib.
O'xshatish: do'kon. Ochilishda — har bo'lim o'z rastasini tayyorlaydi (onModuleInit), keyin eshik ochiladi (onApplicationBootstrap). Yopilishda — eshikka "yopiq" yozuvi (beforeApplicationShutdown), ichkaridagi xaridorlar to'lab chiqadi (server yopiladi), keyin kassa yopiladi va chiroq o'chadi (onApplicationShutdown). Kassani xaridor ichkarida turganda yopmaysiz.
1.1 Express'da qanday edi
25–27-qismlardagi main.ts da hamma narsa bitta joyda, qo'lda yozilgan edi:
omborYarat(config, log)— pool va Redis ochiladi.ishgaTushir(...)— server tinglaydi.- Signal kelsa —
yop(server), keyinsaqlash.yop().
Bu yaxshi ishlardi, chunki resurs bitta va uni main.ts yaratgan. Nest'da esa resurslar ko'payadi va ular turli modullarda yashaydi: ombor — OmborModule da, keyinroq kesh, navbat, pochta. Har birini main.ts ga olib chiqib qo'lda yopish — har yangi modul uchun main.ts ni o'zgartirish degani.
| Express (25–27) | Nest (shu dars) | |
|---|---|---|
| Kim yaratadi | main.ts |
modulning fabrikasi |
| Kim yopadi | main.ts dagi oxirida |
modulning o'z hook'i |
| Yangi resurs qo'shilsa | main.ts o'zgaradi |
faqat shu modul |
| Tartibni kim biladi | dasturchi (qo'lda) | Nest (modul daraxti bo'yicha) |
Yutuq — har modul o'zini o'zi tozalaydi. Narx — tartib endi kodda ko'rinmaydi, uni Nest qoidalaridan bilish kerak. Bu dars aynan o'sha qoidalar haqida.
2. Besh hook
2.1 Jadval
| Hook | Qachon | Tipik ish |
|---|---|---|
onModuleInit() |
modulning provider'lari tayyor bo'lgach | kesh to'ldirish, ulanishni tekshirish |
onApplicationBootstrap() |
hamma modul tayyor, port hali ochilmagan | boshqa modullarga murojaat, ro'yxatga olish |
onModuleDestroy() |
app.close() boshida |
yangi ish qabul qilmaslik |
beforeApplicationShutdown(signal?) |
server yopilishidan oldin | sog'liq belgisini "tayyor emas" qilish |
onApplicationShutdown(signal?) |
HTTP server yopilgandan keyin | pool, Redis, fayllarni yopish |
Har biri — interfeys (OnModuleInit, OnApplicationShutdown …). implements OnApplicationShutdown yozish majburiy emas — Nest metod nomiga qaraydi. Lekin interfeys imzo xatosini (masalan, onApplicationShutdowm) TypeScript'ga ushlatadi.
Yana bir eslatma — Provider scope'lari darsidan: REQUEST scope'li provider'larda bu hook'lar chaqirilmaydi. Ular ishga tushishda emas, har so'rovda yaratiladi.
TRANSIENT bilan holat boshqacha. Biz demo loyihada tekshirdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0): TRANSIENT xizmatni ikki xizmat (A va B) oladi, yonida REQUEST scope'li Sorovli xizmati bor. Uchalasida ham onModuleInit bor:
onModuleInit: Transient #1
onModuleInit: Transient #2
onApplicationShutdown: Transient #1
onApplicationShutdown: Transient #2Nima bo'ldi? TRANSIENT xizmat har iste'molchi uchun alohida yaratildi — ikki nusxa. Ikkalasida ham hook'lar chaqirildi. Sorovli esa logda umuman yo'q: REQUEST provider'ning hook'i ishlamadi. Qoida: hook — ishga tushishda yaratilgan har nusxada (DEFAULT va TRANSIENT), REQUEST'da — yo'q.
Tekshirib ko'ring: Pool'ni ochadigan va
onApplicationShutdownda yopadiganBazaXizmatiTRANSIENT qilib qo'yildi. Uni uch xizmat oladi. Ilovada nechta pool bo'ladi va yopilishda nima bo'ladi?
Javob
Uchta pool — har iste'molchiga alohida nusxa, har birida o'z pool'i. Yopilishda uch nusxaning hook'i chaqiriladi va uchala pool yopiladi. Xato yo'q, lekin ulanishlar uch baravar ko'p. Pool kabi qimmat resurs — DEFAULT (yagona) provider'da bo'lsin.
2.2 Kanon izi: tartib
Kanon ikki modulli ilova yozdi: ichki modul va uni import qilgan ildiz. Har birida beshala hook vaqtni yozadi; onApplicationShutdown — async, 20 ms kutadi (pool yopilishining o'rnida). Chiqishda ikki narsaga qarang: modullar qaysi tartibda chiqadi va oxirgi ikki qator orasidagi vaqt farqi. Natija (kanon tajribasi, Vitest muhitida):
1) ulanishsiz
211 ms create() tugadi
222 ms onModuleInit: ichki modul
222 ms onModuleInit: ildiz modul
222 ms onApplicationBootstrap: ichki modul
222 ms onApplicationBootstrap: ildiz modul
229 ms listen() tugadi
229 ms app.close() boshlandi
229 ms onModuleDestroy: ildiz modul
230 ms onModuleDestroy: ichki modul
230 ms beforeApplicationShutdown: ildiz modul (signal SINOV)
230 ms beforeApplicationShutdown: ichki modul (signal SINOV)
266 ms onApplicationShutdown: ildiz modul (signal SINOV) — 20 ms async
291 ms onApplicationShutdown: ichki modul (signal SINOV) — 20 ms async
291 ms app.close() tugadi (62 ms)Uch qoida ko'rinadi:
- Ishga tushish — pastdan yuqoriga: avval ichki modul, keyin ildiz. Kim nimaga tayansa, o'sha oldin tayyor (Modullar darsidagi
InstanceLoadertartibi). - Yopilish — yuqoridan pastga: avval ildiz, keyin ichki. Iste'molchi bog'liqligidan oldin to'xtaydi — ombordan foydalanadigan xizmat ombordan oldin "xayrlashadi".
- Async hook'lar ketma-ket kutiladi: ildizning
onApplicationShutdowni 266 ms da, ichkiniki 291 ms da — 20 ms'dan ortiq farq. Nest birinchisini kutib, keyin ikkinchisini chaqirdi.
(signal SINOV) — app.close("SINOV") ga berilgan nom. Haqiqiy signalda bu SIGTERM yoki SIGINT bo'ladi.
2.3 JS modelida tartib
Tartibni modelda takrorlaymiz — brauzerda ishlaydi:
const iz = [];
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
// modul daraxti: ildiz ichki modulni import qiladi
const ichki = { nom: "ichki (OmborModule)", imports: [] };
const ildiz = { nom: "ildiz (AppModule)", imports: [ichki] };
// pastdan yuqoriga: avval import qilinganlar
function tartib(m) {
return [...m.imports.flatMap(tartib), m];
}
const hook = (nom) => async (m, signal) => {
if (nom === "onApplicationShutdown") await kut(20); // pool.end()
iz.push(`${nom}: ${m.nom}${signal ? ` (${signal})` : ""}`);
};
async function boshla(m) {
for (const x of tartib(m)) await hook("onModuleInit")(x);
for (const x of tartib(m)) await hook("onApplicationBootstrap")(x);
}
async function yop(m, signal) {
const teskari = tartib(m).toReversed();
for (const x of teskari) await hook("onModuleDestroy")(x);
for (const x of teskari) {
await hook("beforeApplicationShutdown")(x, signal);
}
iz.push("— HTTP server yopildi —");
for (const x of teskari) {
await hook("onApplicationShutdown")(x, signal);
}
}
await boshla(ildiz);
await yop(ildiz, "SIGTERM");
console.log(iz.join("\n"));Konsolda:
onModuleInit: ichki (OmborModule)
onModuleInit: ildiz (AppModule)
onApplicationBootstrap: ichki (OmborModule)
onApplicationBootstrap: ildiz (AppModule)
onModuleDestroy: ildiz (AppModule)
onModuleDestroy: ichki (OmborModule)
beforeApplicationShutdown: ildiz (AppModule) (SIGTERM)
beforeApplicationShutdown: ichki (OmborModule) (SIGTERM)
— HTTP server yopildi —
onApplicationShutdown: ildiz (AppModule) (SIGTERM)
onApplicationShutdown: ichki (OmborModule) (SIGTERM)for … await — har hook oldingisi tugagach chaqiriladi, Nest'dagidek. "HTTP server yopildi" qatori beforeApplicationShutdown va onApplicationShutdown orasida — pool'ni yopish uchun eng xavfsiz lahza shuning uchun keyingisi. Model soddalashtirilgan: haqiqiy Nest bir modul ichidagi bir nechta provider'ning hook'larini parallel chaqiradi (bitta modul — Promise.all), modullar orasida esa navbat bilan.
stateDiagram-v2
[*] --> Yaratildi: NestFactory.create
Yaratildi --> Tayyor: onModuleInit va onApplicationBootstrap
Tayyor --> Ishlayapti: listen
Ishlayapti --> Yopilmoqda: app.close
Yopilmoqda --> ServerYopiq: onModuleDestroy va beforeApplicationShutdown
ServerYopiq --> [*]: onApplicationShutdownHolatlar diagrammasida o'tishlar ustida qaysi hook'lar chaqirilishi yozilgan. "Ishlayapti" — so'rovlar qabul qilinadigan yagona holat.
3. Bajarilayotgan so'rov va app.close()
3.1 Kanon o'lchovi
Kanon uchta holatni o'lchadi: ulanishsiz (yuqorida, 62 ms), bo'sh keep-alive ulanish ochiq va 300 ms davom etadigan so'rov ishlab turganda. Ikkinchisi:
2) bo'sh keep-alive ulanish ochiq
…
30 ms app.close() boshlandi
…
91 ms app.close() tugadi (61 ms)Bo'sh keep-alive ulanish yopilishni kechiktirmadi: Node 24 server.close() paytida o'sha lahzada bo'sh turgan ulanishlarni o'zi yopadi. Uchinchisi:
3) bajarilayotgan 300 ms so'rov bor
…
75 ms app.close() boshlandi
76 ms onModuleDestroy: ildiz modul
77 ms onModuleDestroy: ichki modul
77 ms beforeApplicationShutdown: ildiz modul (signal SINOV)
77 ms beforeApplicationShutdown: ichki modul (signal SINOV)
345 ms uzoq so'rov tugadi
353 ms mijoz javob oldi: ok
3377 ms onApplicationShutdown: ildiz modul (signal SINOV) — 20 ms async
3400 ms onApplicationShutdown: ichki modul (signal SINOV) — 20 ms async
3400 ms app.close() tugadi (3325 ms)So'rov 345 ms da tugadi, mijoz javobini oldi — yaxshi. Lekin app.close() yana 3 sekund kutdi. Sabab: so'rov tugagach, uning ulanishi bo'sh keep-alive holatiga o'tdi. Yopish boshlangan paytda u band edi, shuning uchun darhol yopilmadi. Keyin u keep-alive timeout tugaguncha (tajribada sukut — taxminan 3 s) kutdi. vazifalar-api da keepAliveTimeout = 65_000 (Bootstrap darsidagi ishgaTushir) — ya'ni yopish 65 sekundgacha cho'zilishi mumkin edi. Orkestrator esa odatda 30 s kutadi va keyin jarayonni majburan o'ldiradi — pool to'g'ri yopilmay qoladi.
3.2 Yechim — 25-qismdagi yop()
25-qismda bu muammoni allaqachon hal qilgandik: yop(server) har 100 ms server.closeIdleConnections() chaqiradi — so'rov tugab, ulanish bo'shashi bilan u darhol yopiladi. Hisobni kichik modelda solishtiraylik (brauzerda ishlaydi):
// yopish qachon tugaydi (ms): ulanishlar — bo'sh yoki band (so'rov
// tugash vaqti bilan); keepAlive — bo'sh ulanish yashash vaqti
function yopishVaqti(ulanishlar, { keepAlive, bosh100 }) {
const vaqtlar = ulanishlar.map((u) => {
const bandTugash = u.band ?? 0; // so'rov tugaydigan lahza
if (bosh100) {
// har 100 ms bo'shlar yopiladi: band tugagach keyingi 100 ms
return Math.ceil(bandTugash / 100) * 100;
}
// server.close(): band bo'lgan ulanish keyin keep-alive kutadi;
// yopish boshida bo'sh bo'lganini Node 24 darhol yopadi
return u.band === undefined ? 0 : bandTugash + keepAlive;
});
return Math.max(0, ...vaqtlar);
}
const ulanishlar = [{}, { band: 270 }]; // bitta bo'sh, bitta band
for (const bosh100 of [false, true]) {
const ms = yopishVaqti(ulanishlar, { keepAlive: 3000, bosh100 });
const nom = bosh100 ? "yop() (har 100 ms):" : "app.close():";
console.log(nom, ms, "ms");
}
const uzoq = { keepAlive: 65000, bosh100: false };
console.log("keepAlive 65 s bilan app.close():",
yopishVaqti(ulanishlar, uzoq), "ms");Konsolda:
app.close(): 3270 ms
yop() (har 100 ms): 300 ms
keepAlive 65 s bilan app.close(): 65270 msBirinchi qator kanon o'lchovining (3 325 ms) taxminiy aksi: band ulanish + keep-alive. Model — oddiy arifmetika, haqiqiy taymerlar emas; lekin xulosa bir xil: yop() bilan yopish so'rov tugashi bilan deyarli bir vaqtda tugaydi.
3.3 Kanon qarori
Shuning uchun vazifalar-api da yopish tartibi:
- Signal (
SIGTERM/SIGINT) —jarayon.tsushlaydi (25-qism). yop(server)— yangi ulanish yo'q, boshlanganlar tugaydi, bo'sh keep-alive'lar darhol yopiladi.app.close()— Nest hook'lari:OmborModuledagionApplicationShutdownpool va Redis'ni yopadi.
3.4 Qolgan hook'lar nimaga kerak
Loyihada faqat onApplicationShutdown ishlatildi. Qolgan to'rttasi qachon kerak bo'ladi?
onModuleInityokionApplicationBootstrap? Farq — "kim tayyor".onModuleInitda faqat shu modulning provider'lari aniq tayyor. Boshqa modullarni ishlatish kerak bo'lsa (masalan, hamma@Hodisadametodlarni yig'ish) —onApplicationBootstrap: u paytda ilovaning hamma moduli tayyor. Keyingi darsdagiDiscoveryServicemisoli aynan shu hook'da ishlaydi.onModuleDestroy— yopish boshida "endi yangi ish olmayman" deyish uchun. Masalan, navbat ishchisi yangi topshiriq olishni to'xtatadi, lekin boshlanganini tugatadi.beforeApplicationShutdown— server hali yopilmagan. Bu lahzada sog'liq tekshiruvi "tayyor emas" deb javob bera boshlasa, yuk taqsimlovchi bu nusxaga yangi so'rov yubormay qo'yadi. Sog'liq tekshiruvlarining o'zi (/health/ready) — Health check'lar darsida.
Qisqa qoida: ochish — onModuleInit (o'z modulim) yoki onApplicationBootstrap (butun ilova); yopish — onApplicationShutdown (boshlangan so'rovlar tugagach).
4. enableShutdownHooks()
app.enableShutdownHooks();Bu qator Nest'ga SIGTERM, SIGINT va boshqa signallarni tinglashni buyuradi: signal kelsa, Nest app.close(signal) ni o'zi chaqiradi. Sukut bo'yicha u o'chiq — Nest hujjati sababini aytadi: tinglovchilar xotira sarflaydi va bir jarayonda bir nechta Nest ilovasi bo'lsa, ular ko'payib ketadi. Skelet main.ts da (Nest CLI va loyiha tuzilmasi darsida) bu qator bor edi.
vazifalar-api da u yo'q. Signalni allaqachon jarayon.ts ushlaydi. Ikkalasi ham tinglasa, bitta SIGTERM ikki yopishni boshlaydi: biri yop() + app.close(), ikkinchisi to'g'ridan app.close() — yop() siz. Ikkinchisi yuqoridagi 3 sekund (yoki 65) muammosini qaytaradi va pool ikki marta yopilishga urinadi.
Windows'da SIGTERM ni boshqa jarayondan yuborib bo'lmaydi — jonli signal sinovi kanonda sinalmagan (25-qismdagidek). Tartib testlarda app.close() ni to'g'ridan chaqirib tekshirildi.
Tekshirib ko'ring: Siz
enableShutdownHooks()ni qo'shdingiz va 25-qismdagijarayon.tsham ishlayapti. KonteynerSIGTERMyubordi. Poolend()necha marta chaqiriladi va nima bo'ladi?
Javob
Ikki marta urinish bo'ladi: Nest'ning o'z signal ishlovchisi app.close() ni chaqiradi, jarayon.ts esa yop() dan keyin yana app.close() ni. pg pool'ida end() ni ikkinchi marta chaqirish xato beradi (Called end on pool more than once). Signalni bitta joyda ushlang. Ikkinchi chaqiruvga chidamli hook yozish — «Idempotent yopuvchi» mashqida.
5. abortOnError: fabrika yiqilsa
5.1 Sukut xulq
Endi ombor Nest ichida, async fabrikada yaratiladi. Fabrika yiqilsa (baza yo'q, EGASI_ID noto'g'ri), Nest sukut bo'yicha xatoni logga yozadi va jarayonni o'zi tugatadi. Bu main.ts uchun mos — fail-fast. Lekin testda?
Biz tekshirdik (biz ishga tushirdik: NestJS 12.2.0, Node 24.21.0, PostgreSQL 17 — o'z klasterimiz). Yangi test ilovaYarat ni noto'g'ri EGASI_ID bilan chaqiradi va rejects kutadi. abortOnError sozlanmagan holda Vitest:
----- JavaScript stack trace -----
1: handleInitializationError (file:///…/node_modules/@nestjs/core/nest-factory.js:117:21)
2: initialize (file:///…/node_modules/@nestjs/core/nest-factory.js:112:18)
3: processTicksAndRejections (node:internal/process/task_queues:104:5)
⎯⎯⎯⎯⎯⎯ Unhandled Error ⎯⎯⎯⎯⎯⎯⎯
Error: [vitest-pool]: Worker forks emitted error.
Caused by: Error: Worker exited unexpectedly with exit code 134 during started state while running test file …/test/ombor-modul.test.ts(Yo'llarni va ichki iz qatorlarini qisqartirdik.) Bu chiqishda bizning test nomimiz ham, rejects xabari ham yo'q — butun test fayli bilan birga yo'qolgan. Eng muhim qator — oxirgisi. Tarjimasi: "Vitest ishchi jarayoni kutilmaganda 134 kodi bilan tugadi." 134 — abort() signali. Nest handleInitializationError ichida jarayonni to'xtatdi — test fayli bilan birga. rejects gacha yetib bormadi.
5.2 abortOnError: false
NestFactory.create(AppModule.forRoot(…), {
bodyParser: false, logger: nestLogi, abortOnError: false,
});Endi Nest jarayonni o'ldirmaydi — xato create qaytargan Promise orqali chiqadi. Test uni rejects bilan ushlaydi. main.ts da esa top-level await rad etiladi va Node baribir exit 1 bilan tugaydi — fail-fast saqlanadi. Biz main.ts ni EGASI_ID=999 bilan yurgizdik:
ERROR [ExceptionHandler] Error: EGASI_ID=999 foydalanuvchilar jadvalida yo'q
at asosiyOmbor (file:///…/dist/ombor-yarat.js:67:23)
at async omborYarat (file:///…/dist/ombor-yarat.js:26:15)
at async Injector.instantiateClass (…/injector.js:528:37)
…
Error: EGASI_ID=999 foydalanuvchilar jadvalida yo'q
…
Node.js v24.21.0
exit 1(Jarayon raqami, vaqt va yo'llar qisqartirildi.) Birinchi blok — Nest logi, ikkinchisi — Node'ning ushlanmagan xatosi. Exit kodi — 1, xato matni — 27-qismdagi omborYarat ning o'zi.
6. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Yopilishda "Connection terminated" xatolari | pool so'rovlar tugamasdan yopildi | pool'ni onApplicationShutdown da yoping |
app.close() sekundlab kutadi |
band ulanish keyin keep-alive kutadi | closeIdleConnections bilan drenaj (yop()) |
| Pool ikki marta yopiladi | signal ikki joyda ushlangan | bitta joy; hook'ni idempotent qiling |
| Hook chaqirilmaydi | REQUEST provider yoki ilova close() siz tugadi |
DEFAULT scope; signalni ushlab close() |
| Hook bir necha marta chaqiriladi | TRANSIENT provider — har iste'molchida alohida nusxa | resursni DEFAULT provider'da yoping |
| Testda Vitest ishchisi 134 bilan o'ladi | fabrika xatosida Nest abort() |
abortOnError: false |
7. Mashqlar
Mashqlar kurs/mashqlar/30/13-lifecycle/ da.
1-mashq (oson): qaysi hook?
- Pool'ni yopish uchun eng xavfsiz hook —
- Hamma modul tayyor, port hali yopiq —
- Yopilishda birinchi chaqiriladigan hook —
- Yopilishda qaysi modul birinchi: ildiz yoki ichki? —
- Nest'ga signallarni tinglatadigan metod —
app.()
Yechim
- onApplicationShutdown — HTTP server yopilgandan keyin.
- onApplicationBootstrap —
listendan oldin. - onModuleDestroy —
app.close()boshida. - ildiz — yopilish yuqoridan pastga; ishga tushish esa pastdan yuqoriga.
- enableShutdownHooks —
vazifalar-apida ishlatilmaydi.
2-mashq (o'rta): idempotent yopuvchi
SaqlashYopuvchi modelini yozing: onApplicationShutdown(signal) ikki marta (parallel) chaqirilsa ham pool.end() bir marta chaqirilsin. Soxta pool end() ikkinchi marta chaqirilsa Error("Called end on pool more than once") tashlasin. Ishora: birinchi chaqiruvning Promise'ini maydonda saqlang va ??= (mantiqiy tayinlash) bilan qayta ishlating.
Yechim
class SaqlashYopuvchi {
#yopilmoqda = null;
constructor(pool) { this.pool = pool; }
onApplicationShutdown(signal) {
// ikkinchi chaqiruv o'sha Promise'ni oladi — pool bir marta
// yopiladi
this.#yopilmoqda ??= this.pool.end().then(() => {
console.log(`pool yopildi (${signal})`);
});
return this.#yopilmoqda;
}
}
let yopishlar = 0;
const pool = {
end: async () => {
yopishlar++;
if (yopishlar > 1) {
throw new Error("Called end on pool more than once");
}
},
};
const y = new SaqlashYopuvchi(pool);
await Promise.all([
y.onApplicationShutdown("SIGTERM"),
y.onApplicationShutdown("SIGINT"),
]);
console.log(`pool.end chaqirildi: ${yopishlar} marta`);Konsolda:
pool yopildi (SIGTERM)
pool.end chaqirildi: 1 martaIkkinchi chaqiruv yangi yopish boshlamadi — birinchisining Promise'ini qaytardi va u bilan birga kutdi. Bu himoya signal bitta joyda ushlangan bo'lsa ham foydali: test ham, main.ts ham app.close() ni bir necha marta chaqirishi mumkin.
3-mashq (qiyin): onModuleInit da tekshiruv
Ba'zi jamoalar ulanishni fabrikada emas, onModuleInit da tekshiradi. Model yozing: BazaXizmati klassi onModuleInit() da await this.pool.query("SELECT 1") qiladi; xato bo'lsa Error("baza javob bermadi: <xabar>") tashlaydi. ishgaTushir(provider'lar) hamma provider'ning onModuleInit ini ketma-ket chaqirsin va birinchi xatoda to'xtab, "ilova ishga tushmadi: …" chiqarsin; hammasi o'tsa — "ilova tayyor". Ikki soxta pool bilan sinang: ishlaydigan va ECONNREFUSED tashlaydigan. Ishora: typeof p.onModuleInit === "function" — hook bormi.
Yechim
class BazaXizmati {
constructor(pool) { this.pool = pool; }
async onModuleInit() {
try {
await this.pool.query("SELECT 1");
} catch (x) {
throw new Error(`baza javob bermadi: ${x.message}`);
}
}
}
class LogXizmati {} // hook'i yo'q
async function ishgaTushir(providerlar) {
try {
for (const p of providerlar) {
if (typeof p.onModuleInit === "function") {
await p.onModuleInit();
}
}
console.log("ilova tayyor");
} catch (x) {
console.log(`ilova ishga tushmadi: ${x.message}`);
}
}
const yaxshi = { query: async () => ({ rows: [{ "?column?": 1 }] }) };
const yomon = {
query: async () => {
throw new Error("connect ECONNREFUSED 127.0.0.1:5432");
},
};
await ishgaTushir([new LogXizmati(), new BazaXizmati(yaxshi)]);
await ishgaTushir([new LogXizmati(), new BazaXizmati(yomon)]);Konsolda:
ilova tayyor
ilova ishga tushmadi: baza javob bermadi: connect ECONNREFUSED 127.0.0.1:5432Nest ham onModuleInit dagi xatoda ishga tushishni to'xtatadi. Farq: fabrikada tekshirish (kanon yo'li) xizmatni yaroqsiz holatda yaratmaydi — ombor faqat tekshiruvdan keyin paydo bo'ladi. onModuleInit da esa obyekt avval yaratiladi, keyin tekshiriladi. Ikkalasi ham fail-fast; fabrika biroz toza.
8. Loyiha qadami: ombor Nest ichida yaratiladi va yopiladi
8.1 Reja
Uch o'zgarish: OmborModule.forRootAsync() ga yopuvchi provider; ilova.ts ga abortOnError: false; main.ts omborni yaratmaydi — faqat config beradi. Va yangi testlar: fabrika xatosi va pool yopilishi.
8.2 ombor.module.ts: SaqlashYopuvchi
Faylga yangi importlar (Inject, Injectable, OnApplicationShutdown turi) va klass:
/** Lifecycle hook: onApplicationShutdown — HTTP server
* yopilgandan KEYIN (boshlangan so'rovlar bazani ishlatib bo'lgach)
* chaqiriladi */
@Injectable()
class SaqlashYopuvchi implements OnApplicationShutdown {
constructor(
@Inject(SAQLASH) private readonly saqlash: Saqlash,
@Inject(LOG) private readonly log: Log,
) {}
async onApplicationShutdown(signal?: string): Promise<void> {
await this.saqlash.yop();
this.log.info({ signal }, "ombor ulanishlari yopildi");
}
}forRootAsync() provider'lari ro'yxati oxiriga — SaqlashYopuvchi,. Fayl boshidagi izohga "ilova yopilganda pool/redis yopiladi" qo'shiladi. forRoot(ombor) ga yopuvchi qo'shilmaydi: tayyor omborni bergan kim bo'lsa (test), u o'zi yopadi.
Nega alohida klass, SAQLASH fabrikasi ichida emas? Fabrika — funksiya, unda hook bo'lmaydi. Hook'lar faqat klass nusxalarida chaqiriladi. SaqlashYopuvchi eksport qilinmagan, faqat shu modul ichida — u hech kimga xizmat qilmaydi, faqat yopadi. SAQLASH token'i ham shu yerda kerak bo'ldi — Custom provider'lar darsidagi savolning javobi.
8.3 ilova.ts va main.ts
ilova.ts da NestFactory.create sozlamalariga bitta maydon va izoh qo'shiladi:
// abortOnError: false — provider fabrikasi yiqilsa (baza yo'q,
// EGASI_ID yo'q) Nest process.exit(1) QILMASIN, xato Promise orqali
// chiqsin: testda ushlanadi, main.ts da top-level await → exit 1
const app = await NestFactory.create<NestExpressApplication>(
AppModule.forRoot({
ombor, config, log, xabarchi, sozlamalar: { tz },
}),
{ bodyParser: false, logger: nestLogi, abortOnError: false },
);main.ts:
-import { ishgaTushir, omborYarat } from "./ishga-tushir.ts";
+import { ishgaTushir } from "./ishga-tushir.ts"; const log = logYarat(config.LOG_LEVEL);
-const saqlash = await omborYarat(config, log);
const { server, manzil, app } = await ishgaTushir({
port: config.PORT,
host: config.HOST,
bogliqliklar: {
- ombor: saqlash.ombor,
config,Oxiridagi jarayonniBoshqar uchinchi argumenti — oxirida — soddalashadi: app.close() va saqlash.yop() o'rniga endi faqat app.close() (pool'ni OmborModule yopadi):
}, { oxirida: () => app.close() });jarayonniBoshqar chaqiruvi oldiga izoh qo'shiladi:
// Signal (SIGTERM/SIGINT) va "let it crash" — o'zimizniki
// (jarayon.ts): avval HTTP ulanishlar to'g'ri yopiladi (yop — bo'sh
// keep-alive'lar darhol), keyin app.close() — Nest lifecycle
// hook'lari (OmborModule: pool va Redis). app.enableShutdownHooks()
// ISHLATILMAYDI — signal ikki joyda ushlanib, yopish ikki marta
// boshlanmasinFayl boshidagi izoh ham yangilanadi: "config (fail-fast) → Nest ilovasi (ombor DI ichida yaratiladi) → server → jarayon hodisalari". main.ts endi ombor haqida hech narsa bilmaydi — u faqat config va logni beradi.
8.4 Test: fabrika xatosi va pool yopilishi
test/ombor-modul.test.ts ga Postgres qismi qo'shiladi (fayl boshiga afterAll, describe, pg va xotiraLogi importlari; izohga "va lifecycle … app.close() pool ulanishlarini yopadi"):
// namuna bazasi (Aziz — id 1): test bazalarini boshqa testlar
// parallel TRUNCATE qiladi — bu yerda faqat o'qiladi
const URL = process.env["TEST_NAMUNA_DATABASE_URL"] ??
"postgres://postgres@127.0.0.1:5495/vazifalar";
const admin = new pg.Pool({ connectionString: URL, max: 1 });
const bor = await admin.query("SELECT 1")
.then(() => true, () => false);
if (!bor) {
test.skip(`OmborModule (postgres) — baza yo'q (${URL})`, () => {});
} else {
afterAll(() => admin.end());
const SQL = "SELECT count(*)::int AS n FROM pg_stat_activity " +
"WHERE application_name = 'vazifalar-api' " +
"AND datname = current_database()";
const ulanishlar = async () =>
(await admin.query<{ n: number }>(SQL)).rows[0]?.n ?? -1;
const pgConfig = (EGASI_ID: string) => configYukla({
SAQLASH: "postgres", DATABASE_URL: URL, EGASI_ID,
});
describe("OmborModule.forRootAsync (postgres)", () => {
test("EGASI_ID bazada yo'q — ilovaYarat rad etiladi " +
"(jarayon o'lmaydi: abortOnError false)", async () => {
await assert.rejects(
ilovaYarat({ config: pgConfig("999999") }),
/EGASI_ID=999999 foydalanuvchilar jadvalida yo'q/,
);
});
test("app.close() → onApplicationShutdown: pool yopiladi " +
"(pg_stat_activity'da ulanish qolmaydi)", async () => {
const { log, qatorlar } = xotiraLogi();
const app = await ilovaYarat({ config: pgConfig("1"), log });
assert.ok((await ulanishlar()) >= 1);
await app.close();
// pool.end() soket yopilishini kutmaydi — server tomonida seans
// bir lahza qolishi mumkin
let n = await ulanishlar();
for (let i = 0; i < 40 && n > 0; i++) {
await new Promise((r) => setTimeout(r, 50));
n = await ulanishlar();
}
assert.equal(n, 0);
assert.ok(
qatorlar.some((q) => q.msg === "ombor ulanishlari yopildi"),
);
});
});
}Ikkinchi test ulanishlarni Postgres'ning o'zidan so'raydi: pg_stat_activity — serverdagi seanslar ro'yxati (Monitoring). application_name = 'vazifalar-api' — bizning pool. app.close() dan keyin u 0 bo'lishi kerak. Kutish halqasi — Bootstrap darsidagi migratsiya.test topilmasi: pool.end() soket yopilishini kutmaydi.
8.5 Tekshiruv: avval va keyin
Testni bosqichma-bosqich yurgizdik (biz ishga tushirdik, PostgreSQL 17 — o'z klasterimiz, kanon sxemasi; TEST_NAMUNA_DATABASE_URL shu bazaga). abortOnError siz — yuqoridagi exit code 134. abortOnError: false bilan, lekin SaqlashYopuvchi siz:
❯ test/ombor-modul.test.ts (4 tests | 1 failed) 2536ms
✓ SAQLASH=xotira: ombor config'dan (fabrika) — XotiraOmbori 93ms
✓ ombor ham, config ham yo'q — darhol xato 2ms
❯ OmborModule.forRootAsync (postgres) (2)
✓ EGASI_ID bazada yo'q — ilovaYarat rad etiladi (jarayon o'lmaydi: abortOnError false) 64ms
× app.close() → onApplicationShutdown: pool yopiladi (pg_stat_activity'da ulanish qolmaydi) 2374ms
AssertionError: Expected values to be strictly equal:
1 !== 0Chiqishda uchta test yashil, bittasi qizil — qaysi biri ekaniga qarang. 1 !== 0 — app.close() dan 2 sekund keyin ham bitta ulanish ochiq: pool'ni hech kim yopmadi. E'tibor bering: ilova xatosiz yopildi. Hech qanday log yoki ogohlantirish chiqmadi — oqishni faqat test ko'rdi. Bu — "ulanish oqishi" (leak); prodakshnda har qayta ishga tushirish bazada osilgan seans qoldirardi. SaqlashYopuvchi qo'shilgach:
✓ test/ombor-modul.test.ts (4 tests) 247ms
Test Files 1 passed (1)
Tests 4 passed (4)To'liq tekshiruv ham o'tdi: tip 0, lint 0, build. Kanon xuddi shu holatni o'z Postgres 18 bazasida tekshirgan: EGASI_ID=999 → exit 1, app.close() dan keyin pg_stat_activity da 0 ulanish va log qatori. Hali commit qilmang — keyingi darsda bitta test qo'shiladi va holat commit qilinadi.
9. Real ishda
- Kubernetes va Docker konteynerni to'xtatishda
SIGTERMyuboradi va ~30 s kutadi, keyinSIGKILL. To'g'ri yopish shu oynaga sig'ishi kerak (33-qismda). onModuleInit— kesh qizdirish, Kafka/RabbitMQ ulanishi, cron ro'yxatga olish uchun keng ishlatiladi; Nest kutubxonalarining ko'pi (@nestjs/schedule) hamonApplicationBootstrapda ishga tushadi.- Kod review'da: "bu ulanish qayerda yopiladi?" — har yangi resurs uchun birinchi savol. Ikkinchisi: "bu provider'ning scope'i qanday — hook umuman chaqiriladimi?".
- Intervyu: "Nest lifecycle hook'larini tartib bilan ayting", "
enableShutdownHooksnima qiladi?", "Graceful shutdown qanday qilinadi?".
Xulosa
- Ishga tushish:
onModuleInit→onApplicationBootstrap(pastdan yuqoriga); yopish:onModuleDestroy→beforeApplicationShutdown→ server yopiladi →onApplicationShutdown(yuqoridan pastga). - Async hook'lar ketma-ket kutiladi; resurslarni
onApplicationShutdownda yoping. - Hook'lar DEFAULT va TRANSIENT nusxalarda chaqiriladi (TRANSIENT — har nusxada alohida), REQUEST provider'larda — yo'q.
- Bajarilayotgan so'rov bor paytda
app.close()keep-alive timeout'gacha kutadi — 25-qismdagiyop()drenajni hal qiladi. enableShutdownHooks()va o'z signal ishlovchingiz bir vaqtda bo'lmasin —vazifalar-apio'zinikini ishlatadi.abortOnError: false— fabrika xatosi Promise orqali; testlar o'lmaydi,main.tsbaribir exit 1.
Keyingi dars: Reflector, metadata va DiscoveryService — dekorator metama'lumotini o'qish, o'z dekoratorimiz va marshrutlar jadvalini avtomatik tekshiradigan test; holat commit'i.
Manbalar
- NestJS hujjati — Lifecycle events (hook'lar tartibi,
enableShutdownHooks): https://docs.nestjs.com/fundamentals/lifecycle-events - Node.js hujjati —
server.closeIdleConnections(): https://nodejs.org/api/http.html#servercloseidleconnections - PostgreSQL hujjati —
pg_stat_activity: https://www.postgresql.org/docs/18/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW - Kanon tajribasi (IlmHamroh, 2026-10-27): hook tartibi va
app.close()vaqtlari uch holatda, NestJS 12.2.0, Vitest muhitida. TRANSIENT va REQUEST provider'larda hook'lar (2026-11-02, demo),abortOnErrorsinovi,EGASI_ID=999bilanmain.ts,SaqlashYopuvchisiz va bilan testlar — biz ishga tushirdik (2026-11-02, NestJS 12.2.0, Node 24.21.0, PostgreSQL 17).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!