-
Notifications
You must be signed in to change notification settings - Fork 61
feat: add mysql-dual-conn E2E sample app #122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| version: '3.8' | ||
| services: | ||
| mysql: | ||
| image: mysql:8.0 | ||
| container_name: mysql-dual-conn | ||
| environment: | ||
| MYSQL_ROOT_PASSWORD: rootpass | ||
| ports: | ||
| - "3306:3306" | ||
| volumes: | ||
| - ./init.sql:/docker-entrypoint-initdb.d/init.sql |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| CREATE DATABASE IF NOT EXISTS myntra_oms; | ||
| CREATE DATABASE IF NOT EXISTS camunda; | ||
|
|
||
| CREATE USER IF NOT EXISTS 'omsAppUser'@'%' IDENTIFIED BY 'omsPassword'; | ||
| GRANT ALL PRIVILEGES ON myntra_oms.* TO 'omsAppUser'@'%'; | ||
|
|
||
| CREATE USER IF NOT EXISTS 'stagebuster'@'%' IDENTIFIED BY 'camundaPassword'; | ||
| GRANT ALL PRIVILEGES ON camunda.* TO 'stagebuster'@'%'; | ||
|
|
||
| FLUSH PRIVILEGES; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" | ||
| xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
|
|
||
| <parent> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-parent</artifactId> | ||
| <version>3.2.5</version> | ||
| <relativePath/> | ||
| </parent> | ||
|
|
||
| <groupId>com.example</groupId> | ||
| <artifactId>mysql-dual-conn</artifactId> | ||
| <version>0.0.1-SNAPSHOT</version> | ||
| <name>mysql-dual-conn</name> | ||
| <description>E2E test for Keploy MySQL multi-connection handshake matching</description> | ||
|
|
||
| <properties> | ||
| <java.version>17</java.version> | ||
| </properties> | ||
|
|
||
| <dependencies> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-web</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-starter-jdbc</artifactId> | ||
| </dependency> | ||
| <dependency> | ||
| <groupId>com.mysql</groupId> | ||
| <artifactId>mysql-connector-j</artifactId> | ||
| <scope>runtime</scope> | ||
| </dependency> | ||
| </dependencies> | ||
|
|
||
| <build> | ||
| <plugins> | ||
| <plugin> | ||
| <groupId>org.springframework.boot</groupId> | ||
| <artifactId>spring-boot-maven-plugin</artifactId> | ||
| </plugin> | ||
| </plugins> | ||
| </build> | ||
| </project> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package com.example.mysqlreplicate; | ||
|
|
||
| import com.zaxxer.hikari.HikariConfig; | ||
| import com.zaxxer.hikari.HikariDataSource; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.Primary; | ||
| import org.springframework.jdbc.core.JdbcTemplate; | ||
|
|
||
| import javax.sql.DataSource; | ||
|
|
||
| /** | ||
| * Replicates the multi-datasource setup that triggers the Keploy | ||
| * "no mysql mocks matched the HandshakeResponse41" error. | ||
| * | ||
| * Two HikariCP pools connect to the SAME MySQL server but with | ||
| * DIFFERENT usernames and databases. During Keploy test replay, | ||
| * each new TCP connection triggers simulateInitialHandshake which | ||
| * must match the client's HandshakeResponse41 against recorded | ||
| * config mocks. The mismatch occurs because: | ||
| * | ||
| * 1. mocks[0] (always omsAppUser/myntra_oms) is used to send the | ||
| * server greeting to ALL incoming connections. | ||
| * 2. When the camunda pool connects, its HandshakeResponse41 has | ||
| * username=stagebuster, database=camunda, and different | ||
| * capability_flags (423535119 vs 20881935). | ||
| * 3. The matcher loops all config mocks looking for a match on | ||
| * username + database + capability_flags + charset + filler. | ||
| * If no recorded config mock matches those exact fields, the | ||
| * error fires. | ||
| */ | ||
| @Configuration | ||
| public class DataSourceConfig { | ||
|
|
||
| // ---- OMS pool (matches omsAppUser / myntra_oms mocks) ---- | ||
|
|
||
| @Value("${datasource.oms.jdbc-url}") | ||
| private String omsJdbcUrl; | ||
|
|
||
| @Value("${datasource.oms.username}") | ||
| private String omsUsername; | ||
|
|
||
| @Value("${datasource.oms.password}") | ||
| private String omsPassword; | ||
|
|
||
| @Value("${datasource.oms.driver-class-name}") | ||
| private String omsDriverClass; | ||
|
|
||
| // ---- Camunda pool (matches stagebuster / camunda mocks) ---- | ||
|
|
||
| @Value("${datasource.camunda.jdbc-url}") | ||
| private String camundaJdbcUrl; | ||
|
|
||
| @Value("${datasource.camunda.username}") | ||
| private String camundaUsername; | ||
|
|
||
| @Value("${datasource.camunda.password}") | ||
| private String camundaPassword; | ||
|
|
||
| @Value("${datasource.camunda.driver-class-name}") | ||
| private String camundaDriverClass; | ||
|
|
||
| @Bean(name = "omsDataSource", destroyMethod = "close") | ||
| @Primary | ||
| public HikariDataSource omsDataSource() { | ||
| HikariConfig config = new HikariConfig(); | ||
| config.setPoolName("oms-dataSource"); | ||
| config.setUsername(omsUsername); | ||
| config.setPassword(omsPassword); | ||
| return buildDataSource(config, 5, omsJdbcUrl, omsDriverClass); | ||
| } | ||
|
|
||
| @Bean(name = "camundaDataSource", destroyMethod = "close") | ||
| public HikariDataSource camundaDataSource() { | ||
| HikariConfig config = new HikariConfig(); | ||
| config.setPoolName("camunda-dataSource"); | ||
| config.setUsername(camundaUsername); | ||
| config.setPassword(camundaPassword); | ||
| return buildDataSource(config, 5, camundaJdbcUrl, camundaDriverClass); | ||
| } | ||
|
|
||
| @Bean(name = "omsJdbcTemplate") | ||
| @Primary | ||
| public JdbcTemplate omsJdbcTemplate() { | ||
| return new JdbcTemplate(omsDataSource()); | ||
| } | ||
|
|
||
| @Bean(name = "camundaJdbcTemplate") | ||
| public JdbcTemplate camundaJdbcTemplate() { | ||
| return new JdbcTemplate(camundaDataSource()); | ||
| } | ||
|
|
||
| private HikariDataSource buildDataSource(HikariConfig config, int maxConns, | ||
| String jdbcUrl, String driverClass) { | ||
| config.setMaximumPoolSize(maxConns); | ||
| config.setMinimumIdle(2); | ||
| config.setKeepaliveTime(5000); | ||
| config.setIdleTimeout(10000); | ||
| config.setConnectionTimeout(5000); | ||
| config.setValidationTimeout(2000); | ||
| config.setMaxLifetime(7200000); | ||
| config.setLeakDetectionThreshold(2000); | ||
|
Comment on lines
+96
to
+103
|
||
| config.setDriverClassName(driverClass); | ||
| config.setJdbcUrl(jdbcUrl); | ||
| return new HikariDataSource(config); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package com.example.mysqlreplicate; | ||
|
|
||
| import org.springframework.boot.SpringApplication; | ||
| import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
|
|
||
| @SpringBootApplication | ||
| public class MysqlReplicateApplication { | ||
| public static void main(String[] args) { | ||
| SpringApplication.run(MysqlReplicateApplication.class, args); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package com.example.mysqlreplicate; | ||
|
|
||
| import org.springframework.beans.factory.annotation.Qualifier; | ||
| import org.springframework.jdbc.core.JdbcTemplate; | ||
| import org.springframework.web.bind.annotation.GetMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * Simple REST controller that queries both datasources. | ||
| * Each endpoint triggers a DB query that forces a connection | ||
| * from the respective pool — reproducing the multi-handshake | ||
| * scenario during Keploy replay. | ||
| */ | ||
| @RestController | ||
| public class QueryController { | ||
|
|
||
| private final JdbcTemplate omsJdbc; | ||
| private final JdbcTemplate camundaJdbc; | ||
|
|
||
| public QueryController(@Qualifier("omsJdbcTemplate") JdbcTemplate omsJdbc, | ||
| @Qualifier("camundaJdbcTemplate") JdbcTemplate camundaJdbc) { | ||
| this.omsJdbc = omsJdbc; | ||
| this.camundaJdbc = camundaJdbc; | ||
| } | ||
|
|
||
| /** | ||
| * Queries both databases, triggering connections from both pools. | ||
| * During Keploy test mode this forces two distinct HandshakeResponse41 | ||
| * packets with different username/database/capability_flags values. | ||
| */ | ||
| @GetMapping("/api/query-both") | ||
| public Map<String, Object> queryBoth() { | ||
| Map<String, Object> result = new HashMap<>(); | ||
|
|
||
| // OMS query — user=omsAppUser, db=myntra_oms | ||
| List<Map<String, Object>> omsResult = omsJdbc.queryForList("SELECT 1 AS oms_check"); | ||
| result.put("oms", omsResult); | ||
|
Comment on lines
+36
to
+41
|
||
|
|
||
| // Camunda query — user=stagebuster, db=camunda | ||
| List<Map<String, Object>> camundaResult = camundaJdbc.queryForList("SELECT 1 AS camunda_check"); | ||
| result.put("camunda", camundaResult); | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| @GetMapping("/api/oms") | ||
| public List<Map<String, Object>> queryOms() { | ||
| return omsJdbc.queryForList("SELECT 1 AS oms_check"); | ||
| } | ||
|
|
||
| @GetMapping("/api/camunda") | ||
| public List<Map<String, Object>> queryCamunda() { | ||
| return camundaJdbc.queryForList("SELECT 1 AS camunda_check"); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| server.port=8080 | ||
|
|
||
| # --- OMS DataSource (primary) --- | ||
| datasource.oms.jdbc-url=jdbc:mysql://localhost:3306/myntra_oms?useSSL=false | ||
| datasource.oms.username=omsAppUser | ||
| datasource.oms.password=omsPassword | ||
| datasource.oms.driver-class-name=com.mysql.cj.jdbc.Driver | ||
|
|
||
| # --- Camunda DataSource (secondary, different user & capability flags) --- | ||
| # allowPublicKeyRetrieval=true causes different MySQL capability flags, | ||
| # which is the key condition that triggers the multi-handshake matching bug. | ||
| datasource.camunda.jdbc-url=jdbc:mysql://localhost:3306/camunda?useSSL=false&allowPublicKeyRetrieval=true | ||
| datasource.camunda.username=stagebuster | ||
| datasource.camunda.password=camundaPassword | ||
| datasource.camunda.driver-class-name=com.mysql.cj.jdbc.Driver |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new sample module doesn't include a
README.mdwith run instructions (docker compose up, how to run the app, and which endpoints to hit). Other sample apps in this repo include per-module READMEs, and having one here will make the E2E scenario reproducible for contributors and CI debugging.