Menjawab langsung masalah

Connection leak terjadi ketika aplikasi Go Fiber gagal mengembalikan koneksi ke pool SQLx setelah query gagal atau terjadi panic di middleware, sehingga akhirnya terlihat gejala timeout 504, antrian request panjang, dan log "connection pool is full". Fokus artikel ini adalah menunjukkan gejala nyata, data observability yang perlu diperiksa, akar masalah di middleware, dan langkah perbaikan konkret sampai verifikasi fix dan mitigasi otomatis.

Gejala nyata yang memicu debugging

  • Timeout 504 di gateway dan reverse proxy setelah traffic tinggi, meskipun database masih responsif saat diuji langsung.
  • Log database mencatat "connection pool is full" atau "timeout acquiring connection" secara berkala.
  • Antrian request Fiber menumpuk karena goroutine menunggu koneksi yang tidak dikembalikan.
  • Turunan Metrik seperti active connections terus meningkat sementara idle tetap nol.

Observability & metrics yang membantu diagnosis

Untuk mempersempit masalah connection leak, pantau metrik-metrik ini:

  • Active vs idle connection dari SQLx (biasanya sumber log driver atau ekspor via Prometheus).
  • Latency p95/p99 handler untuk melihat di mana timeout terjadi.
  • Jumlah goroutine atau Fiber queue depth menunjukkan blocking yang menunggu koneksi.
  • Trace request untuk melihat channel pipeline mana yang tidak menutup db.Rows atau tx.

Dengan data tersebut, Anda dapat membuktikan bahwa walau pool dikonfigurasi cukup besar, koneksi tetap habis karena tidak dikembalikan.

Root cause: pool SQLx habis saat rows/tx tidak ditutup

Go Fiber middleware yang memanggil SQLx secara langsung sering kali menangani error tanpa memastikan rows atau transaksi ditutup. Ini menyebabkan koneksi tetap terbuka sampai GC menutupnya, yang bisa sangat lambat di load tinggi.

Contoh middleware bermasalah

func handleRequest(c *fiber.Ctx) error {
    tx, err := db.Beginx(c.Context())
    if err != nil {
        return c.Status(500).SendString("gagal mulai transaksi")
    }

    rows, err := tx.QueryxContext(c.Context(), "SELECT ...")
    if err != nil {
        return c.Status(500).SendString("query gagal") // rows belum ditutup
    }

    for rows.Next() {
        var item Item
        rows.StructScan(&item)
        // proses
    }

    tx.Commit()
    return c.JSON(...) 
}

Jika QueryxContext mengembalikan error, rows bernilai nil dan tidak pernah secara eksplisit ditutup. Dalam kasus lain, panic sebelum rows.Close() atau tx.Commit() akan menyebabkan koneksi menunggu timeout.

Konfigurasi pool SQLx yang mendukung

db.SetMaxOpenConns(50)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(15 * time.Minute)

Walau konfigurasi di atas valid, pool tetap akan habis jika handler tidak mengembalikan koneksi. Perlu ada batasan yang konservatif agar tidak terlalu banyak koneksi terbuka bersamaan.

Langkah perbaikan konkret

  • Gunakan defer untuk menutup rows/tx: Selalu panggil defer rows.Close() segera setelah berhasil membuka hasil query. Sama halnya dengan defer tx.Rollback() untuk memastikan transaksi tidak menggantung.
  • Tangani error dengan memastikan defer dieksekusi: Misalnya, setelah rows, err := tx.QueryxContext(...), tulis if err != nil { return err } tapi sebelum itu, pastikan defer rows.Close() dipanggil.
  • Pisahkan logika middleware: Jika middleware memanggil beberapa query, pertimbangkan mengekstrak fungsi kecil agar defer tetap terbaca dan tidak terlewat.
  • Tambahkan health check database: Endpoint health check harus memanggil db.PingContext dan mengembalikan status degradasi jika tidak bisa membuka koneksi baru.
  • Monitoring alert: Buat alert ketika ratio active/MaxOpenConns mendekati 100% atau ketika request Fiber menunggu lebih dari timeout standar.

Patern yang lebih aman

func queryItems(ctx context.Context, tx *sqlx.Tx) ([]Item, error) {
    rows, err := tx.QueryxContext(ctx, "SELECT ...")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var items []Item
    for rows.Next() {
        var item Item
        if err := rows.StructScan(&item); err != nil {
            return nil, err
        }
        items = append(items, item)
    }
    return items, rows.Err()
}

func handleRequest(c *fiber.Ctx) error {
    tx, err := db.Beginx(c.Context())
    if err != nil {
        return fiber.NewError(fiber.StatusInternalServerError, "gagal mulai transaksi")
    }
    defer tx.Rollback()

    items, err := queryItems(c.Context(), tx)
    if err != nil {
        return err
    }

    if err := tx.Commit(); err != nil {
        return err
    }
    return c.JSON(items)
}

Dengan pola di atas, setiap fungsi bertanggung jawab menutup resource-nya sendiri, membuat connection leak lebih sulit terjadi.

Verifikasi perbaikan dan mitigasi otomatis

Setelah memperbaiki middleware, lakukan langkah-langkah berikut:

  1. Uji stres terbatas dengan beban simulasi dan pantau metrik connection pool untuk memastikan idle kembali normal setelah request selesai.
  2. Periksa log untuk memastikan tidak ada lagi pesan "connection pool is full" dan tidak ada goroutine menunggu terlalu lama.
  3. Tambahkan health check berkala yang memanggil db.PingContext dan melaporkan kegagalan ke monitoring system yang sama.
  4. Otomatis mitigasi: Buat alert yang memicu rollback otomatis atau circuit breaker ketika active connection melebihi threshold 90% dari MaxOpenConns, sehingga Anda dapat menolak request baru sementara pool pulih.

Dengan kombinasi pengecekan observability, penutupan resource yang konsisten, dan mekanisme mitigation otomatis, kejadian connection leak dapat dihindari atau dideteksi lebih awal sebelum memicu incident besar.