Tiga bagian sebelumnya menghasilkan pemroses yang tampak benar; bagian ini membuat keadaannya terlihat dan membuktikannya berulang kali. Kita menutup proyek dengan tiga pekerjaan: endpoint /statistik beserta saringan status sebagai jendela pemantauan, suite test tiga lapis yang mengunci perilaku tanpa satu pun timer, dan titik masuk produksi yang membaca port dari environment lalu berhenti dengan rapi saat diberi sinyal. Pola test-nya memakai node:test — sama dengan Proyek REST API Manajemen Tugas — dengan satu tambahan khas antrean: menunggu keadaan akhir lewat kontrak selesai(), bukan lewat jeda waktu.

Endpoint Statistik dan Saringan Status #

Pemantauan paling sederhana adalah menjawab satu pertanyaan: berapa banyak pekerjaan di setiap status? Method statistik pada antrean sudah menghitungnya; endpoint /statistik tinggal membungkusnya dalam kirimJson tiga baris. Saringan pada daftar pekerjaan sedikit lebih tebal karena memvalidasi masukannya:

js src/server.js
async function tanganiDaftar({ req, res, url, pemroses }) {
  const saringan = url.searchParams.get('status');
  let daftar = pemroses.daftarPekerjaan();
  if (saringan !== null) {
    if (!STATUS_VALID.includes(saringan)) {
      throw new GalatValidasi([
        { field: 'status', pesan: `status harus salah satu: ${STATUS_VALID.join(', ')}` },
      ]);
    }
    daftar = daftar.filter((p) => p.status === saringan);
  }
  kirimJson(res, 200, daftar);
}

Baris 55 membaca parameter ?status= dari searchParams — hasilnya null bila parameter tidak dikirim, sehingga tanpa saringan seluruh daftar dikembalikan. Bila parameter ada tetapi bukan salah satu dari STATUS_VALID, handler melempar GalatValidasi dan server menjawab 422 dengan daftar nilai yang diterima — client langsung tahu apa yang salah tanpa menebak. Saringan ini berguna justru setelah kegagalan: GET /pekerjaan?status=gagal menjadi pintu one-liner untuk mengaudit pekerjaan yang menetap gagal, lengkap dengan pesan dari percobaan terakhirnya.

Peristiwa sebagai Alat Pemantauan #

Endpoint membaca keadaan saat ditanya; peristiwa memberi tahu begitu sesuatu terjadi. Listener PekerjaanSelesai dan PekerjaanGagal bisa dipasang siapa pun yang berminat — mencatat ke log terstruktur, mengirim notifikasi, atau sekadar mengumpulkan ringkasan. Sesi berikut memakai keduanya sekaligus: tiga pekerjaan dimasukkan lewat HTTP selagi worker belum menyala, statistik dibaca sebelum dan sesudah antrean dikuras, lalu daftar peristiwa dicetak:

bash
node src/demo-pemantauan.mjs

Output yang Dihasilkan:

text
POST /pekerjaan -> 201 {"id":1,"jenis":"kirim-email","data":{"penerima":"[email protected]","subjek":"Kwitansi"},"status":"menunggu","percobaan":0}
POST /pekerjaan -> 201 {"id":2,"jenis":"kirim-email","data":{"penerima":"bukan-email","subjek":"Uji kegagalan"},"status":"menunggu","percobaan":0}
POST /pekerjaan -> 201 {"id":3,"jenis":"buat-laporan","data":{"judul":"Laporan penjualan"},"status":"menunggu","percobaan":0}
GET /statistik (worker belum menyala) -> 200 {"menunggu":3,"diproses":0,"selesai":0,"gagal":0,"total":3}
GET /statistik (setelah terproses) -> 200 {"menunggu":0,"diproses":0,"selesai":2,"gagal":1,"total":3}
GET /pekerjaan?status=gagal -> 200 [{"id":2,"jenis":"kirim-email","data":{"penerima":"bukan-email","subjek":"Uji kegagalan"},"status":"gagal","percobaan":3,"pesan":"Penerima email tidak valid: bukan-email"}]
GET /pekerjaan?status=tunda -> 422 {"error":{"kode":"VALIDASI_GAGAL","pesan":"Data pekerjaan tidak lolos validasi","detail":[{"field":"status","pesan":"status harus salah satu: menunggu, diproses, selesai, gagal"}]}}
peristiwa: #1 selesai, #3 selesai, #2 gagal

