Skip to content

Commit 8a4717e

Browse files
committed
feat: add ps-cache-kotlin sample for connection-affinity mock matching
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>
1 parent be4a658 commit 8a4717e

7 files changed

Lines changed: 241 additions & 0 deletions

File tree

ps-cache-kotlin/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM maven:3.9-eclipse-temurin-21 AS builder
2+
WORKDIR /app
3+
COPY pom.xml .
4+
RUN mvn dependency:go-offline -q
5+
COPY src/ src/
6+
RUN mvn package -DskipTests -q
7+
8+
FROM eclipse-temurin:21-jre-alpine
9+
WORKDIR /app
10+
COPY --from=builder /app/target/kotlin-app-1.0.0.jar app.jar
11+
EXPOSE 8080
12+
CMD ["java", "-jar", "app.jar"]

ps-cache-kotlin/docker-compose.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
services:
2+
db:
3+
image: postgres:16-alpine
4+
environment:
5+
POSTGRES_USER: postgres
6+
POSTGRES_PASSWORD: postgres
7+
POSTGRES_DB: testdb
8+
ports:
9+
- "5433:5432"
10+
volumes:
11+
- pgdata:/var/lib/postgresql/data
12+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
13+
healthcheck:
14+
test: ["CMD-SHELL", "pg_isready -U postgres"]
15+
interval: 2s
16+
timeout: 5s
17+
retries: 5
18+
19+
api:
20+
build: .
21+
ports:
22+
- "8080:8080"
23+
environment:
24+
DB_HOST: db
25+
DB_PORT: "5432"
26+
DB_USER: postgres
27+
DB_PASSWORD: postgres
28+
DB_NAME: testdb
29+
depends_on:
30+
db:
31+
condition: service_healthy
32+
33+
volumes:
34+
pgdata:

ps-cache-kotlin/init.sql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
CREATE SCHEMA IF NOT EXISTS travelcard;
2+
3+
CREATE TABLE IF NOT EXISTS travelcard.travel_account (
4+
id SERIAL PRIMARY KEY,
5+
member_id INT NOT NULL UNIQUE,
6+
name TEXT NOT NULL,
7+
balance INT NOT NULL DEFAULT 0
8+
);
9+
10+
INSERT INTO travelcard.travel_account (member_id, name, balance) VALUES
11+
(19, 'Alice', 1000),
12+
(23, 'Bob', 2500),
13+
(31, 'Charlie', 500),
14+
(42, 'Diana', 7500)
15+
ON CONFLICT (member_id) DO NOTHING;

