Skip to content

Commit bd64f05

Browse files
authored
feat: add mysql-dual-conn E2E sample app (#122)
Spring Boot app with two HikariCP connection pools using different MySQL credentials and JDBC URL parameters. Tests that Keploy correctly matches HandshakeResponse41 packets during replay when capability flags differ between connections.
1 parent 85cec14 commit bd64f05

File tree

7 files changed

+262
-0
lines changed

7 files changed

+262
-0
lines changed

mysql-dual-conn/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
version: '3.8'
2+
services:
3+
mysql:
4+
image: mysql:8.0
5+
container_name: mysql-dual-conn
6+
environment:
7+
MYSQL_ROOT_PASSWORD: rootpass
8+
ports:
9+
- "3306:3306"
10+
volumes:
11+
- ./init.sql:/docker-entrypoint-initdb.d/init.sql

mysql-dual-conn/init.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
CREATE DATABASE IF NOT EXISTS myntra_oms;
2+
CREATE DATABASE IF NOT EXISTS camunda;
3+
4+
CREATE USER IF NOT EXISTS 'omsAppUser'@'%' IDENTIFIED BY 'omsPassword';
5+
GRANT ALL PRIVILEGES ON myntra_oms.* TO 'omsAppUser'@'%';
6+
7+
CREATE USER IF NOT EXISTS 'stagebuster'@'%' IDENTIFIED BY 'camundaPassword';
8+
GRANT ALL PRIVILEGES ON camunda.* TO 'stagebuster'@'%';
9+
10+
FLUSH PRIVILEGES;

mysql-dual-conn/pom.xml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<parent>
8+
<groupId>org.springframework.boot</groupId>
9+
<artifactId>spring-boot-starter-parent</artifactId>
10+
<version>3.2.5</version>
11+
<relativePath/>
12+
</parent>
13+
14+
<groupId>com.example</groupId>
15+
<artifactId>mysql-dual-conn</artifactId>
16+
<version>0.0.1-SNAPSHOT</version>
17+
<name>mysql-dual-conn</name>
18+
<description>E2E test for Keploy MySQL multi-connection handshake matching</description>
19+
20+
<properties>
21+
<java.version>17</java.version>
22+
</properties>
23+
24+
<dependencies>
25+
<dependency>
26+
<groupId>org.springframework.boot</groupId>
27+
<artifactId>spring-boot-starter-web</artifactId>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.springframework.boot</groupId>
31+
<artifactId>spring-boot-starter-jdbc</artifactId>
32+
</dependency>
33+
<dependency>
34+
<groupId>com.mysql</groupId>
35+
<artifactId>mysql-connector-j</artifactId>
36+
<scope>runtime</scope>
37+
</dependency>
38+
</dependencies>
39+
40+
<build>
41+
<plugins>
42+
<plugin>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot-maven-plugin</artifactId>
45+
</plugin>
46+
</plugins>
47+
</build>
48+
</project>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.example.mysqlreplicate;
2+
3+
import com.zaxxer.hikari.HikariConfig;
4+
import com.zaxxer.hikari.HikariDataSource;
5+
import org.springframework.beans.factory.annotation.Value;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
import org.springframework.context.annotation.Primary;
9+
import org.springframework.jdbc.core.JdbcTemplate;
10+
11+
import javax.sql.DataSource;
12+
13+
/**
14+
* Replicates the multi-datasource setup that triggers the Keploy
15+
* "no mysql mocks matched the HandshakeResponse41" error.
16+
*
17+
* Two HikariCP pools connect to the SAME MySQL server but with
18+
* DIFFERENT usernames and databases. During Keploy test replay,
19+
* each new TCP connection triggers simulateInitialHandshake which
20+
* must match the client's HandshakeResponse41 against recorded
21+
* config mocks. The mismatch occurs because:
22+
*
23+
* 1. mocks[0] (always omsAppUser/myntra_oms) is used to send the
24+
* server greeting to ALL incoming connections.
25+
* 2. When the camunda pool connects, its HandshakeResponse41 has
26+
* username=stagebuster, database=camunda, and different
27+
* capability_flags (423535119 vs 20881935).
28+
* 3. The matcher loops all config mocks looking for a match on
29+
* username + database + capability_flags + charset + filler.
30+
* If no recorded config mock matches those exact fields, the
31+
* error fires.
32+
*/
33+
@Configuration
34+
public class DataSourceConfig {
35+
36+
// ---- OMS pool (matches omsAppUser / myntra_oms mocks) ----
37+
38+
@Value("${datasource.oms.jdbc-url}")
39+
private String omsJdbcUrl;
40+
41+
@Value("${datasource.oms.username}")
42+
private String omsUsername;
43+
44+
@Value("${datasource.oms.password}")
45+
private String omsPassword;
46+
47+
@Value("${datasource.oms.driver-class-name}")
48+
private String omsDriverClass;
49+
50+
// ---- Camunda pool (matches stagebuster / camunda mocks) ----
51+
52+
@Value("${datasource.camunda.jdbc-url}")
53+
private String camundaJdbcUrl;
54+
55+
@Value("${datasource.camunda.username}")
56+
private String camundaUsername;
57+
58+
@Value("${datasource.camunda.password}")
59+
private String camundaPassword;
60+
61+
@Value("${datasource.camunda.driver-class-name}")
62+
private String camundaDriverClass;
63+
64+
@Bean(name = "omsDataSource", destroyMethod = "close")
65+
@Primary
66+
public HikariDataSource omsDataSource() {
67+
HikariConfig config = new HikariConfig();
68+
config.setPoolName("oms-dataSource");
69+
config.setUsername(omsUsername);
70+
config.setPassword(omsPassword);
71+
return buildDataSource(config, 5, omsJdbcUrl, omsDriverClass);
72+
}
73+
74+
@Bean(name = "camundaDataSource", destroyMethod = "close")
75+
public HikariDataSource camundaDataSource() {
76+
HikariConfig config = new HikariConfig();
77+
config.setPoolName("camunda-dataSource");
78+
config.setUsername(camundaUsername);
79+
config.setPassword(camundaPassword);
80+
return buildDataSource(config, 5, camundaJdbcUrl, camundaDriverClass);
81+
}
82+
83+
@Bean(name = "omsJdbcTemplate")
84+
@Primary
85+
public JdbcTemplate omsJdbcTemplate() {
86+
return new JdbcTemplate(omsDataSource());
87+
}
88+
89+
@Bean(name = "camundaJdbcTemplate")
90+
public JdbcTemplate camundaJdbcTemplate() {
91+
return new JdbcTemplate(camundaDataSource());
92+
}
93+
94+
private HikariDataSource buildDataSource(HikariConfig config, int maxConns,
95+
String jdbcUrl, String driverClass) {
96+
config.setMaximumPoolSize(maxConns);
97+
config.setMinimumIdle(2);
98+
config.setKeepaliveTime(5000);
99+
config.setIdleTimeout(10000);
100+
config.setConnectionTimeout(5000);
101+
config.setValidationTimeout(2000);
102+
config.setMaxLifetime(7200000);
103+
config.setLeakDetectionThreshold(2000);
104+
config.setDriverClassName(driverClass);
105+
config.setJdbcUrl(jdbcUrl);
106+
return new HikariDataSource(config);
107+
}
108+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.example.mysqlreplicate;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class MysqlReplicateApplication {
8+
public static void main(String[] args) {
9+
SpringApplication.run(MysqlReplicateApplication.class, args);
10+
}
11+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.example.mysqlreplicate;
2+
3+
import org.springframework.beans.factory.annotation.Qualifier;
4+
import org.springframework.jdbc.core.JdbcTemplate;
5+
import org.springframework.web.bind.annotation.GetMapping;
6+
import org.springframework.web.bind.annotation.RestController;
7+
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
/**
13+
* Simple REST controller that queries both datasources.
14+
* Each endpoint triggers a DB query that forces a connection
15+
* from the respective pool — reproducing the multi-handshake
16+
* scenario during Keploy replay.
17+
*/
18+
@RestController
19+
public class QueryController {
20+
21+
private final JdbcTemplate omsJdbc;
22+
private final JdbcTemplate camundaJdbc;
23+
24+
public QueryController(@Qualifier("omsJdbcTemplate") JdbcTemplate omsJdbc,
25+
@Qualifier("camundaJdbcTemplate") JdbcTemplate camundaJdbc) {
26+
this.omsJdbc = omsJdbc;
27+
this.camundaJdbc = camundaJdbc;
28+
}
29+
30+
/**
31+
* Queries both databases, triggering connections from both pools.
32+
* During Keploy test mode this forces two distinct HandshakeResponse41
33+
* packets with different username/database/capability_flags values.
34+
*/
35+
@GetMapping("/api/query-both")
36+
public Map<String, Object> queryBoth() {
37+
Map<String, Object> result = new HashMap<>();
38+
39+
// OMS query — user=omsAppUser, db=myntra_oms
40+
List<Map<String, Object>> omsResult = omsJdbc.queryForList("SELECT 1 AS oms_check");
41+
result.put("oms", omsResult);
42+
43+
// Camunda query — user=stagebuster, db=camunda
44+
List<Map<String, Object>> camundaResult = camundaJdbc.queryForList("SELECT 1 AS camunda_check");
45+
result.put("camunda", camundaResult);
46+
47+
return result;
48+
}
49+
50+
@GetMapping("/api/oms")
51+
public List<Map<String, Object>> queryOms() {
52+
return omsJdbc.queryForList("SELECT 1 AS oms_check");
53+
}
54+
55+
@GetMapping("/api/camunda")
56+
public List<Map<String, Object>> queryCamunda() {
57+
return camundaJdbc.queryForList("SELECT 1 AS camunda_check");
58+
}
59+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
server.port=8080
2+
3+
# --- OMS DataSource (primary) ---
4+
datasource.oms.jdbc-url=jdbc:mysql://localhost:3306/myntra_oms?useSSL=false
5+
datasource.oms.username=omsAppUser
6+
datasource.oms.password=omsPassword
7+
datasource.oms.driver-class-name=com.mysql.cj.jdbc.Driver
8+
9+
# --- Camunda DataSource (secondary, different user & capability flags) ---
10+
# allowPublicKeyRetrieval=true causes different MySQL capability flags,
11+
# which is the key condition that triggers the multi-handshake matching bug.
12+
datasource.camunda.jdbc-url=jdbc:mysql://localhost:3306/camunda?useSSL=false&allowPublicKeyRetrieval=true
13+
datasource.camunda.username=stagebuster
14+
datasource.camunda.password=camundaPassword
15+
datasource.camunda.driver-class-name=com.mysql.cj.jdbc.Driver

0 commit comments

Comments
 (0)