Bacaan output: tiga POST menghasilkan tiga pekerjaan menunggu; GET /statistik pertama membuktikan belum ada yang dikerjakan; setelah mulai() dan await selesai(), statistik berubah menjadi selesai: 2, gagal: 1; saringan ?status=gagal mengembalikan tepat satu pekerjaan dengan percobaan: 3 beserta pesannya; saringan ?status=tunda ditolak 422; dan baris terakhir menampilkan urutan peristiwa — #2 gagal tercatat setelah #1 dan #3 selesai, sesuai penjadwalan ulang ke ujung barisan dari Bagian 3.

Unit Test Tanpa Timer #

Menguji kode asynchronous paling rawan jebak pada pola “tunggu sebentar lalu periksa”. Suite ini menolak pola itu: setiap test yang menyentuh worker menunggu keadaan akhir lewat await pekerja.selesai(), sehingga tidak ada durasi yang perlu dikira-kira. Contoh paling mewakili adalah test percobaan ulang:

js tests/pekerja.test.js
  test('handler yang gagal sekali berhasil pada percobaan kedua', async () => {
    const antrean = buatAntrean();
    let panggilan = 0;
    const pekerja = buatPekerja({
      antrean,
      handler: {
        'sinkronisasi-data': async () => {
          panggilan += 1;
          if (panggilan < 2) throw new Error('sumber data belum siap');
          return 'sinkronisasi selesai';
        },
      },
    });

    pekerja.mulai();
    antrean.tambah('sinkronisasi-data', {});
    await pekerja.selesai();

    assert.strictEqual(panggilan, 2);
    const akhir = antrean.cari(1);
    assert.strictEqual(akhir.status, 'selesai');
    assert.strictEqual(akhir.percobaan, 2);
    assert.strictEqual(akhir.hasil, 'sinkronisasi selesai');
  });

Tiga teknik bekerja bersama di sini. Pertama, handler disuntik sebagai object literal — pekerjaan sinkronisasi-data tidak ada di registry bawaan, dan memang tidak perlu: unit test menguji worker, bukan handler produksi. Kedua, variabel panggilan menghitung berapa kali handler dijalankan, sehingga klaim “dicoba dua kali” dibuktikan dari jumlah pemanggilan, bukan dari percobaan saja. Ketiga, await pekerja.selesai() pada baris 69 adalah satu-satunya penantuan — sesudahnya seluruh keadaan bersifat final dan bisa dibandingkan dengan assert tanpa syarat. Suite worker berisi sembilan test semacam ini: jalur sukses, gagal tetap dengan pesan, batas percobaan kustom, urutan antre ulang, worker yang belum menyala, dan penghentian di tengah pekerjaan.

Integration Test dengan Server Sungguhan #

Lapis terakhir menguji rantai penuh: routing, parsing body, validasi, sampai antrean terkuras. Server dinyalakan pada port ephemeral dan ditutup di afterEach :

js tests/integrasi.test.js
  let pemroses;
  let server;
  let base;

  beforeEach(async () => {
    pemroses = buatPemroses();
    server = buatServer(pemroses);
    await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
    base = `http://127.0.0.1:${server.address().port}`;
  });

  afterEach(async () => {
    pemroses.berhenti();
    server.closeAllConnections();
    await new Promise((resolve) => server.close(resolve));
  });

Setiap test mendapat pemroses dan server segar; closeAllConnections memutus koneksi keep-alive supaya promise close cepat selesai. Satu test khas menjalankan skenario lengkap dari Bagian 1 sampai 3 lewat HTTP: tiga POST (satu dengan penerima tidak valid), pembacaan /statistik sebelum dan sesudah pemroses.mulai() plus await pemroses.selesai(), lalu pemeriksaan saringan ?status=gagal. Seluruh suite dijalankan dari akar proyek tanpa argumen:

bash
node --test

Output yang Dihasilkan:

text
▶ buatAntrean
  ✔ tambah menghasilkan pekerjaan menunggu dengan id berurutan
  ✔ ambilBerikutnya mengikuti urutan masuk dan menaikkan percobaan
  ✔ pekerjaan yang diambil berstatus diproses dengan percobaan terhitung
  ✔ jadwalkanUlang menempatkan pekerjaan di ujung barisan
  ✔ tandaiSelesai dan tandaiGagal menyimpan hasil dan pesan
  ✔ cari mengembalikan salinan sehingga data internal tidak berubah
  ✔ cari dan tandai pada id yang tidak ada mengembalikan undefined
  ✔ statistik menghitung setiap status beserta total
