Lonjakan latensi mendadak pada endpoint yang memanggil API eksternal sering kali berujung pada error fatal: java.sql.SQLTransientConnectionException: Connection is not available, request timed out after 30000ms. Masalah ini dikenal sebagai connection pool starvation pada HikariCP. Penyebab utamanya jarang berasal dari database lambat, melainkan perilaku default Spring Boot yang mengaktifkan Open Session In View (OSIV).
Gejala: ConnectionTimeoutException dan Antrean Request
Gejala starvation muncul di bawah beban trafik moderat ketika alur eksekusi request mencakup kombinasi query database dan I/O blocking (seperti REST client pihak ketiga atau payment gateway). Skenario tipikal:
- Aplikasi menerima HTTP request, lalu membaca data konfigurasi atau entitas user dari database.
- Aplikasi memanggil API pihak ketiga menggunakan
RestTemplate,RestClient, atau HTTP client blocking lainnya. - Pihak ketiga mengalami degradasi latensi (merespons dalam 3-5 detik).
- Metrik HikariCP mencatat
hikaricp.connections.activemenyentuh batas maksimum (default: 10), sementarahikaricp.connections.pendingmelonjak. - Request berikutnya gagal memperoleh koneksi database dan melempar exception berikut:
org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction
Caused by: org.hibernate.exception.JDBCConnectionException: Unable to acquire JDBC Connection
Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.
at com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:696)
at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:197)Root Cause: Siklus Hidup Koneksi pada Open-in-View (OSIV)
Secara default, Spring Boot mengaktifkan properti spring.jpa.open-in-view=true. Konfigurasi ini mendaftarkan OpenEntityManagerInViewInterceptor (atau Filter) ke dalam siklus hidup HTTP servlet.
Mekanisme OSIV:
- Bukan OSIV: Koneksi database dipinjam dari pool saat transaksi (
@Transactional) dimulai di Service layer, lalu langsung dikembalikan ke HikariCP begitu transaksi commit/rollback. - Dengan OSIV: Hibernate
Sessiondibuka di awal HTTP request. Begitu query pertama dieksekusi, satu koneksi JDBC fisik ditarik dari HikariCP. Koneksi ini tetap ditahan (held) sepanjang siklus HTTP request belum selesai, mencakup eksekusi controller, pemanggilan network I/O eksternal, hingga proses serialisasi response JSON.
Jika satu panggilan API pihak ketiga memakan waktu 4 detik, satu koneksi database fisik tersandera selama 4 detik penuh tanpa melakukan query aktif apapun. Dengan maximum-pool-size: 10, hanya dibutuhkan 10 concurrent request untuk membuat seluruh instance Spring Boot lumpuh.
Solusi 1: Nonaktifkan OSIV di application.yml
Langkah mitigasi pertama adalah memutus pengikatan EntityManager ke thread HTTP request. Matikan OSIV secara eksplisit:
spring:
jpa:
open-in-view: falseSetelah dimatikan, koneksi JDBC hanya akan dipinjam selama batas blok @Transactional berjalan. Ketika service selesai dan kode beralih ke external API call, koneksi fisik sudah kembali ke pool HikariCP dan siap dipakai thread lain.
Solusi 2: Tangani LazyInitializationException
Efek samping mematikan OSIV adalah munculnya LazyInitializationException jika Controller atau Jackson serializing engine mencoba mengakses relasi fetch = FetchType.LAZY di luar konteks transaksi. Selesaikan masalah ini di data access layer.
Opsi A: Gunakan JOIN FETCH pada JPQL
Jika seluruh entitas dibutuhkan, tarik relasi lazy secara eksplisit dalam satu query transaksi:
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("SELECT o FROM Order o JOIN FETCH o.orderItems WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
}Opsi B: DTO Projection (Direkomendasikan)
Hindari mengekspos JPA Entity langsung ke presentation layer. DTO projection mengeksekusi SELECT hanya untuk field yang dibutuhkan, memotong alokasi memori dan dependensi lazy loading.
public record OrderResponseDto(
Long id,
String orderNumber,
BigDecimal totalAmount
) {}
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT new com.example.dto.OrderResponseDto(o.id, o.orderNumber, o.totalAmount)
FROM Order o
WHERE o.id = :id
""")
Optional<OrderResponseDto> findDtoById(@Param("id") Long id);
}Solusi 3: Konfigurasi Leak Detection dan Metrik HikariCP
Untuk memvalidasi bahwa koneksi tidak tertahan melebihi durasi transaksi normal, aktifkan leak-detection-threshold pada konfigurasi HikariCP.
spring:
datasource:
hikari:
maximum-pool-size: 10
minimum-idle: 10
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 2000 # Log warning jika koneksi ditahan > 2 detikCatatan: Nilai
leak-detection-thresholdharus lebih tinggi dari estimasi query database terlama, namun jauh lebih rendah dariconnection-timeout. Nilai 2000ms hingga 5000ms ideal untuk aplikasi OLTP.
Jika ada thread yang menahan koneksi melebihi threshold, HikariCP mencatat stack trace tanpa mematikan koneksi secara paksa, memudahkan pinpointing baris kode bermasalah:
WARN 12104 --- [pool-thread-1] com.zaxxer.hikari.pool.ProxyLeakTask : Connection leak detection triggered for org.postgresql.jdbc.PgConnection@5e321e, stack trace follows
java.lang.Exception: Apparent connection leak detected
at com.example.service.OrderService.processOrder(OrderService.java:45)Monitoring via Micrometer
Ekspos metrik internal HikariCP via Spring Boot Actuator untuk observability pada Prometheus/Grafana:
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
enable:
hikaricp: truePantau metrik utama berikut pada dashboard sistem:
hikaricp.connections.active: Jumlah koneksi yang sedang dipakai thread.hikaricp.connections.pending: Jumlah thread yang antre menunggu koneksi (harus selalu mendekati 0).hikaricp.connections.idle: Koneksi menganggur yang tersedia di pool.hikaricp.connections.acquire.seconds: Latensi peminjaman koneksi dari pool.
Ringkasan Arsitektur
Pisahkan urutan eksekusi: hindari meletakkan pemanggilan I/O eksternal di dalam blok @Transactional. Ambil data database via Service (koneksi dipinjam, query jalan, transaksi commit, koneksi lepas), panggil API pihak ketiga di layer terpisah tanpa ikatan JDBC, lalu buka transaksi baru jika perlu menyimpan responsnya kembali ke database.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!