Skip to content

Commit cd0b08e

Browse files
authored
Introduce skipCQL4MetadataResolveMethod to address CQL4 issues (#463)
Introduce skipCQL4MetadataResolveMethod to Mitigate CQL4 Issues CQL4 has a flaw where, after a schema change, an affected prepared statement may not be invalidated on the client side. As a result, the driver may read data that does not match the cached metadata, leading to deserialization failures or incorrect deserialization. For more details, see: scylladb/scylladb#20860. This commit introduces the skipCQL4MetadataResolveMethod, which determines how the driver resolves the skip metadata flag for CQL4 prepared statements. It supports three modes: `SMART` (default) – Disables the skip metadata flag only for wildcard selects and selects that return UDTs (including collections and maps). `ENABLED` – Always enables the skip metadata flag, no metadata is being sent. `DISABLED` – Always disables the skip metadata flag, metadata is being sent with every bound statement RESULT frame.
1 parent 63a9828 commit cd0b08e

File tree

4 files changed

+305
-8
lines changed

4 files changed

+305
-8
lines changed

driver-core/src/main/java/com/datastax/driver/core/DefaultPreparedStatement.java

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,19 @@
2424
import static com.datastax.driver.core.ProtocolVersion.V4;
2525

2626
import com.datastax.driver.core.policies.RetryPolicy;
27+
import com.google.common.base.Splitter;
2728
import com.google.common.collect.ImmutableMap;
2829
import java.nio.ByteBuffer;
2930
import java.util.List;
3031
import java.util.Map;
32+
import org.slf4j.Logger;
33+
import org.slf4j.LoggerFactory;
3134

3235
public class DefaultPreparedStatement implements PreparedStatement {
33-
36+
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPreparedStatement.class);
3437
private static final String SCYLLA_CDC_LOG_SUFFIX = "_scylla_cdc_log";
38+
private static final Splitter SPACE_SPLITTER = Splitter.onPattern("\\s+");
39+
private static final Splitter COMMA_SPLITTER = Splitter.onPattern(",");
3540

3641
final PreparedId preparedId;
3742

@@ -50,6 +55,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
5055
volatile RetryPolicy retryPolicy;
5156
volatile ImmutableMap<String, ByteBuffer> outgoingPayload;
5257
volatile Boolean idempotent;
58+
volatile boolean skipMetadata;
5359

5460
private DefaultPreparedStatement(
5561
PreparedId id,
@@ -66,6 +72,7 @@ private DefaultPreparedStatement(
6672
this.cluster = cluster;
6773
this.isLWT = isLWT;
6874
this.partitioner = partitioner;
75+
this.skipMetadata = this.calculateSkipMetadata();
6976
}
7077

7178
static DefaultPreparedStatement fromMessage(
@@ -172,6 +179,62 @@ private static Token.Factory partitioner(ColumnDefinitions defs, Cluster cluster
172179
return null;
173180
}
174181

182+
private boolean calculateSkipMetadata() {
183+
if (cluster.manager.protocolVersion() == ProtocolVersion.V1
184+
|| preparedId.resultSetMetadata.variables == null) {
185+
// CQL1 does not support it.
186+
// If no rows returned there is no reason to send this flag, consequently, no metadata.
187+
return false;
188+
}
189+
190+
if (preparedId.resultSetMetadata.id != null
191+
&& preparedId.resultSetMetadata.id.bytes.length > 0) {
192+
// It is CQL 5 or higher.
193+
// Prepared statement invalidation works perfectly no need to disable skip metadata
194+
return true;
195+
}
196+
197+
switch (cluster.getConfiguration().getQueryOptions().getSkipCQL4MetadataResolveMethod()) {
198+
case ENABLED:
199+
return true;
200+
case DISABLED:
201+
return false;
202+
}
203+
204+
if (isWildcardSelect(query)) {
205+
LOGGER.warn(
206+
"Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
207+
+ "These issues may lead to broken deserialization or data corruption. "
208+
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
209+
+ "though this negatively impacts performance. "
210+
+ "To avoid this, consider using a targeted select instead. "
211+
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
212+
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
213+
query);
214+
return false;
215+
}
216+
// Disable skipping metadata if results contains udt and
217+
for (ColumnDefinitions.Definition columnDefinition : preparedId.resultSetMetadata.variables) {
218+
if (containsUDT(columnDefinition.getType())) {
219+
LOGGER.warn(
220+
"Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
221+
+ "These issues may lead to broken deserialization or data corruption. "
222+
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
223+
+ "though this negatively impacts performance. "
224+
+ "To avoid this, consider using a targeted select instead. "
225+
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
226+
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
227+
query);
228+
return false;
229+
}
230+
}
231+
return true;
232+
}
233+
234+
public boolean isSkipMetadata() {
235+
return skipMetadata;
236+
}
237+
175238
@Override
176239
public ColumnDefinitions getVariables() {
177240
return preparedId.boundValuesMetadata.variables;
@@ -315,4 +378,44 @@ public Boolean isIdempotent() {
315378
public boolean isLWT() {
316379
return isLWT;
317380
}
381+
382+
private static boolean containsUDT(DataType dataType) {
383+
if (dataType.isCollection()) {
384+
for (DataType elementType : dataType.getTypeArguments()) {
385+
if (containsUDT(elementType)) {
386+
return true;
387+
}
388+
}
389+
return false;
390+
}
391+
return dataType instanceof UserType;
392+
}
393+
394+
private static boolean isWildcardSelect(String query) {
395+
List<String> chunks = SPACE_SPLITTER.splitToList(query.trim().toLowerCase());
396+
if (chunks.size() < 2) {
397+
// Weird query, assuming no result expected
398+
return false;
399+
}
400+
401+
if (!chunks.get(0).equals("select")) {
402+
// In case if non-select sneaks in, disable skip metadata for it no result expected.
403+
return false;
404+
}
405+
406+
for (String chunk : chunks) {
407+
if (chunk.equals("from")) {
408+
return false;
409+
}
410+
if (chunk.equals("*")) {
411+
return true;
412+
}
413+
for (String part : COMMA_SPLITTER.split(chunk)) {
414+
if (part.equals("*")) {
415+
return true;
416+
}
417+
}
418+
}
419+
return false;
420+
}
318421
}

driver-core/src/main/java/com/datastax/driver/core/QueryOptions.java

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public class QueryOptions {
6969
private volatile boolean reprepareOnUp = true;
7070
private volatile Cluster.Manager manager;
7171
private volatile boolean prepareOnAllHosts = true;
72+
private volatile CQL4SkipMetadataResolveMethod skipCQL4MetadataResolveMethod =
73+
CQL4SkipMetadataResolveMethod.SMART;
7274

7375
private volatile boolean schemaQueriesPaged = true;
7476

@@ -193,6 +195,38 @@ public boolean getDefaultIdempotence() {
193195
return defaultIdempotence;
194196
}
195197

198+
/**
199+
* There is known problem in CQL 4.x when prepared statement invalidation could be voided: <a
200+
* href="https://github.com/scylladb/scylladb/issues/20860">more info</a> When it happens metadata
201+
* on client side does not match data and deserialization can go wrong in many ways To avoid
202+
* driver can disable skip metadata flag to make server respond with metadata on every query.
203+
* Unfortunately it causes excessive network traffic and CPU overhead on both server and driver
204+
* side. This option controls how driver resolves skip metadata flag for CQL4 prepared statements.
205+
* **SMART** - disable flag only for wildcard selects (select * from) and selects that return
206+
* UDTs, including collections of UDTs and maps that contain UDTs **ENABLED** - flag is always set
207+
* **DISABLED** - flag is always disabled Default is SMART Required: yes Modifiable at runtime:
208+
* yes, the new value will be used for requests issued after the change. Overridable in a profile:
209+
* yes
210+
*
211+
* @param method the new value to set as skip metadata resolve method.
212+
* @return this {@code QueryOptions} instance.
213+
*/
214+
public QueryOptions setSkipCQL4MetadataResolveMethod(CQL4SkipMetadataResolveMethod method) {
215+
this.skipCQL4MetadataResolveMethod = method;
216+
return this;
217+
}
218+
219+
/**
220+
* Skip metadata resolve method .
221+
*
222+
* <p>It defaults to {@link #skipCQL4MetadataResolveMethod.SMART}.
223+
*
224+
* @return the default idempotence for queries.
225+
*/
226+
public CQL4SkipMetadataResolveMethod getSkipCQL4MetadataResolveMethod() {
227+
return this.skipCQL4MetadataResolveMethod;
228+
}
229+
196230
/**
197231
* Set whether the driver should prepare statements on all hosts in the cluster.
198232
*
@@ -583,4 +617,10 @@ public int hashCode() {
583617
public boolean isConsistencySet() {
584618
return consistencySet;
585619
}
620+
621+
public enum CQL4SkipMetadataResolveMethod {
622+
ENABLED,
623+
DISABLED,
624+
SMART
625+
}
586626
}

driver-core/src/main/java/com/datastax/driver/core/SessionManager.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,11 +654,16 @@ else if (fetchSize != Integer.MAX_VALUE)
654654
}
655655
if (protocolVersion.compareTo(ProtocolVersion.V4) < 0) bs.ensureAllSet();
656656

657-
// skip resultset metadata if version > 1 (otherwise this feature is not supported)
658-
// and if we already have metadata for the prepared statement being executed.
659-
boolean skipMetadata =
660-
protocolVersion != ProtocolVersion.V1
661-
&& bs.statement.getPreparedId().resultSetMetadata.variables != null;
657+
boolean skipMetadata;
658+
if (bs.statement instanceof DefaultPreparedStatement) {
659+
skipMetadata = ((DefaultPreparedStatement) bs.statement).isSkipMetadata();
660+
} else {
661+
skipMetadata =
662+
protocolVersion != ProtocolVersion.V1
663+
&& bs.statement.getPreparedId().resultSetMetadata.variables != null;
664+
// skip resultset metadata if version > 1 (otherwise this feature is not supported)
665+
// and if we already have metadata for the prepared statement being executed.
666+
}
662667

663668
Requests.QueryProtocolOptions options =
664669
new Requests.QueryProtocolOptions(

0 commit comments

Comments
 (0)