Menjawab Masalah Deadlock API Go Fiber secara Langsung

Sebuah API POST di Go Fiber tiba-tiba mulai timeout dan menumpuk request. Analisis awal menunjukkan error log berupa context deadline exceeded dan sql: database is closed, padahal kode handler tidak berubah. Gejala ini menunjukkan deadlock: transaksi tidak selesai karena koneksi tetap tertahan, sehingga pool habis dan request baru antre tanpa respons. Artikel ini membahas studi kasus nyata, langkah debugging praktis, dan solusi konkret untuk menghindari penguncian semacam ini.

Gejala Deadlock pada API Go Fiber

Gejala umum yang mendahului diagnosis deadlock adalah:

  • Timeout berkepanjangan di endpoint POST yang sebelumnya cepat, sementara GET normal.
  • Antrean request bertambah, terlihat dari histogram latency dan connection queue di load balancer.
  • Log error Fiber/PostgreSQL mencatat pesan context deadline exceeded, net/http: request canceled, atau database/sql: connection was null.
  • Pool database terlihat penuh di monitoring: jumlah koneksi aktif mencapai batas maksimal, tetapi tidak ada aktivitas transaksi baru.

Pola ini mengarahkan tim ke dua penyebab utama: pemakaian mutex yang tidak dilepas dan pool database yang terus menunggu koneksi karena transaksi lama belum selesai.

Studi Kasus Bug Nyata: API POST Terjebak Deadlock

Sebuah aplikasi e-commerce menggunakan handler Go Fiber untuk menyimpan pesanan pengguna. Handler menjalankan beberapa query dalam satu transaksi dan menulis audit log dengan mutex global untuk menjaga urutan logging.

Kode asli terlihat seperti ini:

var auditMutex sync.Mutex

func createOrder(c *fiber.Ctx) error {
    auditMutex.Lock()
    defer auditMutex.Unlock()

    tx, err := db.BeginTx(c.Context(), &sql.TxOptions{Isolation: sql.LevelSerializable})
    if err != nil {
        return err
    }

    if _, err := tx.ExecContext(c.Context(), "INSERT INTO orders ..."); err != nil {
        tx.Rollback()
        return err
    }

    time.Sleep(3 * time.Second) // simulasi penulisan log lambat

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

    return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "ok"})
}

Dalam situasi request paralel tinggi, mutex global membuat handler berjalan serial, sementara tiap handler menunggu eksekusi log hingga selesai. Transaksi tetap terbuka dan koneksi pool menunggu, membuat jumlah koneksi aktif meningkat hingga mencapai MaxOpenConns dan request baru auto-timeout.

Root Cause

  • Pool database habis karena transaksi tertahan selama mutex serialisasi.
  • Mutex/blocking tidak dilepas saat handler mengalami panic atau delay, membuat handler lain menunggu lebih lama.
  • Request paralel tinggi dengan handler yang memblokir resouce intensif.

Langkah Debugging Praktis

1. Profiling dengan pprof

Aktifkan pprof pada Fiber (misalnya melalui pprofhandler) kemudian jalankan go tool pprof http://localhost:6060/debug/pprof/block untuk melihat goroutine yang sedang menunggu mutex atau koneksi database. Ini memudahkan menemukan handler yang menghalangi.

2. Tracing Request

Gunakan middleware tracing (OpenTelemetry, Jaeger) agar setiap request punya trace ID. Lacak trace pada waktu timeout: jika ada span transaksi SQL yang berlangsung terlalu lama, itu pertanda kuat transaksi tidak selesai.

3. EXPLAIN Query

Gunakan EXPLAIN ANALYZE pada query yang digunakan. Meski bukan penyebab utama deadlock di tingkat application lock, query kompleks bisa memicu latch di database sehingga transaksi menahan koneksi lebih lama dari perkiraan.

4. Monitoring Pool

Pasang metrik DBStats dari database/sql untuk menampilkan MaxOpenConns, InUse, dan Idle. Ketika InUse selalu sama dengan MaxOpenConns, berarti pool tidak memiliki buffer untuk permintaan baru.

Solusi dan Pencegahan

1. Setel Konfigurasi Pool yang Realistis

Sesuaikan db.SetMaxOpenConns dan db.SetConnMaxLifetime dengan beban. Contoh:

db.SetMaxOpenConns(40)
db.SetMaxIdleConns(20)
db.SetConnMaxLifetime(5 * time.Minute)

Nilai ini memastikan ada ruang untuk request burst, sementara koneksi lama tidak menumpuk. Jika pool masih penuh saat load tinggi, tambahkan circuit breaker di layer handler untuk menolak request baru hingga koneksi tersedia.

2. Refactor Locking

Kurangi ruang lingkup mutex dengan memisahkan penulisan log ke goroutine terpisah atau gunakan channel buffer. Pastikan mutex tidak menutup transaksi.

func createOrder(c *fiber.Ctx) error {
    tx, err := db.BeginTx(c.Context(), nil)
    if err != nil {
        return err
    }

    if err := insertOrder(tx, c.Body()); err != nil {
        tx.Rollback()
        return err
    }

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

    go publishAuditLog(c.Context(), logData)
    return c.Status(fiber.StatusCreated).JSON(...)
}

Dengan memisahkan audit log ke goroutine, mai handler cepat selesai dan koneksi dilepas lebih awal. Gunakan context.WithTimeout jika goroutine memerlukan akses database tambahan.

3. Terapkan Timeout dan Fallback

Gunakan context timeout pada setiap operasi database agar transaksi tidak menggantung terlalu lama.

ctx, cancel := context.WithTimeout(c.Context(), 5*time.Second)
defer cancel()

if err := tx.QueryRowContext(ctx, ...).Scan(...); err != nil {
    tx.Rollback()
    return err
}

Jika database tidak responsif, kembalikan error terkontrol ke klien dan gunakan strategi retry/fallback di sisi konsumen atau queue pekerjaan untuk diproses nanti.

4. Batasi Paralelisme dengan Worker Pool

Gunakan limiter seperti semaphore untuk membatasi jumlah handler yang mengakses resource sensitif secara bersamaan.

Verifikasi Setelah Perbaikan

Lakukan tes beban terkontrol (misalnya vegeta/hey) untuk memastikan latency tetap stabil dan pool tidak penuh. Perhatikan log Fiber untuk memastikan penyebab deadlock hilang. Pastikan metrik DB menunjukkan idle connection naik saat load berkurang.

Pencegahan Serupa

Untuk mencegah kejadian serupa:

  • Selalu pairing transaksi panjang dengan timeout context.
  • Hindari mutex global pada handler HTTP; gunakan struktur data channel atau queue terpisah.
  • Monitoring pool database secara real-time dengan alert ketika InUse mendekati MaxOpenConns.
  • Selalu tes skenario load tinggi setelah perubahan yang menyangkut locking atau transaksi.

Dengan pendekatan ini, deadlock API POST di Go Fiber dapat dideteksi lebih awal, solusi tepat waktu diaplikasikan, dan sistem tetap responsif saat menghadapi peningkatan request.