Skip to content

Commit 4065ffa

Browse files
olim7tadutra
authored andcommitted
JAVA-2053: Cache results of session.prepare()
1 parent 1313a33 commit 4065ffa

File tree

18 files changed

+357
-270
lines changed

18 files changed

+357
-270
lines changed

changelog/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -4,6 +4,7 @@
4

4

5
### 4.0.0-beta3 (in progress)
5
### 4.0.0-beta3 (in progress)
6

6

7+
- [improvement] JAVA-2053: Cache results of session.prepare()
7
- [improvement] JAVA-2058: Make programmatic config reloading part of the public API
8
- [improvement] JAVA-2058: Make programmatic config reloading part of the public API
8
- [improvement] JAVA-1943: Fail fast in execute() when the session is closed
9
- [improvement] JAVA-1943: Fail fast in execute() when the session is closed
9
- [improvement] JAVA-2056: Reduce HashedWheelTimer tick duration
10
- [improvement] JAVA-2056: Reduce HashedWheelTimer tick duration

core/src/main/java/com/datastax/oss/driver/api/core/CqlSession.java

Lines changed: 30 additions & 0 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -123,6 +123,21 @@ default CompletionStage<? extends AsyncResultSet> executeAsync(@NonNull String q
123
*
123
*
124
* If you want to customize this behavior, you can write your own implementation of {@link
124
* If you want to customize this behavior, you can write your own implementation of {@link
125
* PrepareRequest} and pass it to {@link #prepare(PrepareRequest)}.
125
* PrepareRequest} and pass it to {@link #prepare(PrepareRequest)}.
126+
*
127+
* <p>The result of this method is cached: if you call it twice with the same {@link
128+
* SimpleStatement}, you will get the same {@link PreparedStatement} instance. We still recommend
129+
* keeping a reference to it (for example by caching it as a field in a DAO); if that's not
130+
* possible (e.g. if query strings are generated dynamically), it's OK to call this method every
131+
* time: there will just be a small performance overhead to check the internal cache. Note that
132+
* caching is based on:
133+
*
134+
* <ul>
135+
* <li>the query string exactly as you provided it: the driver does not perform any kind of
136+
* trimming or sanitizing.
137+
* <li>all other execution parameters: for example, preparing two statements with identical
138+
* query strings but different {@linkplain SimpleStatement#getConsistencyLevel() consistency
139+
* levels} will yield distinct prepared statements.
140+
* </ul>
126
*/
141
*/
127
@NonNull
142
@NonNull
128
default PreparedStatement prepare(@NonNull SimpleStatement statement) {
143
default PreparedStatement prepare(@NonNull SimpleStatement statement) {
@@ -134,6 +149,9 @@ default PreparedStatement prepare(@NonNull SimpleStatement statement) {
134
/**
149
/**
135
* Prepares a CQL statement synchronously (the calling thread blocks until the statement is
150
* Prepares a CQL statement synchronously (the calling thread blocks until the statement is
136
* prepared).
151
* prepared).
152+
*
153+
* <p>The result of this method is cached (see {@link #prepare(SimpleStatement)} for more
154+
* explanations).
137
*/
155
*/
138
@NonNull
156
@NonNull
139
default PreparedStatement prepare(@NonNull String query) {
157
default PreparedStatement prepare(@NonNull String query) {
@@ -150,6 +168,9 @@ default PreparedStatement prepare(@NonNull String query) {
150
* customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link
168
* customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link
151
* #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal
169
* #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal
152
* with {@link PrepareRequest} directly.
170
* with {@link PrepareRequest} directly.
171+
*
172+
* <p>The result of this method is cached (see {@link #prepare(SimpleStatement)} for more
173+
* explanations).
153
*/
174
*/
154
@NonNull
175
@NonNull
155
default PreparedStatement prepare(@NonNull PrepareRequest request) {
176
default PreparedStatement prepare(@NonNull PrepareRequest request) {
@@ -165,6 +186,9 @@ default PreparedStatement prepare(@NonNull PrepareRequest request) {
165
* <p>Note that the bound statements created from the resulting prepared statement will inherit
186
* <p>Note that the bound statements created from the resulting prepared statement will inherit
166
* some of the attributes of {@code query}; see {@link #prepare(SimpleStatement)} for more
187
* some of the attributes of {@code query}; see {@link #prepare(SimpleStatement)} for more
167
* details.
188
* details.
189+
*
190+
* <p>The result of this method is cached (see {@link #prepare(SimpleStatement)} for more
191+
* explanations).
168
*/
192
*/
169
@NonNull
193
@NonNull
170
default CompletionStage<? extends PreparedStatement> prepareAsync(
194
default CompletionStage<? extends PreparedStatement> prepareAsync(
@@ -177,6 +201,9 @@ default CompletionStage<? extends PreparedStatement> prepareAsync(
177
/**
201
/**
178
* Prepares a CQL statement asynchronously (the call returns as soon as the prepare query was
202
* Prepares a CQL statement asynchronously (the call returns as soon as the prepare query was
179
* sent, generally before the statement is prepared).
203
* sent, generally before the statement is prepared).
204+
*
205+
* <p>The result of this method is cached (see {@link #prepare(SimpleStatement)} for more
206+
* explanations).
180
*/
207
*/
181
@NonNull
208
@NonNull
182
default CompletionStage<? extends PreparedStatement> prepareAsync(@NonNull String query) {
209
default CompletionStage<? extends PreparedStatement> prepareAsync(@NonNull String query) {
@@ -193,6 +220,9 @@ default CompletionStage<? extends PreparedStatement> prepareAsync(@NonNull Strin
193
* customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link
220
* customize how attributes are propagated when you prepare a {@link SimpleStatement} (see {@link
194
* #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal
221
* #prepare(SimpleStatement)} for more explanations). Otherwise, you should rarely have to deal
195
* with {@link PrepareRequest} directly.
222
* with {@link PrepareRequest} directly.
223+
*
224+
* <p>The result of this method is cached (see {@link #prepare(SimpleStatement)} for more
225+
* explanations).
196
*/
226
*/
197
@NonNull
227
@NonNull
198
default CompletionStage<? extends PreparedStatement> prepareAsync(PrepareRequest request) {
228
default CompletionStage<? extends PreparedStatement> prepareAsync(PrepareRequest request) {

core/src/main/java/com/datastax/oss/driver/api/core/metrics/DefaultSessionMetric.java

Lines changed: 1 addition & 0 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -29,6 +29,7 @@ public enum DefaultSessionMetric implements SessionMetric {
29
THROTTLING_DELAY("throttling.delay"),
29
THROTTLING_DELAY("throttling.delay"),
30
THROTTLING_QUEUE_SIZE("throttling.queue-size"),
30
THROTTLING_QUEUE_SIZE("throttling.queue-size"),
31
THROTTLING_ERRORS("throttling.errors"),
31
THROTTLING_ERRORS("throttling.errors"),
32+
CQL_PREPARED_CACHE_SIZE("cql-prepared-cache-size"),
32
;
33
;
33

34

34
private static final Map<String, DefaultSessionMetric> BY_PATH = sortByPath();
35
private static final Map<String, DefaultSessionMetric> BY_PATH = sortByPath();

core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareAsyncHandler.java

Lines changed: 0 additions & 42 deletions
This file was deleted.

core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareAsyncProcessor.java

Lines changed: 40 additions & 9 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -23,20 +23,26 @@
23
import com.datastax.oss.driver.internal.core.session.DefaultSession;
23
import com.datastax.oss.driver.internal.core.session.DefaultSession;
24
import com.datastax.oss.driver.internal.core.session.RequestProcessor;
24
import com.datastax.oss.driver.internal.core.session.RequestProcessor;
25
import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures;
25
import com.datastax.oss.driver.internal.core.util.concurrent.CompletableFutures;
26-
import java.nio.ByteBuffer;
26+
import com.datastax.oss.driver.shaded.guava.common.cache.Cache;
27+
import com.datastax.oss.driver.shaded.guava.common.cache.CacheBuilder;
28+
import java.util.concurrent.CompletableFuture;
27
import java.util.concurrent.CompletionStage;
29
import java.util.concurrent.CompletionStage;
28-
import java.util.concurrent.ConcurrentMap;
30+
import java.util.concurrent.ExecutionException;
29
import net.jcip.annotations.ThreadSafe;
31
import net.jcip.annotations.ThreadSafe;
30

32

31
@ThreadSafe
33
@ThreadSafe
32
public class CqlPrepareAsyncProcessor
34
public class CqlPrepareAsyncProcessor
33
implements RequestProcessor<PrepareRequest, CompletionStage<PreparedStatement>> {
35
implements RequestProcessor<PrepareRequest, CompletionStage<PreparedStatement>> {
34

36

35-
private final ConcurrentMap<ByteBuffer, DefaultPreparedStatement> preparedStatementsCache;
37+
protected final Cache<PrepareRequest, CompletableFuture<PreparedStatement>> cache;
36

38

37-
public CqlPrepareAsyncProcessor(
39+
public CqlPrepareAsyncProcessor() {
38-
ConcurrentMap<ByteBuffer, DefaultPreparedStatement> preparedStatementsCache) {
40+
this(CacheBuilder.newBuilder().weakValues().build());
39-
this.preparedStatementsCache = preparedStatementsCache;
41+
}
42+
43+
protected CqlPrepareAsyncProcessor(
44+
Cache<PrepareRequest, CompletableFuture<PreparedStatement>> cache) {
45+
this.cache = cache;
40
}
46
}
41

47

42
@Override
48
@Override
@@ -50,13 +56,38 @@ public CompletionStage<PreparedStatement> process(
50
DefaultSession session,
56
DefaultSession session,
51
InternalDriverContext context,
57
InternalDriverContext context,
52
String sessionLogPrefix) {
58
String sessionLogPrefix) {
53-
return new CqlPrepareAsyncHandler(
59+
54-
request, preparedStatementsCache, session, context, sessionLogPrefix)
60+
try {
55-
.handle();
61+
CompletableFuture<PreparedStatement> result = cache.getIfPresent(request);
62+
if (result == null) {
63+
CompletableFuture<PreparedStatement> mine = new CompletableFuture<>();
64+
result = cache.get(request, () -> mine);
65+
if (result == mine) {
66+
new CqlPrepareHandler(request, session, context, sessionLogPrefix)
67+
.handle()
68+
.whenComplete(
69+
(preparedStatement, error) -> {
70+
if (error != null) {
71+
mine.completeExceptionally(error);
72+
cache.invalidate(request); // Make sure failure isn't cached indefinitely
73+
} else {
74+
mine.complete(preparedStatement);
75+
}
76+
});
77+
}
78+
}
79+
return result;
80+
} catch (ExecutionException e) {
81+
return CompletableFutures.failedFuture(e.getCause());
82+
}
56
}
83
}
57

84

58
@Override
85
@Override
59
public CompletionStage<PreparedStatement> newFailure(RuntimeException error) {
86
public CompletionStage<PreparedStatement> newFailure(RuntimeException error) {
60
return CompletableFutures.failedFuture(error);
87
return CompletableFutures.failedFuture(error);
61
}
88
}
89+
90+
public Cache<PrepareRequest, CompletableFuture<PreparedStatement>> getCache() {
91+
return cache;
92+
}
62
}
93
}

core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandlerBase.java renamed to core/src/main/java/com/datastax/oss/driver/internal/core/cql/CqlPrepareHandler.java

Lines changed: 27 additions & 58 deletions
Original file line numberOriginal file lineDiff line numberDiff line change
@@ -55,7 +55,6 @@
55
import io.netty.util.Timer;
55
import io.netty.util.Timer;
56
import io.netty.util.concurrent.Future;
56
import io.netty.util.concurrent.Future;
57
import io.netty.util.concurrent.GenericFutureListener;
57
import io.netty.util.concurrent.GenericFutureListener;
58-
import java.nio.ByteBuffer;
59
import java.time.Duration;
58
import java.time.Duration;
60
import java.util.AbstractMap;
59
import java.util.AbstractMap;
61
import java.util.ArrayList;
60
import java.util.ArrayList;
@@ -65,7 +64,6 @@
65
import java.util.concurrent.CancellationException;
64
import java.util.concurrent.CancellationException;
66
import java.util.concurrent.CompletableFuture;
65
import java.util.concurrent.CompletableFuture;
67
import java.util.concurrent.CompletionStage;
66
import java.util.concurrent.CompletionStage;
68-
import java.util.concurrent.ConcurrentMap;
69
import java.util.concurrent.CopyOnWriteArrayList;
67
import java.util.concurrent.CopyOnWriteArrayList;
70
import java.util.concurrent.TimeUnit;
68
import java.util.concurrent.TimeUnit;
71
import net.jcip.annotations.ThreadSafe;
69
import net.jcip.annotations.ThreadSafe;
@@ -74,14 +72,13 @@
74

72

75
/** Handles the lifecycle of the preparation of a CQL statement. */
73
/** Handles the lifecycle of the preparation of a CQL statement. */
76
@ThreadSafe
74
@ThreadSafe
77-
public abstract class CqlPrepareHandlerBase implements Throttled {
75+
public class CqlPrepareHandler implements Throttled {
78

76

79-
private static final Logger LOG = LoggerFactory.getLogger(CqlPrepareHandlerBase.class);
77+
private static final Logger LOG = LoggerFactory.getLogger(CqlPrepareHandler.class);
80

78

81
private final long startTimeNanos;
79
private final long startTimeNanos;
82
private final String logPrefix;
80
private final String logPrefix;
83
private final PrepareRequest request;
81
private final PrepareRequest request;
84-
private final ConcurrentMap<ByteBuffer, DefaultPreparedStatement> preparedStatementsCache;
85
private final DefaultSession session;
82
private final DefaultSession session;
86
private final InternalDriverContext context;
83
private final InternalDriverContext context;
87
private final DriverExecutionProfile executionProfile;
84
private final DriverExecutionProfile executionProfile;
@@ -100,9 +97,8 @@ public abstract class CqlPrepareHandlerBase implements Throttled {
100
// We don't use a map because nodes can appear multiple times.
97
// We don't use a map because nodes can appear multiple times.
101
private volatile List<Map.Entry<Node, Throwable>> errors;
98
private volatile List<Map.Entry<Node, Throwable>> errors;
102

99

103-
protected CqlPrepareHandlerBase(
100+
protected CqlPrepareHandler(
104
PrepareRequest request,
101
PrepareRequest request,
105-
ConcurrentMap<ByteBuffer, DefaultPreparedStatement> preparedStatementsCache,
106
DefaultSession session,
102
DefaultSession session,
107
InternalDriverContext context,
103
InternalDriverContext context,
108
String sessionLogPrefix) {
104
String sessionLogPrefix) {
@@ -112,7 +108,6 @@ protected CqlPrepareHandlerBase(
112
LOG.trace("[{}] Creating new handler for prepare request {}", logPrefix, request);
108
LOG.trace("[{}] Creating new handler for prepare request {}", logPrefix, request);
113

109

114
this.request = request;
110
this.request = request;
115-
this.preparedStatementsCache = preparedStatementsCache;
116
this.session = session;
111
this.session = session;
117
this.context = context;
112
this.context = context;
118
this.executionProfile = Conversions.resolveExecutionProfile(request, context);
113
this.executionProfile = Conversions.resolveExecutionProfile(request, context);
@@ -171,6 +166,10 @@ public void onThrottleReady(boolean wasDelayed) {
171
sendRequest(null, 0);
166
sendRequest(null, 0);
172
}
167
}
173

168

169+
public CompletableFuture<PreparedStatement> handle() {
170+
return result;
171+
}
172+
174
private Timeout scheduleTimeout(Duration timeoutDuration) {
173
private Timeout scheduleTimeout(Duration timeoutDuration) {
175
if (timeoutDuration.toNanos() > 0) {
174
if (timeoutDuration.toNanos() > 0) {
176
return this.timer.newTimeout(
175
return this.timer.newTimeout(
@@ -221,7 +220,7 @@ private void recordError(Node node, Throwable error) {
221
// Use a local variable to do only a single single volatile read in the nominal case
220
// Use a local variable to do only a single single volatile read in the nominal case
222
List<Map.Entry<Node, Throwable>> errorsSnapshot = this.errors;
221
List<Map.Entry<Node, Throwable>> errorsSnapshot = this.errors;
223
if (errorsSnapshot == null) {
222
if (errorsSnapshot == null) {
224-
synchronized (CqlPrepareHandlerBase.this) {
223+
synchronized (CqlPrepareHandler.this) {
225
errorsSnapshot = this.errors;
224
errorsSnapshot = this.errors;
226
if (errorsSnapshot == null) {
225
if (errorsSnapshot == null) {
227
this.errors = errorsSnapshot = new CopyOnWriteArrayList<>();
226
this.errors = errorsSnapshot = new CopyOnWriteArrayList<>();
@@ -236,58 +235,28 @@ private void setFinalResult(Prepared prepared) {
236
// Whatever happens below, we're done with this stream id
235
// Whatever happens below, we're done with this stream id
237
throttler.signalSuccess(this);
236
throttler.signalSuccess(this);
238

237

239-
DefaultPreparedStatement newStatement =
238+
DefaultPreparedStatement preparedStatement =
240
Conversions.toPreparedStatement(prepared, request, context);
239
Conversions.toPreparedStatement(prepared, request, context);
241

240

242-
DefaultPreparedStatement cachedStatement = cache(newStatement);
241+
session
243-
242+
.getRepreparePayloads()
244-
if (cachedStatement != newStatement) {
243+
.put(preparedStatement.getId(), preparedStatement.getRepreparePayload());
245-
// The statement already existed in the cache, assume it's because the client called
244+
if (prepareOnAllNodes) {
246-
// prepare() twice, and therefore it's already been prepared on other nodes.
245+
prepareOnOtherNodes()
247-
result.complete(cachedStatement);
246+
.thenRun(
248-
} else {
247+
() -> {
249-
session
248+
LOG.trace(
250-
.getRepreparePayloads()
249+
"[{}] Done repreparing on other nodes, completing the request", logPrefix);
251-
.put(cachedStatement.getId(), cachedStatement.getRepreparePayload());
250+
result.complete(preparedStatement);
252-
if (prepareOnAllNodes) {
251+
})
253-
prepareOnOtherNodes()
252+
.exceptionally(
254-
.thenRun(
253+
error -> {
255-
() -> {
254+
result.completeExceptionally(error);
256-
LOG.trace(
255+
return null;
257-
"[{}] Done repreparing on other nodes, completing the request", logPrefix);
256+
});
258-
result.complete(cachedStatement);
259-
})
260-
.exceptionally(
261-
error -> {
262-
result.completeExceptionally(error);
263-
return null;
264-
});
265-
} else {
266-
LOG.trace("[{}] Prepare on all nodes is disabled, completing the request", logPrefix);
267-
result.complete(cachedStatement);
268-
}
269-
}
270-
}
271-
272-
private DefaultPreparedStatement cache(DefaultPreparedStatement preparedStatement) {
273-
DefaultPreparedStatement previous =
274-
preparedStatementsCache.putIfAbsent(preparedStatement.getId(), preparedStatement);
275-
if (previous != null) {
276-
LOG.warn(
277-
"Re-preparing already prepared query. "
278-
+ "This is generally an anti-pattern and will likely affect performance. "
279-
+ "The cached version of the PreparedStatement will be returned, which may use "
280-
+ "different bound statement execution parameters (CL, timeout, etc.) from the "
281-
+ "current session.prepare call. Consider preparing the statement only once. "
282-
+ "Query='{}'",
283-
preparedStatement.getQuery());
284-
285-
// The one object in the cache will get GCed once it's not referenced by the client anymore
286-
// since we use a weak reference. So we need to make sure that the instance we do return to
287-
// the user is the one that is in the cache.
288-
return previous;
289
} else {
257
} else {
290-
return preparedStatement;
258+
LOG.trace("[{}] Prepare on all nodes is disabled, completing the request", logPrefix);
259+
result.complete(preparedStatement);
291
}
260
}
292
}
261
}
293

262

0 commit comments

Comments
 (0)