feat: add ps-cache-kotlin sample for connection-affinity testing#123
feat: add ps-cache-kotlin sample for connection-affinity testing#123slayerjain merged 4 commits intomainfrom
Conversation
Kotlin + Spring Boot + JDBC sample that demonstrates the PS-cache mock mismatch. Uses HikariCP max-pool-size=1, prepareThreshold=1, and a /evict endpoint to force connection cycling. The test records 4 /account requests across 2 connection windows. Without the affinity fix (keploy/integrations#121), test-5 returns Alice's data for Charlie's request. Signed-off-by: slayerjain <shubhamkjain@outlook.com>
There was a problem hiding this comment.
Pull request overview
Adds a new ps-cache-kotlin sample app (Kotlin + Spring Boot + JDBC/Postgres) intended to reproduce and validate PS-cache connection-affinity behavior across two connection “windows” (with an explicit pool eviction in between), supporting verification of keploy/integrations#121.
Changes:
- Introduces a Spring Boot Kotlin/JDBC API with
/account(member lookup) and/evict(Hikari soft-eviction) endpoints. - Adds Docker-based local environment (Postgres + API) plus seed SQL for deterministic data.
- Adds a shell script to exercise two request windows separated by eviction to detect mock mismatch regressions.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| ps-cache-kotlin/src/main/kotlin/com/demo/App.kt | Implements the API endpoints and connection-eviction behavior used to trigger PS-cache affinity scenarios. |
| ps-cache-kotlin/src/main/resources/application.properties | Configures Postgres JDBC URL + prepareThreshold, and Hikari pool sizing to enforce connection reuse. |
| ps-cache-kotlin/docker-compose.yml | Provides a runnable Postgres + API stack with healthchecks and env wiring. |
| ps-cache-kotlin/Dockerfile | Builds and runs the Spring Boot fat jar in a container. |
| ps-cache-kotlin/pom.xml | Maven build for Kotlin + Spring Boot web/jdbc + Postgres driver. |
| ps-cache-kotlin/init.sql | Seeds the schema/table and deterministic member records for the test scenario. |
| ps-cache-kotlin/test.sh | Drives the two-window request sequence (including /evict) to reproduce/verify the mismatch behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| curl -s "$BASE_URL/account?member=19" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=23:" | ||
| curl -s "$BASE_URL/account?member=23" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo "" | ||
| echo "--- Evict (force new connection) ---" | ||
| echo " /evict:" | ||
| curl -s "$BASE_URL/evict" | ||
| echo "" | ||
| sleep 1 | ||
|
|
||
| echo "" | ||
| echo "--- Window 2: Connection B ---" | ||
| echo " /account?member=31:" | ||
| curl -s "$BASE_URL/account?member=31" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=42:" | ||
| curl -s "$BASE_URL/account?member=42" |
There was a problem hiding this comment.
This uses curl -s, which will not fail the script on HTTP 4xx/5xx responses. For a regression test, use curl’s fail-on-error behavior (e.g., -f plus -S) so server errors cause a non-zero exit.
| curl -s "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -s "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -s "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -s "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -s "$BASE_URL/account?member=42" | |
| curl -fsS "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -fsS "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -fsS "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -fsS "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -fsS "$BASE_URL/account?member=42" |
| hikari.hikariPoolMXBean?.softEvictConnections() | ||
| // Also wait briefly for eviction | ||
| Thread.sleep(200) | ||
| return mapOf("evicted" to true, "active" to (hikari.hikariPoolMXBean?.activeConnections ?: 0), | ||
| "idle" to (hikari.hikariPoolMXBean?.idleConnections ?: 0)) |
There was a problem hiding this comment.
The endpoint reports evicted=true even if hikari.hikariPoolMXBean is null, which can yield a false-positive success response. Consider returning an error/evicted=false when the MXBean isn’t available, or ensure it’s non-null before responding.
| hikari.hikariPoolMXBean?.softEvictConnections() | |
| // Also wait briefly for eviction | |
| Thread.sleep(200) | |
| return mapOf("evicted" to true, "active" to (hikari.hikariPoolMXBean?.activeConnections ?: 0), | |
| "idle" to (hikari.hikariPoolMXBean?.idleConnections ?: 0)) | |
| val poolMxBean = hikari.hikariPoolMXBean | |
| ?: return mapOf( | |
| "evicted" to false, | |
| "error" to "Hikari pool MXBean is unavailable; verify Hikari management is enabled and retry the request." | |
| ) | |
| poolMxBean.softEvictConnections() | |
| // Also wait briefly for eviction | |
| Thread.sleep(200) | |
| return mapOf( | |
| "evicted" to true, | |
| "active" to poolMxBean.activeConnections, | |
| "idle" to poolMxBean.idleConnections | |
| ) |
| } else { | ||
| mapOf("error" to "not found", "member_id" to memberId) | ||
| } |
There was a problem hiding this comment.
/account returns an error map with HTTP 200 when the member isn’t found. If this is meant to behave like a real API (and to make failures obvious in tests), return a 404 status (e.g., via ResponseEntity.notFound()), or otherwise clearly signal the failure in the HTTP response code.
| curl -s "$BASE_URL/account?member=19" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=23:" | ||
| curl -s "$BASE_URL/account?member=23" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo "" | ||
| echo "--- Evict (force new connection) ---" | ||
| echo " /evict:" | ||
| curl -s "$BASE_URL/evict" | ||
| echo "" | ||
| sleep 1 | ||
|
|
||
| echo "" | ||
| echo "--- Window 2: Connection B ---" | ||
| echo " /account?member=31:" | ||
| curl -s "$BASE_URL/account?member=31" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=42:" | ||
| curl -s "$BASE_URL/account?member=42" |
There was a problem hiding this comment.
This uses curl -s, which will not fail the script on HTTP 4xx/5xx responses. For a regression test, use curl’s fail-on-error behavior (e.g., -f plus -S) so server errors cause a non-zero exit.
| curl -s "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -s "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -s "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -s "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -s "$BASE_URL/account?member=42" | |
| curl -fsS "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -fsS "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -fsS "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -fsS "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -fsS "$BASE_URL/account?member=42" |
| curl -s "$BASE_URL/account?member=19" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=23:" | ||
| curl -s "$BASE_URL/account?member=23" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo "" | ||
| echo "--- Evict (force new connection) ---" | ||
| echo " /evict:" | ||
| curl -s "$BASE_URL/evict" | ||
| echo "" | ||
| sleep 1 | ||
|
|
||
| echo "" | ||
| echo "--- Window 2: Connection B ---" | ||
| echo " /account?member=31:" | ||
| curl -s "$BASE_URL/account?member=31" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=42:" | ||
| curl -s "$BASE_URL/account?member=42" |
There was a problem hiding this comment.
This uses curl -s, which will not fail the script on HTTP 4xx/5xx responses. For a regression test, use curl’s fail-on-error behavior (e.g., -f plus -S) so server errors cause a non-zero exit.
| curl -s "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -s "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -s "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -s "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -s "$BASE_URL/account?member=42" | |
| curl -fsS "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -fsS "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -fsS "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -fsS "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -fsS "$BASE_URL/account?member=42" |
| @GetMapping("/evict") | ||
| fun evict(): Map<String, Any> { | ||
| val hikari = dataSource as HikariDataSource | ||
| hikari.hikariPoolMXBean?.softEvictConnections() | ||
| // Also wait briefly for eviction |
There was a problem hiding this comment.
/evict is an unauthenticated endpoint that churns the connection pool. If this sample is ever exposed beyond local/dev, it’s an easy DoS vector; consider guarding it behind a profile/property, requiring a shared-secret header, or clearly documenting it as dev-only.
| val ps = conn.prepareStatement( | ||
| """SELECT id, member_id, name, balance | ||
| FROM travelcard.travel_account | ||
| WHERE member_id = ?""" | ||
| ) | ||
| ps.setInt(1, memberId) | ||
| val rs = ps.executeQuery() | ||
|
|
||
| val result = if (rs.next()) { | ||
| Account( | ||
| id = rs.getInt("id"), | ||
| memberId = rs.getInt("member_id"), | ||
| name = rs.getString("name"), | ||
| balance = rs.getInt("balance") | ||
| ) | ||
| } else { | ||
| mapOf("error" to "not found", "member_id" to memberId) | ||
| } | ||
|
|
||
| rs.close() | ||
| ps.close() |
There was a problem hiding this comment.
If anything throws after prepareStatement/executeQuery, the ResultSet/PreparedStatement won’t be closed (they’re only closed on the happy path). Use Kotlin use {} (or a finally) around ps and rs so they are always closed before the connection is returned to the pool.
| val ps = conn.prepareStatement( | |
| """SELECT id, member_id, name, balance | |
| FROM travelcard.travel_account | |
| WHERE member_id = ?""" | |
| ) | |
| ps.setInt(1, memberId) | |
| val rs = ps.executeQuery() | |
| val result = if (rs.next()) { | |
| Account( | |
| id = rs.getInt("id"), | |
| memberId = rs.getInt("member_id"), | |
| name = rs.getString("name"), | |
| balance = rs.getInt("balance") | |
| ) | |
| } else { | |
| mapOf("error" to "not found", "member_id" to memberId) | |
| } | |
| rs.close() | |
| ps.close() | |
| val result = conn.prepareStatement( | |
| """SELECT id, member_id, name, balance | |
| FROM travelcard.travel_account | |
| WHERE member_id = ?""" | |
| ).use { ps -> | |
| ps.setInt(1, memberId) | |
| ps.executeQuery().use { rs -> | |
| if (rs.next()) { | |
| Account( | |
| id = rs.getInt("id"), | |
| memberId = rs.getInt("member_id"), | |
| name = rs.getString("name"), | |
| balance = rs.getInt("balance") | |
| ) | |
| } else { | |
| mapOf("error" to "not found", "member_id" to memberId) | |
| } | |
| } | |
| } |
| conn.rollback() | ||
| throw e | ||
| } | ||
| }!! |
There was a problem hiding this comment.
The non-null assertion on the result of jdbc.execute { ... } can throw an NPE at runtime (JdbcTemplate’s execute returns a nullable type). Prefer returning a non-null value without !! (e.g., use the generic execute<T>/ConnectionCallback<T> form or handle the null case explicitly).
| }!! | |
| } ?: throw IllegalStateException( | |
| "Database query returned no result; retry the request or verify the database connection state." | |
| ) |
| curl -s "$BASE_URL/account?member=19" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=23:" | ||
| curl -s "$BASE_URL/account?member=23" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo "" | ||
| echo "--- Evict (force new connection) ---" | ||
| echo " /evict:" | ||
| curl -s "$BASE_URL/evict" | ||
| echo "" | ||
| sleep 1 | ||
|
|
||
| echo "" | ||
| echo "--- Window 2: Connection B ---" | ||
| echo " /account?member=31:" | ||
| curl -s "$BASE_URL/account?member=31" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=42:" | ||
| curl -s "$BASE_URL/account?member=42" |
There was a problem hiding this comment.
This uses curl -s, which will not fail the script on HTTP 4xx/5xx responses. For a regression test, use curl’s fail-on-error behavior (e.g., -f plus -S) so server errors cause a non-zero exit.
| curl -s "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -s "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -s "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -s "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -s "$BASE_URL/account?member=42" | |
| curl -fsS "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -fsS "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -fsS "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -fsS "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -fsS "$BASE_URL/account?member=42" |
| curl -s "$BASE_URL/account?member=19" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=23:" | ||
| curl -s "$BASE_URL/account?member=23" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo "" | ||
| echo "--- Evict (force new connection) ---" | ||
| echo " /evict:" | ||
| curl -s "$BASE_URL/evict" | ||
| echo "" | ||
| sleep 1 | ||
|
|
||
| echo "" | ||
| echo "--- Window 2: Connection B ---" | ||
| echo " /account?member=31:" | ||
| curl -s "$BASE_URL/account?member=31" | ||
| echo "" | ||
| sleep 0.3 | ||
|
|
||
| echo " /account?member=42:" | ||
| curl -s "$BASE_URL/account?member=42" |
There was a problem hiding this comment.
This uses curl -s, which will not fail the script on HTTP 4xx/5xx responses. For a regression test, use curl’s fail-on-error behavior (e.g., -f plus -S) so server errors cause a non-zero exit.
| curl -s "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -s "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -s "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -s "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -s "$BASE_URL/account?member=42" | |
| curl -fsS "$BASE_URL/account?member=19" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=23:" | |
| curl -fsS "$BASE_URL/account?member=23" | |
| echo "" | |
| sleep 0.3 | |
| echo "" | |
| echo "--- Evict (force new connection) ---" | |
| echo " /evict:" | |
| curl -fsS "$BASE_URL/evict" | |
| echo "" | |
| sleep 1 | |
| echo "" | |
| echo "--- Window 2: Connection B ---" | |
| echo " /account?member=31:" | |
| curl -fsS "$BASE_URL/account?member=31" | |
| echo "" | |
| sleep 0.3 | |
| echo " /account?member=42:" | |
| curl -fsS "$BASE_URL/account?member=42" |
… curl -fSs
- Use Kotlin use{} blocks for PreparedStatement and ResultSet cleanup
- Return 404 ResponseEntity when member not found
- Handle null HikariPoolMXBean with proper error response
- Remove non-null assertion (!!) — return ResponseEntity directly
- Use curl -fSs in test.sh to fail on HTTP errors
Signed-off-by: slayerjain <shubhamkjain@outlook.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| sleep 0.3 | ||
|
|
There was a problem hiding this comment.
sleep 0.3 relies on non-POSIX fractional sleep support and can fail on some environments/shells. Consider using integer sleeps or an alternative portable delay mechanism so this script runs consistently across platforms.
| sleep 0.3 | ||
|
|
There was a problem hiding this comment.
sleep 0.3 relies on non-POSIX fractional sleep support and can fail on some environments/shells. Consider using integer sleeps or an alternative portable delay mechanism so this script runs consistently across platforms.
| @@ -0,0 +1,7 @@ | |||
| server.port=8080 | |||
| spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:testdb}?prepareThreshold=1&preparedStatementCacheQueries=256 | |||
There was a problem hiding this comment.
The DB port defaults to 5432 in application.properties, but docker-compose.yml exposes Postgres on host port 5433. If the intended workflow includes running the app locally against the compose DB, consider aligning the default (DB_PORT) or the compose port mapping to avoid connection failures without extra env vars.
| spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:testdb}?prepareThreshold=1&preparedStatementCacheQueries=256 | |
| spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5433}/${DB_NAME:testdb}?prepareThreshold=1&preparedStatementCacheQueries=256 |
| ?: return ResponseEntity.status(500).body(mapOf("error" to "pool MXBean not available")) | ||
|
|
||
| mxBean.softEvictConnections() | ||
| Thread.sleep(200) |
There was a problem hiding this comment.
/evict blocks the request thread with a fixed Thread.sleep(200), which adds latency and makes eviction timing dependent on a hard-coded delay. Prefer returning immediately (the caller already waits), or poll for the pool state with a bounded timeout if you need to ensure a new connection is established before responding.
| Thread.sleep(200) |
| sleep 0.3 | ||
|
|
There was a problem hiding this comment.
sleep 0.3 relies on non-POSIX fractional sleep support and can fail on some environments/shells. Consider using integer sleeps or an alternative portable delay mechanism so this script runs consistently across platforms.
Signed-off-by: slayerjain <shubhamkjain@outlook.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| POSTGRES_PASSWORD: postgres | ||
| POSTGRES_DB: testdb | ||
| ports: | ||
| - "5433:5432" |
There was a problem hiding this comment.
db is published on host port 5433, but the app’s default DB_PORT in application.properties is 5432. If someone runs Postgres via this compose file and runs the app locally (not in the api container), the default connection settings will point to the wrong port. Consider either mapping 5432:5432 here, or changing the default DB_PORT to 5433 (and documenting the intended run mode).
| - "5433:5432" | |
| - "5432:5432" |
|
|
||
| conn.commit() | ||
| account |
There was a problem hiding this comment.
conn.commit() is called while the ResultSet / PreparedStatement are still inside their use {} scopes. Some JDBC drivers can close cursors/result sets on commit, which can lead to surprising behavior. Consider moving the commit so it happens after the use blocks have completed (resources closed), and keeping rollback/cleanup centralized.
| @GetMapping("/evict") | ||
| fun evict(): ResponseEntity<Map<String, Any>> { |
There was a problem hiding this comment.
/evict changes server state (evicts pooled connections) but is exposed as a GET. For correctness with HTTP semantics (and to avoid accidental triggering by caches/prefetchers), consider making this a POST endpoint and updating test.sh accordingly.
| ?: return ResponseEntity.status(500).body(mapOf("error" to "not a HikariDataSource")) | ||
|
|
||
| val mxBean = hikari.hikariPoolMXBean | ||
| ?: return ResponseEntity.status(500).body(mapOf("error" to "pool MXBean not available")) | ||
|
|
||
| mxBean.softEvictConnections() | ||
| Thread.sleep(500) |
There was a problem hiding this comment.
Avoid Thread.sleep on the request thread; it blocks the server thread pool and makes response latency non-deterministic. If you need to wait for eviction to take effect for the test, consider polling the pool state with a bounded timeout (or just return immediately and let the next /account request establish the new connection).
| ?: return ResponseEntity.status(500).body(mapOf("error" to "not a HikariDataSource")) | |
| val mxBean = hikari.hikariPoolMXBean | |
| ?: return ResponseEntity.status(500).body(mapOf("error" to "pool MXBean not available")) | |
| mxBean.softEvictConnections() | |
| Thread.sleep(500) | |
| ?: return ResponseEntity.status(500).body( | |
| mapOf( | |
| "error" to "not a HikariDataSource", | |
| "next_step" to "Verify that the application is configured to use HikariCP before calling /evict." | |
| ) | |
| ) | |
| val mxBean = hikari.hikariPoolMXBean | |
| ?: return ResponseEntity.status(500).body( | |
| mapOf( | |
| "error" to "pool MXBean not available", | |
| "next_step" to "Enable HikariCP pool management or confirm the datasource exposes a pool MXBean, then retry /evict." | |
| ) | |
| ) | |
| mxBean.softEvictConnections() |
Signed-off-by: slayerjain <shubhamkjain@outlook.com>
Bug: JDBC PS-cache + connection eviction causes wrong mock data during replay
When HikariCP evicts a connection, the JDBC driver's prepared statement cache resets. The recorded mocks from the evicted connection have warm-cache structure (Bind-only) while replay sends cold-cache structure (Parse+Bind+Describe+Execute). Combined with
bindParamMatchLenmode treating all int4 params as equal (4 bytes), the matcher picks mocks from the wrong connection window -- returning Alice's financial data for Charlie's account.This was reported by a customer running a Kotlin/Spring Boot app. This sample reproduces the exact failure.
How to reproduce the failure
Expected failure: The post-eviction
/account?member=31test fails:With obfuscation enabled (worse): 2 failures instead of 1
See README.md for full details.
Fix
Recording-connection affinity in the Postgres mock matcher: keploy/integrations#121