Skip to content

Prepared statements are not cached properly with observation enabled #1601

@Nicola239

Description

@Nicola239

Summary

Enabling observation for cassandra queries via ObservableCqlSessionFactoryBean breaks prepared statements caching in Datastax driver.
For each DML statement execution, a PREPARE statement is also executed, negatively affecting performance.

Affected versions

To create this issue, I used spring-data-cassandra:4.5.3 (Spring Boot 3.5.5), but the issue was present on earlier versions too.
I also encountered it in 4.2.5 (Boot 3.2.5), but perhaps it was introduced even earlier.

Example Project

This sample project might be helpful to reproduce the issue. It has some instructions in its readme file.

Detailed description of the problem

By default, without observation enabled, prepared statements are cached by the Datastax driver in com.datastax.oss.driver.internal.core.cql.CqlPrepareAsyncProcessor.process.
In CqlPrepareAsyncProcessor.java:147, a cache lookup is performed, which uses equals and hashCode of a SimpleStatement (wrapped in DefaultPrepareRequest) to find a cached PreparedStatement instance.

The official doc suggests to enable Micrometer instrumentation for a CqlSession by registering an
org.springframework.data.cassandra.observability.ObservableCqlSessionFactoryBean bean.
This leads to the CqlSession being wrapped in a org.springframework.data.cassandra.observability.CqlSessionObservationInterceptor proxy.

When any of the SyncCqlSession#prepare methods are called, they are intercepted by the CqlSessionObservationInterceptor.
It delegates the prepare calls to the SyncCqlSession#prepare(com.datastax.oss.driver.api.core.cql.SimpleStatement)method.
Before calling the delegate, it wraps SimpleStatement argument with a org.springframework.data.cassandra.observability.ObservationStatement proxy (CqlSessionObservationInterceptor.java:94).

When an ObservationStatement object reaches the cache lookup, it's hashCode and equals methods are called, which
are delegated to the JdkDynamicAopProxy implementations. The hashCode implementation doesn't have a problem. For two
delegates with the same hashCode output, their proxies will also return equal hash codes.
The equals method, however, uses org.springframework.aop.framework.AopProxyUtils.equalsInProxy, which checks for
advisors equality, which leads to the call of ObservationStatement.equals, which, in turn, uses a default equals implementation.
Because of the advisors inequality, proxy obects are deemed not equal, prepared statements are not returned from the cache and are executed again for each DML query.

Possible fix

Adding an equals method (and hashCode for consistency) to the ObservationStatement class seems to fix the problem without breaking any tests.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions