Skip to content

Commit d4f724e

Browse files
committed
Disable skip metadata for CQL4 for broken cases
CQL4 has flaws that allows scenario when after schema change affected prepare statement is not invalidated on client side. As result, data that is read by driver does no match cached metadata and client will fail to deserialize or deserialization will go wrong. More info at: scylladb/scylladb#20860 This commit introduces PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD(advanced.prepared-statements.skip-cql4-metadata-resolve-method) that controls how driver resolves skip metadata flag for CQL4 prepared statements, it can be "smart", "always-on", "always-off". Default is "smart". It makes driver disable skip metadata flag only for wildcard selects and selects that returns udts (including collections and maps)
1 parent ce575f3 commit d4f724e

File tree

9 files changed

+338
-7
lines changed

9 files changed

+338
-7
lines changed

core/src/main/java/com/datastax/dse/driver/internal/core/cql/DseConversions.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.datastax.oss.driver.internal.core.ProtocolVersionRegistry;
4040
import com.datastax.oss.driver.internal.core.context.InternalDriverContext;
4141
import com.datastax.oss.driver.internal.core.cql.Conversions;
42+
import com.datastax.oss.driver.internal.core.cql.DefaultPreparedStatement;
4243
import com.datastax.oss.protocol.internal.Message;
4344
import com.datastax.oss.protocol.internal.request.Execute;
4445
import com.datastax.oss.protocol.internal.request.Query;
@@ -115,8 +116,15 @@ public static Message toContinuousPagingMessage(
115116
protocolVersion, DefaultProtocolFeature.UNSET_BOUND_VALUES)) {
116117
Conversions.ensureAllSet(boundStatement);
117118
}
118-
boolean skipMetadata =
119-
boundStatement.getPreparedStatement().getResultSetDefinitions().size() > 0;
119+
120+
boolean skipMetadata;
121+
if (boundStatement.getPreparedStatement() instanceof DefaultPreparedStatement) {
122+
skipMetadata =
123+
((DefaultPreparedStatement) boundStatement.getPreparedStatement()).isSkipMetadata();
124+
} else {
125+
skipMetadata = boundStatement.getPreparedStatement().getResultSetDefinitions().size() > 0;
126+
;
127+
}
120128
DseQueryOptions queryOptions =
121129
new DseQueryOptions(
122130
consistencyCode,

core/src/main/java/com/datastax/oss/driver/api/core/config/DefaultDriverOption.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,6 +689,18 @@ public enum DefaultDriverOption implements DriverOption {
689689
* <p>Value-type: boolean
690690
*/
691691
PREPARE_ON_ALL_NODES("advanced.prepared-statements.prepare-on-all-nodes"),
692+
693+
/**
694+
* How driver resolves skip metadata flag for CQL4 prepared statements. "smart" - disable flag
695+
* only for wildcard selects (select * from) and selects that return UDTs, including collections
696+
* of UDTs and maps that contain UDTs "always-on" - flag is always set "always-off" - flag is
697+
* always disabled
698+
*
699+
* <p>Value-type: string
700+
*/
701+
PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD(
702+
"advanced.prepared-statements.skip-cql4-metadata-resolve-method"),
703+
692704
/**
693705
* Whether the driver tries to prepare on new nodes at all.
694706
*

core/src/main/java/com/datastax/oss/driver/api/core/config/OptionsMap.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ protected static void fillWithDriverDefaults(OptionsMap map) {
258258
map.put(TypedDriverOption.REQUEST_TIMEOUT, requestTimeout);
259259
map.put(TypedDriverOption.REQUEST_CONSISTENCY, "LOCAL_ONE");
260260
map.put(TypedDriverOption.REQUEST_PAGE_SIZE, requestPageSize);
261+
map.put(TypedDriverOption.PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD, "smart");
261262
map.put(TypedDriverOption.REQUEST_SERIAL_CONSISTENCY, "SERIAL");
262263
map.put(TypedDriverOption.REQUEST_DEFAULT_IDEMPOTENCE, false);
263264
map.put(TypedDriverOption.GRAPH_TRAVERSAL_SOURCE, "g");

core/src/main/java/com/datastax/oss/driver/api/core/config/TypedDriverOption.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,10 @@ public String toString() {
583583
/** Whether `Session.prepare` calls should be sent to all nodes in the cluster. */
584584
public static final TypedDriverOption<Boolean> PREPARE_ON_ALL_NODES =
585585
new TypedDriverOption<>(DefaultDriverOption.PREPARE_ON_ALL_NODES, GenericType.BOOLEAN);
586+
/** Method to resolve skip metadata flag for CQL4. */
587+
public static final TypedDriverOption<String> PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD =
588+
new TypedDriverOption<>(
589+
DefaultDriverOption.PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD, GenericType.STRING);
586590
/** Whether the driver tries to prepare on new nodes at all. */
587591
public static final TypedDriverOption<Boolean> REPREPARE_ENABLED =
588592
new TypedDriverOption<>(DefaultDriverOption.REPREPARE_ENABLED, GenericType.BOOLEAN);

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,15 @@ public static Message toMessage(
194194
protocolVersion, DefaultProtocolFeature.UNSET_BOUND_VALUES)) {
195195
ensureAllSet(boundStatement);
196196
}
197-
boolean skipMetadata =
198-
boundStatement.getPreparedStatement().getResultSetDefinitions().size() > 0;
197+
198+
boolean skipMetadata;
199+
if (boundStatement.getPreparedStatement() instanceof DefaultPreparedStatement) {
200+
skipMetadata =
201+
((DefaultPreparedStatement) boundStatement.getPreparedStatement()).isSkipMetadata();
202+
} else {
203+
skipMetadata = boundStatement.getPreparedStatement().getResultSetDefinitions().size() > 0;
204+
}
205+
199206
QueryOptions queryOptions =
200207
new QueryOptions(
201208
consistencyCode,
@@ -382,6 +389,12 @@ public static DefaultPreparedStatement toPreparedStatement(
382389

383390
Partitioner partitioner = PartitionerFactory.partitioner(variableDefinitions, context);
384391

392+
DriverExecutionProfile defaultExecutionProfile =
393+
(request.getExecutionProfileNameForBoundStatements() == null
394+
|| request.getExecutionProfileNameForBoundStatements().isEmpty())
395+
? context.getConfig().getDefaultProfile()
396+
: context.getConfig().getProfile(request.getExecutionProfileNameForBoundStatements());
397+
385398
return new DefaultPreparedStatement(
386399
ByteBuffer.wrap(response.preparedQueryId).asReadOnlyBuffer(),
387400
request.getQuery(),
@@ -409,7 +422,8 @@ public static DefaultPreparedStatement toPreparedStatement(
409422
request.areBoundStatementsTracing(),
410423
context.getCodecRegistry(),
411424
context.getProtocolVersion(),
412-
lwtInfo != null && lwtInfo.isLwt(response.variablesMetadata.flags));
425+
lwtInfo != null && lwtInfo.isLwt(response.variablesMetadata.flags),
426+
defaultExecutionProfile);
413427
}
414428

415429
public static ColumnDefinitions toColumnDefinitions(

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

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,37 @@
2626
import com.datastax.oss.driver.api.core.ConsistencyLevel;
2727
import com.datastax.oss.driver.api.core.CqlIdentifier;
2828
import com.datastax.oss.driver.api.core.ProtocolVersion;
29+
import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
2930
import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
3031
import com.datastax.oss.driver.api.core.cql.BoundStatement;
3132
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
33+
import com.datastax.oss.driver.api.core.cql.ColumnDefinition;
3234
import com.datastax.oss.driver.api.core.cql.ColumnDefinitions;
3335
import com.datastax.oss.driver.api.core.cql.PreparedStatement;
3436
import com.datastax.oss.driver.api.core.cql.Statement;
3537
import com.datastax.oss.driver.api.core.metadata.token.Partitioner;
3638
import com.datastax.oss.driver.api.core.metadata.token.Token;
39+
import com.datastax.oss.driver.api.core.type.ContainerType;
40+
import com.datastax.oss.driver.api.core.type.DataType;
41+
import com.datastax.oss.driver.api.core.type.MapType;
42+
import com.datastax.oss.driver.api.core.type.TupleType;
43+
import com.datastax.oss.driver.api.core.type.UserDefinedType;
3744
import com.datastax.oss.driver.api.core.type.codec.registry.CodecRegistry;
3845
import com.datastax.oss.driver.internal.core.data.ValuesHelper;
3946
import com.datastax.oss.driver.internal.core.session.RepreparePayload;
47+
import com.datastax.oss.driver.shaded.guava.common.base.Splitter;
4048
import edu.umd.cs.findbugs.annotations.NonNull;
4149
import java.nio.ByteBuffer;
4250
import java.time.Duration;
4351
import java.util.List;
4452
import java.util.Map;
4553
import net.jcip.annotations.ThreadSafe;
54+
import org.slf4j.Logger;
55+
import org.slf4j.LoggerFactory;
4656

4757
@ThreadSafe
4858
public class DefaultPreparedStatement implements PreparedStatement {
59+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPreparedStatement.class);
4960

5061
private final ByteBuffer id;
5162
private final RepreparePayload repreparePayload;
@@ -69,6 +80,8 @@ public class DefaultPreparedStatement implements PreparedStatement {
6980
private final Duration timeoutForBoundStatements;
7081
private final Partitioner partitioner;
7182
private final boolean isLWT;
83+
private volatile boolean skipMetadata;
84+
private final DriverExecutionProfile defaultExecutionProfile;
7285

7386
public DefaultPreparedStatement(
7487
ByteBuffer id,
@@ -95,7 +108,8 @@ public DefaultPreparedStatement(
95108
boolean areBoundStatementsTracing,
96109
CodecRegistry codecRegistry,
97110
ProtocolVersion protocolVersion,
98-
boolean isLWT) {
111+
boolean isLWT,
112+
DriverExecutionProfile defaultExecutionProfile) {
99113
this.id = id;
100114
this.partitionKeyIndices = partitionKeyIndices;
101115
// It's important that we keep a reference to this object, so that it only gets evicted from
@@ -122,6 +136,15 @@ public DefaultPreparedStatement(
122136
this.codecRegistry = codecRegistry;
123137
this.protocolVersion = protocolVersion;
124138
this.isLWT = isLWT;
139+
this.defaultExecutionProfile = defaultExecutionProfile;
140+
this.skipMetadata =
141+
resolveSkipMetadata(
142+
query,
143+
resultMetadataId,
144+
resultSetDefinitions,
145+
executionProfileForBoundStatements != null
146+
? executionProfileForBoundStatements
147+
: this.defaultExecutionProfile);
125148
}
126149

127150
@NonNull
@@ -147,6 +170,10 @@ public Partitioner getPartitioner() {
147170
return partitioner;
148171
}
149172

173+
public boolean isSkipMetadata() {
174+
return skipMetadata;
175+
}
176+
150177
@NonNull
151178
@Override
152179
public List<Integer> getPartitionKeyIndices() {
@@ -172,6 +199,15 @@ public boolean isLWT() {
172199
@Override
173200
public void setResultMetadata(
174201
@NonNull ByteBuffer newResultMetadataId, @NonNull ColumnDefinitions newResultSetDefinitions) {
202+
this.skipMetadata =
203+
resolveSkipMetadata(
204+
this.getQuery(),
205+
newResultMetadataId,
206+
newResultSetDefinitions,
207+
executionProfileForBoundStatements != null
208+
? executionProfileForBoundStatements
209+
: this.defaultExecutionProfile);
210+
175211
this.resultMetadata = new ResultMetadata(newResultMetadataId, newResultSetDefinitions);
176212
}
177213

@@ -242,4 +278,88 @@ private ResultMetadata(ByteBuffer resultMetadataId, ColumnDefinitions resultSetD
242278
this.resultSetDefinitions = resultSetDefinitions;
243279
}
244280
}
281+
282+
private static boolean resolveSkipMetadata(
283+
String query,
284+
ByteBuffer resultMetadataId,
285+
ColumnDefinitions resultSet,
286+
DriverExecutionProfile executionProfileForBoundStatements) {
287+
String resolveMethod =
288+
(executionProfileForBoundStatements != null)
289+
? executionProfileForBoundStatements.getString(
290+
DefaultDriverOption.PREPARE_SKIP_CQL4_METADATA_RESOLVE_METHOD)
291+
: "smart";
292+
293+
if (resultSet == null || resultSet.size() == 0) {
294+
// there is no reason to send this flag, there will be no rows in the response and,
295+
// consequently, no metadata.
296+
return false;
297+
}
298+
if (resultMetadataId != null && resultMetadataId.capacity() > 0) {
299+
// It is CQL 5 or higher.
300+
// Prepared statement invalidation works perfectly no need to disable skip metadata
301+
return true;
302+
}
303+
// It is CQL 4 or lower.
304+
switch (resolveMethod) {
305+
case "always-on":
306+
return true;
307+
case "always-off":
308+
return false;
309+
}
310+
311+
if (!resolveMethod.equals("smart")) {
312+
LOGGER.warn(
313+
"Property advanced.prepared-statements.skip-cql4-metadata-resolve-method is incorrectly set to `{}`, available options: smart, always-on, always-off. Defaulting to `smart`",
314+
resolveMethod);
315+
}
316+
List<String> chunks = Splitter.onPattern("\\s+").splitToList(query);
317+
if (chunks.size() < 2) {
318+
// Weird query, assuming no result expected
319+
return false;
320+
}
321+
if (!chunks.get(0).toLowerCase().startsWith("select")) {
322+
// In case if non-select sneaks in, disable skip metadata for it no result expected.
323+
return false;
324+
}
325+
if (chunks.get(1).equals("*")) {
326+
LOGGER.warn(
327+
"Prepared statement `{}` is a wildcard select. Such statements have prepared statement "
328+
+ "invalidation problem when run on CQL4, this problems can lead to broken deserialization or data corruption. "
329+
+ "Consider using targeted select instead, or alternatively, you can set `skip-cql4-metadata-resolve-method` "
330+
+ "to `always-on` in execution profile to make driver ignore this problem and proceed anyway.",
331+
query);
332+
return false;
333+
}
334+
// Disable skipping metadata if results contains udt and
335+
for (ColumnDefinition columnDefinition : resultSet) {
336+
if (contains_udt(columnDefinition.getType())) {
337+
LOGGER.warn(
338+
"Prepared statement `{}` contains UDTs in result set. Such statements have prepared statement "
339+
+ "invalidation problem when run on CQL4, this problems can lead to broken deserialization or data corruption. "
340+
+ "Consider using targeted select instead, or alternatively, you can set `skip-cql4-metadata-resolve-method` "
341+
+ "to `always-on` in execution profile to make driver ignore this problem and proceed anyway.",
342+
query);
343+
return false;
344+
}
345+
}
346+
return true;
347+
}
348+
349+
private static boolean contains_udt(DataType dataType) {
350+
if (dataType instanceof ContainerType) {
351+
return contains_udt(((ContainerType) dataType).getElementType());
352+
} else if (dataType instanceof TupleType) {
353+
for (DataType elementType : ((TupleType) dataType).getComponentTypes()) {
354+
if (contains_udt(elementType)) {
355+
return true;
356+
}
357+
}
358+
return false;
359+
} else if (dataType instanceof MapType) {
360+
return contains_udt(((MapType) dataType).getKeyType())
361+
|| contains_udt(((MapType) dataType).getValueType());
362+
}
363+
return dataType instanceof UserDefinedType;
364+
}
245365
}

core/src/main/resources/reference.conf

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,22 @@ datastax-java-driver {
21822182
# Overridable in a profile: yes
21832183
prepare-on-all-nodes = true
21842184

2185+
# There is known problem in CQL 4.x when prepared statement invalidation could be voided:
2186+
# https://github.com/scylladb/scylladb/issues/20860
2187+
# When it happens metadata on client side does not match data and deserialization can go wrong in many ways
2188+
# To avoid driver can disable skip metadata flag to make server respond with metadata on every query.
2189+
# Unfortunately it causes excessive network traffic and CPU overhead on both server and driver side.
2190+
# This option controls how driver resolves skip metadata flag for CQL4 prepared statements.
2191+
# "smart" - disable flag only for wildcard selects (select * from) and selects that
2192+
# return UDTs, including collections of UDTs and maps that contain UDTs
2193+
# "always-on" - flag is always set
2194+
# "always-off" - flag is always disabled
2195+
# Default is "smart"
2196+
# Required: yes
2197+
# Modifiable at runtime: yes, the new value will be used for requests issued after the change.
2198+
# Overridable in a profile: yes
2199+
skip-cql4-metadata-resolve-method = smart
2200+
21852201
# How the driver replicates prepared statements on a node that just came back up or joined the
21862202
# cluster.
21872203
reprepare-on-up {

core/src/test/java/com/datastax/oss/driver/internal/core/tracker/RequestLogFormatterTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ private PreparedStatement mockPreparedStatement(String query, Map<String, DataTy
294294
false,
295295
context.getCodecRegistry(),
296296
context.getProtocolVersion(),
297-
false);
297+
false,
298+
null);
298299
}
299300
}

0 commit comments

Comments
 (0)