Skip to content

Commit 3c03a6b

Browse files
committed
Introduce skipCQL4MetadataResolveMethod to address CQL4 issues
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). ALWAYS_ON – Always enables the skip metadata flag. ALWAYS_OFF – Always disables the skip metadata flag.
1 parent 3635e7f commit 3c03a6b

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

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

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,16 @@
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";
3538

3639
final PreparedId preparedId;
@@ -50,6 +53,7 @@ public class DefaultPreparedStatement implements PreparedStatement {
5053
volatile RetryPolicy retryPolicy;
5154
volatile ImmutableMap<String, ByteBuffer> outgoingPayload;
5255
volatile Boolean idempotent;
56+
volatile boolean skipMetadata;
5357

5458
private DefaultPreparedStatement(
5559
PreparedId id,
@@ -66,6 +70,7 @@ private DefaultPreparedStatement(
6670
this.cluster = cluster;
6771
this.isLWT = isLWT;
6872
this.partitioner = partitioner;
73+
this.skipMetadata = this.calculateSkipMetadata();
6974
}
7075

7176
static DefaultPreparedStatement fromMessage(
@@ -172,6 +177,70 @@ private static Token.Factory partitioner(ColumnDefinitions defs, Cluster cluster
172177
return null;
173178
}
174179

180+
private boolean calculateSkipMetadata() {
181+
if (cluster.manager.protocolVersion() != ProtocolVersion.V1
182+
|| preparedId.resultSetMetadata.variables == null) {
183+
// CQL1 does not support it.
184+
// If no rows returned there is no reason to send this flag, consequently, no metadata.
185+
return false;
186+
}
187+
188+
if (preparedId.resultSetMetadata.id.bytes.length > 0) {
189+
// It is CQL 5 or higher.
190+
// Prepared statement invalidation works perfectly no need to disable skip metadata
191+
return true;
192+
}
193+
194+
switch (cluster.getConfiguration().getQueryOptions().getSkipCQL4MetadataResolveMethod()) {
195+
case ALWAYS_ON:
196+
return true;
197+
case ALWAYS_OFF:
198+
return false;
199+
}
200+
201+
List<String> chunks = Splitter.onPattern("\\s+").splitToList(query);
202+
if (chunks.size() < 2) {
203+
// Weird query, assuming no result expected
204+
return false;
205+
}
206+
if (!chunks.get(0).toLowerCase().startsWith("select")) {
207+
// In case if non-select sneaks in, disable skip metadata for it no result expected.
208+
return false;
209+
}
210+
if (chunks.get(1).equals("*")) {
211+
LOGGER.warn(
212+
"Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
213+
+ "These issues may lead to broken deserialization or data corruption. "
214+
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
215+
+ "though this negatively impacts performance. "
216+
+ "To avoid this, consider using a targeted select instead. "
217+
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
218+
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
219+
query);
220+
return false;
221+
}
222+
// Disable skipping metadata if results contains udt and
223+
for (ColumnDefinitions.Definition columnDefinition : preparedId.resultSetMetadata.variables) {
224+
if (containsUDT(columnDefinition.getType())) {
225+
LOGGER.warn(
226+
"Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
227+
+ "These issues may lead to broken deserialization or data corruption. "
228+
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
229+
+ "though this negatively impacts performance. "
230+
+ "To avoid this, consider using a targeted select instead. "
231+
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
232+
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
233+
query);
234+
return false;
235+
}
236+
}
237+
return true;
238+
}
239+
240+
public boolean getSkipMetadata() {
241+
return skipMetadata;
242+
}
243+
175244
@Override
176245
public ColumnDefinitions getVariables() {
177246
return preparedId.boundValuesMetadata.variables;
@@ -315,4 +384,16 @@ public Boolean isIdempotent() {
315384
public boolean isLWT() {
316385
return isLWT;
317386
}
387+
388+
private static boolean containsUDT(DataType dataType) {
389+
if (dataType.isCollection()) {
390+
for (DataType elementType : dataType.getTypeArguments()) {
391+
if (containsUDT(elementType)) {
392+
return true;
393+
}
394+
}
395+
return false;
396+
}
397+
return dataType instanceof UserType;
398+
}
318399
}

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

Lines changed: 39 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,37 @@ 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 UDTs,
206+
* including collections of UDTs and maps that contain UDTs ALWAYS_ON - flag is always set
207+
* ALWAYS_OFF - flag is always disabled Default is SMART Required: yes Modifiable at runtime: yes,
208+
* the new value will be used for requests issued after the change. Overridable in a profile: yes
209+
*
210+
* @param method the new value to set as skip metadata resolve method.
211+
* @return this {@code QueryOptions} instance.
212+
*/
213+
public QueryOptions setSkipCQL4MetadataResolveMethod(CQL4SkipMetadataResolveMethod method) {
214+
this.skipCQL4MetadataResolveMethod = method;
215+
return this;
216+
}
217+
218+
/**
219+
* Skip metadata resolve method .
220+
*
221+
* <p>It defaults to {@link #skipCQL4MetadataResolveMethod.SMART}.
222+
*
223+
* @return the default idempotence for queries.
224+
*/
225+
public CQL4SkipMetadataResolveMethod getSkipCQL4MetadataResolveMethod() {
226+
return this.skipCQL4MetadataResolveMethod;
227+
}
228+
196229
/**
197230
* Set whether the driver should prepare statements on all hosts in the cluster.
198231
*
@@ -583,4 +616,10 @@ public int hashCode() {
583616
public boolean isConsistencySet() {
584617
return consistencySet;
585618
}
619+
620+
public enum CQL4SkipMetadataResolveMethod {
621+
ALWAYS_ON,
622+
ALWAYS_OFF,
623+
SMART
624+
}
586625
}

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -654,11 +654,17 @@ 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).getSkipMetadata();
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+
}
667+
662668
Requests.QueryProtocolOptions options =
663669
new Requests.QueryProtocolOptions(
664670
Message.Request.Type.EXECUTE,

0 commit comments

Comments
 (0)