ps-cache-kotlin/pom.xml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>org.springframework.boot</groupId>
8+
<artifactId>spring-boot-starter-parent</artifactId>
9+
<version>3.4.4</version>
10+
</parent>
11+
<groupId>com.demo</groupId>
12+
<artifactId>kotlin-app</artifactId>
13+
<version>1.0.0</version>
14+
<properties>
15+
<java.version>21</java.version>
16+
<kotlin.version>1.9.25</kotlin.version>
17+
</properties>
18+
<dependencies>
19+
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
20+
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency>
21+
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
22+
<dependency><groupId>org.jetbrains.kotlin</groupId><artifactId>kotlin-reflect</artifactId></dependency>
23+
<dependency><groupId>org.jetbrains.kotlin</groupId><artifactId>kotlin-stdlib</artifactId></dependency>
24+
</dependencies>
25+
<build>
26+
<sourceDirectory>src/main/kotlin</sourceDirectory>
27+
<plugins>
28+
<plugin>
29+
<groupId>org.jetbrains.kotlin</groupId>
30+
<artifactId>kotlin-maven-plugin</artifactId>
31+
<version>${kotlin.version}</version>
32+
<configuration>
33+
<compilerPlugins><plugin>spring</plugin></compilerPlugins>
34+
<jvmTarget>${java.version}</jvmTarget>
35+
</configuration>
36+
<dependencies>
37+
<dependency><groupId>org.jetbrains.kotlin</groupId><artifactId>kotlin-maven-allopen</artifactId><version>${kotlin.version}</version></dependency>
38+
</dependencies>
39+
<executions><execution><id>compile</id><goals><goal>compile</goal></goals></execution></executions>
40+
</plugin>
41+
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin>
42+
</plugins>
43+
</build>
44+
</project>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.demo
2+
3+
import org.springframework.boot.autoconfigure.SpringBootApplication
4+
import org.springframework.boot.runApplication
5+
import org.springframework.jdbc.core.JdbcTemplate
6+
import org.springframework.web.bind.annotation.GetMapping
7+
import org.springframework.web.bind.annotation.RequestParam
8+
import org.springframework.web.bind.annotation.RestController
9+
import javax.sql.DataSource
10+
import com.zaxxer.hikari.HikariDataSource
11+
12+
@SpringBootApplication
13+
class App
14+
15+
fun main(args: Array<String>) {
16+
runApplication<App>(*args)
17+
}
18+
19+
data class Account(
20+
val id: Int,
21+
val memberId: Int,
22+
val name: String,
23+
val balance: Int
24+
)
25+
26+
@RestController
27+
class AccountController(private val jdbc: JdbcTemplate, private val dataSource: DataSource) {
28+
29+
@GetMapping("/health")
30+
fun health() = mapOf("status" to "ok")
31+
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+
*/
42+
@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()
54+
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)
64+
}
65+
66+
rs.close()
67+
ps.close()
68+
conn.commit()
69+
result
70+
} catch (e: Exception) {
71+
conn.rollback()
72+
throw e
73+
}
74+
}!!
75+
}
76+
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+
*/
82+
@GetMapping("/evict")
83+
fun evict(): Map<String, Any> {
84+
val hikari = dataSource as HikariDataSource
85+
hikari.hikariPoolMXBean?.softEvictConnections()
86+
// Also wait briefly for eviction
87+
Thread.sleep(200)
88+
return mapOf("evicted" to true, "active" to (hikari.hikariPoolMXBean?.activeConnections ?: 0),
89+
"idle" to (hikari.hikariPoolMXBean?.idleConnections ?: 0))
90+
}
91+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
server.port=8080
2+
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:5432}/${DB_NAME:testdb}?prepareThreshold=1&preparedStatementCacheQueries=256
3+
spring.datasource.username=${DB_USER:postgres}
4+
spring.datasource.password=${DB_PASSWORD:postgres}
5+
spring.datasource.hikari.maximum-pool-size=1
6+
spring.datasource.hikari.minimum-idle=1
7+
spring.sql.init.mode=never

ps-cache-kotlin/test.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
BASE_URL="http://localhost:8080"
5+
6+
echo "=== PS-Cache Mock Mismatch Test (Kotlin/JDBC) ==="
7+
8+
echo "--- Window 1: Connection A ---"
9+
echo " /account?member=19:"
10+
curl -s "$BASE_URL/account?member=19"
11+
echo ""
12+
sleep 0.3
13+
14+
echo " /account?member=23:"
15+
curl -s "$BASE_URL/account?member=23"
16+
echo ""
17+
sleep 0.3
18+
19+
echo ""
20+
echo "--- Evict (force new connection) ---"
21+
echo " /evict:"
22+
curl -s "$BASE_URL/evict"
23+
echo ""
24+
sleep 1
25+
26+
echo ""
27+
echo "--- Window 2: Connection B ---"
28+
echo " /account?member=31:"
29+
curl -s "$BASE_URL/account?member=31"
30+
echo ""
31+
sleep 0.3
32+
33+
echo " /account?member=42:"
34+
curl -s "$BASE_URL/account?member=42"
35+
echo ""
36+
37+
echo ""
38+
echo "=== Done ==="

0 commit comments

Comments
 (0)