@@ -6,6 +6,7 @@ import org.springframework.jdbc.core.JdbcTemplate
66import org.springframework.web.bind.annotation.GetMapping
77import org.springframework.web.bind.annotation.RequestParam
88import org.springframework.web.bind.annotation.RestController
9+ import org.springframework.http.ResponseEntity
910import javax.sql.DataSource
1011import com.zaxxer.hikari.HikariDataSource
1112
@@ -29,63 +30,60 @@ class AccountController(private val jdbc: JdbcTemplate, private val dataSource:
2930 @GetMapping(" /health" )
3031 fun health () = mapOf (" status" to " ok" )
3132
32- /* *
33- * /account?member=N queries the travel_account table.
34- *
35- * JDBC PS caching (prepareThreshold=1):
36- * - 1st call on a fresh connection: Parse(query="SELECT ...") + Bind + Describe + Execute
37- * - 2nd+ calls on same connection: Bind(ps="S_1") + Execute only (cached PS)
38- *
39- * The /evict endpoint forces HikariCP to evict all connections, so the
40- * NEXT /account call gets a fresh connection with cold PS cache.
41- */
4233 @GetMapping(" /account" )
43- fun getAccount (@RequestParam(" member" ) memberId : Int ): Any {
44- return jdbc.execute { conn: java.sql.Connection ->
45- conn.autoCommit = false
46- try {
47- val ps = conn.prepareStatement(
48- """ SELECT id, member_id, name, balance
49- FROM travelcard.travel_account
50- WHERE member_id = ?"""
51- )
52- ps.setInt(1 , memberId)
53- val rs = ps.executeQuery()
34+ fun getAccount (@RequestParam(" member" ) memberId : Int ): ResponseEntity <Any > {
35+ val result = jdbc.execute(
36+ org.springframework.jdbc.core.ConnectionCallback <Account ?> { conn ->
37+ conn.autoCommit = false
38+ try {
39+ conn.prepareStatement(
40+ """ SELECT id, member_id, name, balance
41+ FROM travelcard.travel_account
42+ WHERE member_id = ?"""
43+ ).use { ps ->
44+ ps.setInt(1 , memberId)
45+ ps.executeQuery().use { rs ->
46+ val account = if (rs.next()) {
47+ Account (
48+ id = rs.getInt(" id" ),
49+ memberId = rs.getInt(" member_id" ),
50+ name = rs.getString(" name" ),
51+ balance = rs.getInt(" balance" )
52+ )
53+ } else null
5454
55- val result = if (rs.next()) {
56- Account (
57- id = rs.getInt(" id" ),
58- memberId = rs.getInt(" member_id" ),
59- name = rs.getString(" name" ),
60- balance = rs.getInt(" balance" )
61- )
62- } else {
63- mapOf (" error" to " not found" , " member_id" to memberId)
55+ conn.commit()
56+ account
57+ }
58+ }
59+ } catch (e: Exception ) {
60+ conn.rollback()
61+ throw e
6462 }
63+ })
6564
66- rs.close()
67- ps.close()
68- conn.commit()
69- result
70- } catch (e: Exception ) {
71- conn.rollback()
72- throw e
73- }
74- }!!
65+ return if (result != null ) {
66+ ResponseEntity .ok(result)
67+ } else {
68+ ResponseEntity .status(404 ).body(mapOf (" error" to " not found" , " member_id" to memberId))
69+ }
7570 }
7671
77- /* *
78- * /evict forces HikariCP to evict all idle connections.
79- * Next request gets a FRESH PG connection → cold PS cache.
80- * This simulates what happens in production when connections cycle.
81- */
8272 @GetMapping(" /evict" )
83- fun evict (): Map <String , Any > {
84- val hikari = dataSource as HikariDataSource
85- hikari.hikariPoolMXBean?.softEvictConnections()
86- // Also wait briefly for eviction
73+ fun evict (): ResponseEntity <Map <String , Any >> {
74+ val hikari = dataSource as ? HikariDataSource
75+ ? : return ResponseEntity .status(500 ).body(mapOf (" error" to " not a HikariDataSource" ))
76+
77+ val mxBean = hikari.hikariPoolMXBean
78+ ? : return ResponseEntity .status(500 ).body(mapOf (" error" to " pool MXBean not available" ))
79+
80+ mxBean.softEvictConnections()
8781 Thread .sleep(200 )
88- return mapOf (" evicted" to true , " active" to (hikari.hikariPoolMXBean?.activeConnections ? : 0 ),
89- " idle" to (hikari.hikariPoolMXBean?.idleConnections ? : 0 ))
82+
83+ return ResponseEntity .ok(mapOf (
84+ " evicted" to true ,
85+ " active" to mxBean.activeConnections,
86+ " idle" to mxBean.idleConnections
87+ ))
9088 }
9189}
0 commit comments