Setiap query yang kita kirim pada dua chapter sebelumnya menumpang sebuah koneksi, dan koneksi itu tidak gratis. Membuka koneksi baru ke PostgreSQL atau MySQL berarti jabat tangan TCP, proses autentikasi, lalu penyiapan status sesi di memori server — pekerjaan yang bisa berlipat kali lebih mahal daripada query pendek yang menumpanginya. Server juga tidak mau menyimpan koneksi tanpa batas: secara bawaan PostgreSQL hanya menerima sekitar seratus koneksi serentak, dan MySQL tidak jauh berbeda. Connection pool menjawab kedua masalah itu sekaligus: sedikit koneksi yang dibuka sekali, lalu dipakai bergiliran oleh banyak query. Chapter ini membedah cara kerjanya lewat simulasi yang bisa kita jalankan langsung, lalu memetakan penerapannya di pg dan mysql2.

Anatomi Sebuah Pool #

Pool adalah penampung koneksi dengan aturan main yang ketat. Alurnya berjalan seperti berikut:

  1. Saat permintaan datang, pool memberikan koneksi nganggur yang tersedia.
  2. Bila belum ada koneksi nganggur tetapi jumlah koneksi masih di bawah batas max, pool membuka koneksi baru.
  3. Bila batas sudah tercapai, permintaan masuk ke antrian dan menunggu hingga ada koneksi dikembalikan.
  4. Pemakai wajib mengembalikan koneksi ke pool setelah selesai — dikembalikan, bukan ditutup.
  5. Koneksi yang lama tidak terpakai ditutup bertahap agar sumber daya server tidak menguap tanpa guna.

Hasil akhirnya: server hanya melihat segelintir koneksi yang stabil, sementara ratusan permintaan bergantian menaikinya. Satu pool berkapasitas 10 koneksi cukup melayani aplikasi web dengan concurrency tinggi, karena setiap permintaan hanya memegang koneksi selama beberapa milidetik.

Simulasi Pool dengan Node Murni #

Pola di atas bisa kita bangun sendiri dalam bentuk paling murni — tanpa jaringan, tanpa database, hanya object dan Promise. Simulasi berikut memakai empat tugas yang berebut dua koneksi, lalu mencetak keadaan pool pada setiap langkah:

js
// Simulasi connection pool untuk memahami polanya.
// "Koneksi" di sini hanyalah object biasa — tanpa jaringan sama sekali.

class PoolSimulasi {
  constructor({ maksKoneksi }) {
    this.maksKoneksi = maksKoneksi;
    this.sedia = [];      // koneksi nganggur, siap dipinjam
    this.aktif = 0;       // koneksi yang sedang dipakai
    this.antrian = [];    // pemanggil yang menunggu koneksi
    this.dibuat = 0;      // total koneksi yang pernah dibuat
  }

  acquire() {
    if (this.sedia.length > 0) {
      const koneksi = this.sedia.pop();
      this.aktif += 1;
      return Promise.resolve(koneksi);
    }
    if (this.dibuat < this.maksKoneksi) {
      this.dibuat += 1;
      this.aktif += 1;
      return Promise.resolve({ id: this.dibuat });
    }
    return new Promise((resolve) => this.antrian.push(resolve));
  }

  release(koneksi) {
    this.aktif -= 1;
    const penerima = this.antrian.shift();
    if (penerima) {
      this.aktif += 1;
      penerima(koneksi);
    } else {
      this.sedia.push(koneksi);
    }
  }

  statistik() {
    return {
      dibuat: this.dibuat,
      aktif: this.aktif,
      menunggu: this.antrian.length,
    };
  }
}

const pool = new PoolSimulasi({ maksKoneksi: 2 });

async function kerjakan(namaTugas) {
  const koneksi = await pool.acquire();
  console.log(`${namaTugas} memakai koneksi ${koneksi.id}`, JSON.stringify(pool.statistik()));
  // Simulasi pekerjaan singkat memakai koneksi.
  await new Promise((selesai) => setImmediate(selesai));
  pool.release(koneksi);
}