✔ buatAntrean
▶ API pemroses pekerjaan
  ✔ POST membuat pekerjaan menunggu dengan lokasi dan tipe JSON
  ✔ pekerjaan tetap menunggu selama worker belum menyala
  ✔ GET detail menjawab 200 dan 404 dengan bentuk error seragam
  ✔ jenis asing dijawab 422 beserta detailnya
  ✔ body yang bukan JSON dijawab 400
  ✔ method asing pada endpoint dikenal dijawab 405 dan endpoint asing 404
  ✔ worker menyala: statistik dan saringan status setelah antrean terkuras
✔ API pemroses pekerjaan
▶ buatPekerja
  ✔ handler sukses menandai pekerjaan selesai dan memancarkan peristiwa
  ✔ handler yang selalu gagal berhenti setelah maksimal percobaan
  ✔ handler yang gagal sekali berhasil pada percobaan kedua
  ✔ maksimalPercobaan kustom mengubah batas percobaan
  ✔ pekerjaan yang diulang menunggu di belakang pekerjaan lain
  ✔ tanpa mulai, pekerjaan tetap menunggu
  ✔ setelah berhenti, pekerjaan baru tidak diproses
  ✔ berhenti di tengah pekerjaan menyelesaikan yang sedang berjalan
  ✔ jenis yang tidak dikenal ditandai gagal dengan pesan yang jelas
✔ buatPekerja
▶ validasiPekerjaan
  ✔ jenis dan data yang dikenal lolos tanpa galat
  ✔ jenis yang tidak dikenal ditolak
  ✔ jenis yang bukan teks ditolak
  ✔ data yang bukan object JSON ditolak
✔ validasiPekerjaan
▶ buatPemroses
  ✔ tambahPekerjaan melempar GalatValidasi untuk jenis asing
  ✔ tambahPekerjaan mengembalikan pekerjaan menunggu dengan id pertama
  ✔ pekerjaan diproses penuh setelah mulai dan await selesai
  ✔ handler dan maksimalPercobaan bisa disuntik lewat opsi
  ✔ peristiwa selesai dan gagal dapat dipasang dari luar
✔ buatPemroses
ℹ tests 33
ℹ suites 5
ℹ pass 33
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0

Titik Masuk Produksi #

Terakhir, src/cli.js merangkai semuanya untuk pemakaian sungguhan — port dibaca dari environment, divalidasi, lalu server menyala bersama worker:

js src/cli.js
const pemroses = buatPemroses();
const server = buatServer(pemroses);

pemroses.mulai();

server.listen(port, '127.0.0.1', () => {
  console.log(`Pemroses pekerjaan latar siap di http://127.0.0.1:${port}`);
});

async function matikan() {
  console.log('Menghentikan pemroses...');
  pemroses.berhenti();
  server.closeAllConnections();
  await new Promise((resolve) => server.close(resolve));
  await pemroses.selesai();
  console.log('Pekerjaan yang sedang berjalan telah selesai.');
  process.exit(0);
}

process.on('SIGINT', matikan);
process.on('SIGTERM', matikan);

Urutan di dalam matikan adalah inti graceful shutdown: berhenti menerima pekerjaan baru (berhenti dan penutupan server), menunggu pekerjaan yang sedang berjalan tuntas (await pemroses.selesai()), baru keluar dengan kode 0 — pekerjaan yang masih menunggu memang tidak diproses, dan pada antrean memori itu konsekuensi yang jujur; antrean tahan-lama akan melanjutkannya saat proses menyala kembali. Validasi port dari process.env terlihat langsung dari terminal:

bash
PORT=abc node src/cli.js

Output yang Dihasilkan:

text
PORT tidak valid: abc
kode keluar: 1

Proyek ini menutup rangkaian konsep yang panjang: event loop yang kita pahami sejak awal Course, EventEmitter dari Chapter Event-Driven Architecture, pabrik server dari Proyek REST API, hingga kontrak test deterministik dari Section Testing — semuanya bekerja dalam satu layanan kecil yang hidup di belakang permintaan HTTP. Bekal yang sama akan terpakai lagi pada proyek penutup: Secure Authentication Service, tempat keamanan menjadi pusat perhatian sejak baris kode pertama.