Skip to content

Queries are counted twice when a DataSource bean delegates to another DataSource bean #263

Description

@jjh75607

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    🐛 bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions