Kebocoran koneksi database (connection handle leak) sering kali berujung pada connection pool starvation, kondisi ketika seluruh worker pool habis terpakai dan aplikasi berhenti merespons kueri baru. Pada kode asinkron lawas berbasis Continuation-Passing Style (CPS) atau callback bertingkat, penanganan siklus hidup resource transaksi rawan terlewat. Kegagalan memanggil cabang rollback pada jalur penanganan error (error continuation) menyebabkan handle transaksi menggantung di memory pool tanpa pernah dilepas.

Akar Masalah: Continuation-Passing Style dan Pool Starvation

Pada pola CPS, alur kendali diteruskan secara eksplisit ke fungsi berikutnya via argumen, bukan melalui mekanisme return atau throw bawaan bahasa. Ketika instance koneksi atau transaksi database (trx) diteruskan melalui rantai callback tanpa mekanisme finalizer otomatis seperti blok finally atau disposable pattern, developer bertanggung jawab menutup handle tersebut di setiap kemungkinan cabang eksekusi.

Perhatikan contoh implementasi CPS yang rentan berikut:

// Rentan: Jalur error callback tidak memanggil rollback
function processOrder(orderId, db, done) {
  db.beginTransaction((err, trx) => {
    if (err) return done(err);

    fetchOrderData(orderId, trx, (errFetch, order) => {
      if (errFetch) {
        // BUG: trx tidak ditutup dengan rollback
        return done(errFetch);
      }

      trx.commit((errCommit) => {
        if (errCommit) {
          trx.rollback(() => done(errCommit));
          return;
        }
        done(null, order);
      });
    });
  });
}

Solusi refaktor paling aman pada level kode aplikasi adalah membungkus resource dalam block finalizer atau promise resolver dengan penanganan error terpusat:

// Standar perbaikan: Memastikan rollback dipanggil pada error continuation
function processOrderSafe(orderId, db, done) {
  db.beginTransaction((err, trx) => {
    if (err) return done(err);

    fetchOrderData(orderId, trx, (errFetch, order) => {
      if (errFetch) {
        trx.rollback(() => done(errFetch));
        return;
      }

      trx.commit((errCommit) => {
        if (errCommit) {
          trx.rollback(() => done(errCommit));
          return;
        }
        done(null, order);
      });
    });
  });
}

Membuat Custom Rule AST Linting

Mencegah regresi manual dapat diotomasi melalui Abstract Syntax Tree (AST) linting. Menggunakan ESLint rule builder, kita dapat menavigasi struktur pohon kode untuk memverifikasi apakah pemanggilan beginTransaction selalu memuat pasangan commit dan rollback pada callback body-nya.

Berikut adalah implementasi rule ESLint minimalis (rules/detect-cps-trx-leak.js):

// rules/detect-cps-trx-leak.js
module.exports = {
  meta: {
    type: "problem",
    docs: {
      description: "Mendeteksi unhandled transaction handle pada alur CPS",
    },
    schema: [],
    messages: {
      missingRollback: "Handle transaksi '{{ name }}' tidak memanggil rollback pada seluruh jalur callback.",
    },
  },
  create(context) {
    return {
      CallExpression(node) {
        // Target: db.beginTransaction((err, trx) => ...)
        if (
          node.callee.type === "MemberExpression" &&
          node.callee.property.name === "beginTransaction" &&
          node.arguments.length > 0
        ) {
          const callback = node.arguments[0];
          if (!callback || !callback.params || callback.params.length < 2) return;

          const trxParamName = callback.params[1].name;
          let hasCommit = false;
          let hasRollback = false;

          // Traversal manual pada body callback
          const sourceCode = context.getSourceCode();
          const tokens = sourceCode.getTokens(callback.body);

          for (let i = 0; i < tokens.length - 2; i++) {
            if (tokens[i].value === trxParamName && tokens[i + 1].value === ".") {
              if (tokens[i + 2].value === "commit") hasCommit = true;
              if (tokens[i + 2].value === "rollback") hasRollback = true;
            }
          }

          if (hasCommit && !hasRollback) {
            context.report({
              node: callback,
              messageId: "missingRollback",
              data: { name: trxParamName },
            });
          }
        },
      };
    },
  };
};

Integrasi pada CI Pipeline (Pull Request)

Gunakan rule custom tersebut dalam integrasi Continuous Integration (CI) agar eksekusi kode yang rentan ditolak sebelum masuk ke main branch.

Konfigurasi .eslintrc.js:

// .eslintrc.js
module.exports = {
  plugins: ["local-rules"],
  rules: {
    "local-rules/detect-cps-trx-leak": "error",
  },
};

Snippet konfigurasi pipeline GitHub Actions (.github/workflows/lint.yml):

name: Lint PR Codebase

on:
  pull_request:
    branches: [main, master]

jobs:
  ast-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - run: npx eslint . --ext .js --max-warnings 0

Limitasi Analisis Statis dan Alternatif Arsitektur

Analisis AST statis berbasis token atau tree-matching sederhana memiliki limitasi dalam memetakan cross-function scope. Jika handle trx dioper ke fungsi helper terpisah di file lain, rule AST lokal akan memicu false positive karena pemanggilan rollback berada di luar subtree callback.

Alternatif yang lebih lazim untuk kode modern adalah meninggalkan CPS dan beralih ke async/await menggunakan resource wrapper (Scoped Transaction Manager):

// Alternatif modern: Scoped context runner
await db.transaction(async (trx) => {
  const order = await fetchOrderData(orderId, trx);
  return order;
}); // Otomatis commit jika resolve, rollback jika throw

Pendekatan AST linter cocok dipasang pada codebase lama (legacy) yang proses refactoring ke promise-nya bertahap, memastikan stabilitas connection pool tetap terjaga selama migrasi berlangsung.