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

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-api buni 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.
  • abortOnError sozlamasi testni qanday "o'ldirishini" haqiqiy chiqishda ko'rasiz.
  • vazifalar-api da ombor ulanishlarini Nest yopadigan qilasiz va main.ts fabrika 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:

  1. omborYarat(config, log) — pool va Redis ochiladi.
  2. ishgaTushir(...) — server tinglaydi.
  3. Signal kelsa — yop(server), keyin saqlash.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:

text
onModuleInit: Transient #1
onModuleInit: Transient #2
onApplicationShutdown: Transient #1
onApplicationShutdown: Transient #2

Nima 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 onApplicationShutdown da yopadigan BazaXizmati TRANSIENT 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):

text
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 InstanceLoader tartibi).
  • 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 onApplicationShutdown i 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:

js
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:

text
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 --> [*]: onApplicationShutdown

Holatlar 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:

text
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:

text
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):

js
// 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:

text
app.close(): 3270 ms
yop() (har 100 ms): 300 ms
keepAlive 65 s bilan app.close(): 65270 ms

Birinchi 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:

  1. Signal (SIGTERM/SIGINT) — jarayon.ts ushlaydi (25-qism).
  2. yop(server) — yangi ulanish yo'q, boshlanganlar tugaydi, bo'sh keep-alive'lar darhol yopiladi.
  3. app.close() — Nest hook'lari: OmborModule dagi onApplicationShutdown pool va Redis'ni yopadi.

3.4 Qolgan hook'lar nimaga kerak

Loyihada faqat onApplicationShutdown ishlatildi. Qolgan to'rttasi qachon kerak bo'ladi?

  • onModuleInit yoki onApplicationBootstrap? Farq — "kim tayyor". onModuleInit da faqat shu modulning provider'lari aniq tayyor. Boshqa modullarni ishlatish kerak bo'lsa (masalan, hamma @Hodisada metodlarni yig'ish) — onApplicationBootstrap: u paytda ilovaning hamma moduli tayyor. Keyingi darsdagi DiscoveryService misoli 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()

ts
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-qismdagi jarayon.ts ham ishlayapti. Konteyner SIGTERM yubordi. Pool end() 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:

text
----- 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

ts
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:

text
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 — listen dan oldin.
  • onModuleDestroy — app.close() boshida.
  • ildiz — yopilish yuqoridan pastga; ishga tushish esa pastdan yuqoriga.
  • enableShutdownHooks — vazifalar-api da 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
js
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:

text
pool yopildi (SIGTERM)
pool.end chaqirildi: 1 marta

Ikkinchi 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
js
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:

text
ilova tayyor
ilova ishga tushmadi: baza javob bermadi: connect ECONNREFUSED 127.0.0.1:5432

Nest 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:

ts
/** 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:

ts
  // 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:

diff
-import { ishgaTushir, omborYarat } from "./ishga-tushir.ts";
+import { ishgaTushir } from "./ishga-tushir.ts";
diff
 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):

ts
}, { oxirida: () => app.close() });

jarayonniBoshqar chaqiruvi oldiga izoh qo'shiladi:

ts
// 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
// boshlanmasin

Fayl 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"):

ts
// 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:

text
 ❯ 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 !== 0

Chiqishda 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:

text
 ✓ 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 SIGTERM yuboradi va ~30 s kutadi, keyin SIGKILL. 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) ham onApplicationBootstrap da 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", "enableShutdownHooks nima 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 onApplicationShutdown da 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-qismdagi yop() drenajni hal qiladi.
  • enableShutdownHooks() va o'z signal ishlovchingiz bir vaqtda bo'lmasin — vazifalar-api o'zinikini ishlatadi.
  • abortOnError: false — fabrika xatosi Promise orqali; testlar o'lmaydi, main.ts baribir 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
NestJS lifecycle hook'lar: onModuleInit, onApplicationShutdown va to'g'ri yopish — IlmHamroh