Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,19 @@
import static com.datastax.driver.core.ProtocolVersion.V4;

import com.datastax.driver.core.policies.RetryPolicy;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefaultPreparedStatement implements PreparedStatement {

private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPreparedStatement.class);
private static final String SCYLLA_CDC_LOG_SUFFIX = "_scylla_cdc_log";
private static final Splitter SPACE_SPLITTER = Splitter.onPattern("\\s+");
private static final Splitter COMMA_SPLITTER = Splitter.onPattern(",");

final PreparedId preparedId;

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

private DefaultPreparedStatement(
PreparedId id,
Expand All @@ -66,6 +72,7 @@ private DefaultPreparedStatement(
this.cluster = cluster;
this.isLWT = isLWT;
this.partitioner = partitioner;
this.skipMetadata = this.calculateSkipMetadata();
}

static DefaultPreparedStatement fromMessage(
Expand Down Expand Up @@ -172,6 +179,62 @@ private static Token.Factory partitioner(ColumnDefinitions defs, Cluster cluster
return null;
}

private boolean calculateSkipMetadata() {
if (cluster.manager.protocolVersion() == ProtocolVersion.V1
|| preparedId.resultSetMetadata.variables == null) {
// CQL1 does not support it.
// If no rows returned there is no reason to send this flag, consequently, no metadata.
return false;
}

if (preparedId.resultSetMetadata.id != null
&& preparedId.resultSetMetadata.id.bytes.length > 0) {
// It is CQL 5 or higher.
// Prepared statement invalidation works perfectly no need to disable skip metadata
return true;
}

switch (cluster.getConfiguration().getQueryOptions().getSkipCQL4MetadataResolveMethod()) {
case ENABLED:
return true;
case DISABLED:
return false;
}

if (isWildcardSelect(query)) {
LOGGER.warn(
"Prepared statement {} is a wildcard select, which can cause prepared statement invalidation issues when executed on CQL4. "
+ "These issues may lead to broken deserialization or data corruption. "
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
+ "though this negatively impacts performance. "
+ "To avoid this, consider using a targeted select instead. "
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
query);
return false;
}
// Disable skipping metadata if results contains udt and
for (ColumnDefinitions.Definition columnDefinition : preparedId.resultSetMetadata.variables) {
if (containsUDT(columnDefinition.getType())) {
LOGGER.warn(
"Prepared statement {} contains UDT in result, which can cause prepared statement invalidation issues when executed on CQL4. "
+ "These issues may lead to broken deserialization or data corruption. "
+ "To mitigate this, the driver ensures that the server returns metadata with each query for such statements, "
+ "though this negatively impacts performance. "
+ "To avoid this, consider using a targeted select instead. "
+ "Alternatively, you can enable the skip-cql4-metadata-resolve-method option in the execution profile by setting it to `always-on`, "
+ "allowing the driver to ignore this issue and proceed regardless, risking broken deserialization or data corruption.",
query);
return false;
}
}
return true;
}

public boolean isSkipMetadata() {
return skipMetadata;
}

@Override
public ColumnDefinitions getVariables() {
return preparedId.boundValuesMetadata.variables;
Expand Down Expand Up @@ -315,4 +378,44 @@ public Boolean isIdempotent() {
public boolean isLWT() {
return isLWT;
}

private static boolean containsUDT(DataType dataType) {
if (dataType.isCollection()) {
for (DataType elementType : dataType.getTypeArguments()) {
if (containsUDT(elementType)) {
return true;
}
}
return false;
}
return dataType instanceof UserType;
}

private static boolean isWildcardSelect(String query) {
List<String> chunks = SPACE_SPLITTER.splitToList(query.trim().toLowerCase());
if (chunks.size() < 2) {
// Weird query, assuming no result expected
return false;
}

if (!chunks.get(0).equals("select")) {
// In case if non-select sneaks in, disable skip metadata for it no result expected.
return false;
}

for (String chunk : chunks) {
if (chunk.equals("from")) {
return false;
}
if (chunk.equals("*")) {
return true;
}
for (String part : COMMA_SPLITTER.split(chunk)) {
if (part.equals("*")) {
return true;
}
}
}
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ public class QueryOptions {
private volatile boolean reprepareOnUp = true;
private volatile Cluster.Manager manager;
private volatile boolean prepareOnAllHosts = true;
private volatile CQL4SkipMetadataResolveMethod skipCQL4MetadataResolveMethod =
CQL4SkipMetadataResolveMethod.SMART;

private volatile boolean schemaQueriesPaged = true;

Expand Down Expand Up @@ -193,6 +195,38 @@ public boolean getDefaultIdempotence() {
return defaultIdempotence;
}

/**
* There is known problem in CQL 4.x when prepared statement invalidation could be voided: <a
* href="https://github.com/scylladb/scylladb/issues/20860">more info</a> When it happens metadata
* on client side does not match data and deserialization can go wrong in many ways To avoid
* driver can disable skip metadata flag to make server respond with metadata on every query.
* Unfortunately it causes excessive network traffic and CPU overhead on both server and driver
* side. This option controls how driver resolves skip metadata flag for CQL4 prepared statements.
* **SMART** - disable flag only for wildcard selects (select * from) and selects that return
* UDTs, including collections of UDTs and maps that contain UDTs **ENABLED** - flag is always set
* **DISABLED** - flag is always disabled Default is SMART Required: yes Modifiable at runtime:
* yes, the new value will be used for requests issued after the change. Overridable in a profile:
* yes
*
* @param method the new value to set as skip metadata resolve method.
* @return this {@code QueryOptions} instance.
*/
public QueryOptions setSkipCQL4MetadataResolveMethod(CQL4SkipMetadataResolveMethod method) {
this.skipCQL4MetadataResolveMethod = method;
return this;
}

/**
* Skip metadata resolve method .
*
* <p>It defaults to {@link #skipCQL4MetadataResolveMethod.SMART}.
*
* @return the default idempotence for queries.
*/
public CQL4SkipMetadataResolveMethod getSkipCQL4MetadataResolveMethod() {
return this.skipCQL4MetadataResolveMethod;
}

/**
* Set whether the driver should prepare statements on all hosts in the cluster.
*
Expand Down Expand Up @@ -583,4 +617,10 @@ public int hashCode() {
public boolean isConsistencySet() {
return consistencySet;
}

public enum CQL4SkipMetadataResolveMethod {
ENABLED,
DISABLED,
SMART
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -654,11 +654,16 @@ else if (fetchSize != Integer.MAX_VALUE)
}
if (protocolVersion.compareTo(ProtocolVersion.V4) < 0) bs.ensureAllSet();

// skip resultset metadata if version > 1 (otherwise this feature is not supported)
// and if we already have metadata for the prepared statement being executed.
boolean skipMetadata =
protocolVersion != ProtocolVersion.V1
&& bs.statement.getPreparedId().resultSetMetadata.variables != null;
boolean skipMetadata;
if (bs.statement instanceof DefaultPreparedStatement) {
skipMetadata = ((DefaultPreparedStatement) bs.statement).isSkipMetadata();
} else {
skipMetadata =
protocolVersion != ProtocolVersion.V1
&& bs.statement.getPreparedId().resultSetMetadata.variables != null;
// skip resultset metadata if version > 1 (otherwise this feature is not supported)
// and if we already have metadata for the prepared statement being executed.
}

Requests.QueryProtocolOptions options =
new Requests.QueryProtocolOptions(
Expand Down
Loading
Loading