const tugas = ['tugas-1', 'tugas-2', 'tugas-3', 'tugas-4'];
await Promise.all(tugas.map(kerjakan));

console.log('Semua tugas selesai', JSON.stringify(pool.statistik()));

Output yang Dihasilkan:

text
tugas-1 memakai koneksi 1 {"dibuat":2,"aktif":2,"menunggu":2}
tugas-2 memakai koneksi 2 {"dibuat":2,"aktif":2,"menunggu":2}
tugas-3 memakai koneksi 1 {"dibuat":2,"aktif":2,"menunggu":1}
tugas-4 memakai koneksi 2 {"dibuat":2,"aktif":2,"menunggu":0}
Semua tugas selesai {"dibuat":2,"aktif":0,"menunggu":0}

Output yang tercetak memperlihatkan drama lengkap antrian tersebut:

  1. Baris 13-25 (acquire): tiga cabang berurutan mengimplementasikan aturan main — ambil dari tumpukan sedia, atau buat baru bila masih di bawah maksKoneksi, atau parkir pemanggil ke antrian lewat Promise yang belum diselesaikan.
  2. Baris 27-36 (release): koneksi tidak pernah ditutup. Bila ada pemanggil yang menunggu, koneksi langsung dioper ke pemilik antrian paling depan; bila tidak ada, koneksi kembali ke tumpukan sedia.
  3. Empat baris pertama output: tugas-1 dan tugas-2 masing-masing memicu pembuatan koneksi (id 1 dan 2), sementara tugas-3 dan tugas-4 tercatat menunggu — nilai menunggu: 2 adalah buktinya.
  4. Baris kelima dan keenam output: begitu tugas-1 melepas koneksi 1, pemanggil antrian terdepan — tugas-3 — menerimanya tanpa pembuatan koneksi baru; tugas-4 pun demikian lewat koneksi 2. Total koneksi yang pernah dibuat tetap 2 untuk 4 tugas.
  5. Baris terakhir output: seluruh tugas selesai, tidak ada yang menunggu, dan kedua koneksi kembali nganggur di pool.

Pool di pg dan mysql2 #

Driver sungguhan membungkus seluruh logika itu sehingga kita cukup menyetel opsi. pg menyediakan class Pool, sedangkan mysql2 menyediakan createPool:

js Pool dengan pg
import pg from 'pg';

const { Pool } = pg;

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 5000,
});
js Pool dengan mysql2
import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  connectionLimit: 10,
  waitForConnections: true,
});

Opsi-opsi tersebut paling sering kita sentuh:

OpsiDriverArtinya
maxpgJumlah koneksi maksimum yang dibuat pool
idleTimeoutMillispgLama koneksi nganggur boleh hidup sebelum ditutup
connectionTimeoutMillispgBatas waktu menunggu koneksi sebelum permintaan dianggap gagal
connectionLimitmysql2Padanan max — jumlah koneksi maksimum
waitForConnectionsmysql2Bila true, pemanggil yang kehabisan koneksi menunggu di antrian

Cara pemakaian sehari-hari bahkan lebih ringkas lagi: pool.query(...) diam-diam meminjam satu koneksi, menjalankan query, lalu mengembalikannya — kita tidak pernah menyentuh acquire dan release secara manual. Method pool.connect() baru diperlukan ketika satu rangkaian query harus berbagi koneksi yang sama, dan kasus utamanya adalah transaksi — topik Chapter berikutnya.

Empat tugas kini terlayani dua koneksi, dan kita pahami persis bagaimana pergantian itu terjadi. Dengan koneksi yang tinggal dipinjam dan dikembalikan, satu kebutuhan baru muncul: bagaimana menjamin serangkaian query dalam satu pinjaman berlaku sebagai satu kesatuan yang utuh? Jawabannya adalah transaksi — panggung Chapter berikutnya.