Describe the bug
When two DataSource beans exist and one delegates to the other, a single query is counted twice. LazyConnectionDataSourceProxy is a common way to end up in that setup.
QuickPerfProxyBeanPostProcessor wraps any bean of type DataSource and has no guard against one whose queries are already recorded:
if (bean instanceof DataSource && !ScopedProxyUtils.isScopedTarget(beanName)) {
final ProxyFactory factory = new ProxyFactory(bean);
factory.setProxyTargetClass(true);
factory.addAdvice(new ProxyDataSourceInterceptor((DataSource) bean));
return factory.getProxy();
}
The inner DataSource bean is created and wrapped first. The outer bean then receives the already wrapped inner one as its delegate, and gets wrapped as well because it is a DataSource too. The query is recorded once by the outer proxy and once by the inner one.
Expected behavior
One select statement is counted once and @ExpectSelect(1) passes.
Actual behavior
It is counted twice:
java.lang.AssertionError: a performance-related property is not respected
[PERF] You may think that <1> select statement was sent to the database
But there are in fact <2>...
[JDBC QUERY EXECUTION (executeQuery, executeBatch, ...)]
Time:3, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["select count(*) from member"], Params:[]
Time:4, Success:True, Type:Statement, Batch:False, QuerySize:1, BatchSize:0, Query:["select count(*) from member"], Params:[]
To Reproduce
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App {
@Bean
public DataSource realDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.generateUniqueName(true)
.build();
}
@Bean
@Primary
public DataSource lazyDataSource(DataSource realDataSource) {
return new LazyConnectionDataSourceProxy(realDataSource);
}
}
@SpringBootTest(classes = App.class)
@Import(QuickPerfSqlConfig.class)
@QuickPerfTest
class DoubleWrapTest {
@Autowired
JdbcTemplate jdbcTemplate;
@BeforeEach
void initSchema() {
jdbcTemplate.execute("DROP TABLE IF EXISTS member");
jdbcTemplate.execute("CREATE TABLE member (id INT PRIMARY KEY, name VARCHAR(100))");
}
@Test
@ExpectSelect(1)
void oneSelectShouldBeCountedOnce() {
jdbcTemplate.queryForObject("select count(*) from member", Integer.class);
}
}
The same test passes with one recorded statement on an application that declares a single DataSource bean.
Versions
- QuickPerf: reproduced on both 1.1.0 and master (
3b96d125dacf06b74849cca62df9d60871a86464)
- JDK: 17.0.20 (Temurin)
- OS: macOS
- Database: H2
- Spring Boot: 2.7.18 and 3.2.5 (same result on both)
Additional context
The count silently doubles, and the failure message gives no hint about the DataSource setup.
I have this fixed on another datasource-proxy based project: attach a marker interface to the proxy when wrapping, then walk the delegation chain before wrapping and skip a DataSource that already carries the marker. Two things need care. AbstractRoutingDataSource is not a DelegatingDataSource, and a skip condition that is too broad silently drops a second, independent DataSource.
Happy to open a pull request with tests if this direction works for you.
Describe the bug
When two DataSource beans exist and one delegates to the other, a single query is counted twice.
LazyConnectionDataSourceProxyis a common way to end up in that setup.QuickPerfProxyBeanPostProcessorwraps any bean of typeDataSourceand has no guard against one whose queries are already recorded:The inner DataSource bean is created and wrapped first. The outer bean then receives the already wrapped inner one as its delegate, and gets wrapped as well because it is a
DataSourcetoo. The query is recorded once by the outer proxy and once by the inner one.Expected behavior
One select statement is counted once and
@ExpectSelect(1)passes.Actual behavior
It is counted twice:
To Reproduce
The same test passes with one recorded statement on an application that declares a single DataSource bean.
Versions
3b96d125dacf06b74849cca62df9d60871a86464)Additional context
The count silently doubles, and the failure message gives no hint about the DataSource setup.
I have this fixed on another datasource-proxy based project: attach a marker interface to the proxy when wrapping, then walk the delegation chain before wrapping and skip a DataSource that already carries the marker. Two things need care.
AbstractRoutingDataSourceis not aDelegatingDataSource, and a skip condition that is too broad silently drops a second, independent DataSource.Happy to open a pull request with tests if this direction works for you.