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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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