Penyebaran versi baru berarti proses lama harus menyingkir — dan cara proses “diminta pergi” di dunia Unix adalah lewat sinyal. Tanpa penanganan khusus, SIGTERM menghentikan Node.js seketika: permintaan yang setengah dijawab terputus, koneksi database tidak sempat ditutup, dan log terakhir tidak sempat ditulis. Graceful shutdown adalah kebalikannya — menerima sinyal, berhenti menerima pekerjaan baru, menuntaskan yang sedang berjalan, menutup sumber daya, lalu keluar dengan tenang. Chapter ini membangun urutan itu langkah demi langkah di atas node:http dan node:sqlite, dengan demonstrasi yang benar-benar mengirim sinyal ke proses anak.
Sinyal yang Sama, Nasib yang Berbeda #
Process manager seperti PM2 pada chapter sebelumnya, orchestrator seperti Kubernetes, bahkan perintah kill di terminal — semuanya berkomunikasi lewat sinyal yang sama. Dua yang paling penting untuk kita: SIGTERM, permintaan pematian yang sopan dan bisa kita tangani, dan SIGINT, sinyal yang dikirim saat kita menekan Ctrl+C di terminal. Perilaku bawaan Node.js untuk keduanya sama: proses berhenti seketika, apa pun yang sedang dikerjakan.
Penanganannya satu baris: daftarkan listener untuk sinyal tersebut, dan proses tidak lagi mati seketika — ia menjalankan fungsi kita:
process.on('SIGTERM', () => {
// dari titik ini, kita yang menentukan kapan proses berakhir
});Daftar sinyal lengkap beserta perilaku bawaannya terdokumentasi di referensi process Node.js
. Satu konsekuensi penting: begitu listener terdaftar, sinyal tidak lagi menghentikan proses secara otomatis — bila fungsi kita lupa mengakhiri proses, aplikasi akan menolak mati. Karena itu urutan di dalam listener harus selalu berujung pada process.exit() atau penutupan sumber daya terakhir yang membuat event loop kosong.
Anatomi Pematian Teratur #
Urutan yang benar untuk sebuah server terdiri dari empat langkah yang selalu sama:
- Berhenti menerima koneksi baru —
server.close()memberi tahu kernel agar port tidak lagi menerima koneksi. - Tunggu permintaan berjalan selesai —
server.close()hanya memanggil callback-nya setelah seluruh koneksi aktif berakhir; inilah mekanisme drain bawaan. - Tutup sumber daya lain — koneksi database, berkas yang terbuka, worker yang masih hidup.
- Keluar — dengan
process.exit(0)atau secara alami saat event loop kosong.
Ada satu pengganggu diam-diam pada langkah kedua: keep-alive. Klien HTTP modern menjaga koneksi tetap terbuka untuk dipakai ulang, sehingga server.close() bisa menggantung menunggu koneksi yang tidak pernah ditutup. Fungsi server.closeAllConnections() — hadir sejak Node.js 18.2 dan terdokumentasi di referensi http
— memutus seluruh koneksi diam; strategi yang umum adalah memanggilnya beberapa detik setelah close(), sebagai batas waktu bagi drain.
Menuntaskan Permintaan yang Sedang Berjalan #
Demonstrasi kita membutuhkan dua proses: sebuah induk yang menjalankan server sebagai proses anak, mengirim SIGTERM di saat yang tepat, lalu melaporkan apa yang dilihatnya. Anak memainkan peran server sungguhan dengan satu permintaan yang sedang dikerjakan:
// Server dengan pematian teratur: menerima sinyal SIGTERM saat
// sebuah permintaan sedang dikerjakan, menuntaskannya lebih dulu,
// baru menutup server dan keluar dengan kode 0.
import http from 'node:http';
let permintaanAktif = 0;
const server = http.createServer((req, res) => {
permintaanAktif += 1;
console.log('anak: permintaan /tugas diterima - sedang dikerjakan');
// Beri tahu induk: sekarang ada pekerjaan berjalan.
process.send('siap');
// Simulasi pekerjaan yang memakan waktu.
setTimeout(() => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ tugas: 'daftar tugas siap' }));
permintaanAktif -= 1;
console.log('anak: respons terkirim, permintaan aktif tersisa', permintaanAktif);
}, 400);
});
server.listen(0, '127.0.0.1', async () => {
const port = server.address().port;
const r = await fetch(`http://127.0.0.1:${port}/tugas`);
const isi = await r.json();
console.log('anak: klien menerima', r.status, isi.tugas);
server.closeAllConnections();
});
process.on('SIGTERM', () => {
console.log('anak: SIGTERM diterima - berhenti menerima koneksi baru');
server.close(() => {
console.log('anak: server tertutup - seluruh permintaan selesai');
process.exit(0);
});
});Dan induknya:
import { fork } from 'node:child_process';
const anak = fork('./anak.mjs', [], {
cwd: import.meta.dirname,
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
});
let keluaran = '';
anak.stdout.on('data', (c) => { keluaran += c; });
anak.stderr.on('data', (c) => { keluaran += c; });
console.log('induk: proses anak dijalankan');
anak.on('message', (pesan) => {
console.log('induk: menerima pesan "' + pesan + '" - mengirim SIGTERM');
anak.kill();
});
anak.on('exit', (kode) => {
console.log(keluaran.trimEnd());
console.log('induk: anak berakhir dengan kode ' + kode);
});Output yang Dihasilkan:
induk: proses anak dijalankan
induk: menerima pesan "siap" - mengirim SIGTERM
anak: permintaan /tugas diterima - sedang dikerjakan
anak: SIGTERM diterima - berhenti menerima koneksi baru
anak: respons terkirim, permintaan aktif tersisa 0
anak: klien menerima 200 daftar tugas siap
anak: server tertutup - seluruh permintaan selesai
induk: anak berakhir dengan kode 0Mari kita baca ulurannya baris demi baris:
fork()denganstdioipc: induk menjalankan anak sebagai proses Node terpisah dengan kanal komunikasi —process.send()di anak dan eventmessagedi induk adalah jembatannya.- Handshake
siap: anak mengirim pesan ini dari dalam handler, setelah mencetak bahwa permintaan diterima — dengan demikian induk tahu pasti pekerjaan sedang berjalan sebelum mengirim sinyal; tidak ada adu cepat antar proses. anak.kill(): tanpa argumen,killmengirimSIGTERM— persis sinyal yang dikirim process manager di produksi. Daftar lengkap perilakunya ada di dokumentasinode:child_process.- Listener
SIGTERMdi anak: mencatat, memanggilserver.close()— port berhenti menerima koneksi baru — dan menunggu callback-nya. - Pekerjaan tetap dituntaskan:
setTimeout400 milidetik menyelesaikan respons di tengah-tengah pematian; penghitungpermintaanAktifturun ke 0, klien menerima200— inilah inti graceful shutdown yang sesungguhnya. - Callback
close()terpenuhi: karena satu-satunya koneksi telah berakhir (anak memanggilcloseAllConnections()setelah kliennya selesai), server tertutup dan proses keluar dengan kode 0 — exit code yang dibaca process manager sebagai “pematian bersih”.
Menutup Sumber Daya Lain #
Langkah ketiga anatomi kita — menutup sumber daya — juga punya urutan yang benar. Demo kecil berikut menutup database lalu membuktikan penutupannya dengan mencoba berinteraksi lagi:
import { DatabaseSync } from 'node:sqlite';
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE tugas (id INTEGER PRIMARY KEY, judul TEXT)');
db.prepare('INSERT INTO tugas (judul) VALUES (?)').run('belajar deployment');
function matikanAplikasi() {
db.close();
console.log('koneksi database ditutup');
try {
db.prepare('SELECT COUNT(*) AS jumlah FROM tugas').get();
} catch (e) {
console.log('kueri setelah ditutup ->', e.message);
}
}
matikanAplikasi();Output yang Dihasilkan:
koneksi database ditutup
kueri setelah ditutup -> database is not openUrutannya sederhana namun mengajarkan dua hal. Pertama, db.close() harus dipanggil setelah seluruh query selesai — menutup lebih awal berarti query berjalan melempar galat persis seperti yang ditunjukkan blok try/catch. Kedua, pesan galatnya (database is not open) sengaja dicetak lewat e.message, bukan stack trace penuh — konvensi output yang stabil yang kita pegang sejak awal Course ini.
Batas Waktu untuk Drain yang Macet #
Drain yang baik menunggu, tetapi tidak menunggu selamanya. Permintaan yang menggantung — klien yang lambat membaca respons, pekerjaan yang macet — akan menahan callback close() selamanya, dan process manager umumnya hanya memberi tenggat beberapa detik sebelum mengirim SIGKILL yang tidak bisa ditangani siapa pun. Pola yang lazim: catat niat mati, panggil server.close(), pasang setTimeout beberapa detik yang memanggil server.closeAllConnections() sebagai pemaksa terakhir, dan selalu tutup log sebelum process.exit(). Dengan urutan ini, kasus normal selesai cepat dan kasus macet punya jalan keluar — dua-duanya berakhir dengan exit code yang sehat.
Pematian teratur adalah gerbang terakhir yang berada di tangan aplikasi itu sendiri. Sisanya — memastikan setiap perubahan melewati pemeriksaan sebelum sampai ke produksi — sudah menjadi pekerjaan pipeline, topik penutup section ini: CI/CD dengan GitHub Actions.