Add guardrails for read/write consistency levels

patch by Andrés de la Peña; reviewed by Caleb Rackliffe for CASSANDRA-17188

Co-authored-by: Andrés de la Peña <a.penya.garcia@gmail.com>
Co-authored-by: Aleksandr Sorokoumov <aleksandr.sorokoumov@gmail.com>
This commit is contained in:
Andrés de la Peña 2022-03-11 15:36:39 +00:00
parent 2b2c6decfa
commit a41040ccdc
19 changed files with 1088 additions and 75 deletions

View File

@ -1,4 +1,5 @@
4.1
* Add guardrails for read/write consistency levels (CASSANDRA-17188)
* Add guardrail for SELECT IN terms and their cartesian product (CASSANDRA-17187)
* remove unused imports in cqlsh.py and cqlshlib (CASSANDRA-17413)
* deprecate property windows_timer_interval (CASSANDRA-17404)

View File

@ -1604,7 +1604,8 @@ drop_compact_storage_enabled: false
# The two thresholds default to -1 to disable.
# materialized_views_per_table_warn_threshold: -1
# materialized_views_per_table_fail_threshold: -1
# Guardrail to ignore or reject properties when creating tables. By default all properties are allowed.
# Guardrail to warn about, ignore or reject properties when creating tables. By default all properties are allowed.
# table_properties_warned: []
# table_properties_ignored: []
# table_properties_disallowed: []
# Guardrail to allow/disallow user-provided timestamps. Defaults to true.
@ -1625,6 +1626,12 @@ drop_compact_storage_enabled: false
# The two thresholds default to -1 to disable.
# in_select_cartesian_product_warn_threshold: -1
# in_select_cartesian_product_fail_threshold: -1
# Guardrail to warn about or reject read consistency levels. By default, all consistency levels are allowed.
# read_consistency_levels_warned: []
# read_consistency_levels_disallowed: []
# Guardrail to warn about or reject write consistency levels. By default, all consistency levels are allowed.
# write_consistency_levels_warned: []
# write_consistency_levels_disallowed: []
# Startup Checks are executed as part of Cassandra startup process, not all of them
# are configurable (so you can disable them) but these which are enumerated bellow.

View File

@ -788,8 +788,13 @@ public class Config
public volatile int partition_keys_in_select_fail_threshold = DISABLED_GUARDRAIL;
public volatile int in_select_cartesian_product_warn_threshold = DISABLED_GUARDRAIL;
public volatile int in_select_cartesian_product_fail_threshold = DISABLED_GUARDRAIL;
public volatile Set<String> table_properties_warned = Collections.emptySet();
public volatile Set<String> table_properties_ignored = Collections.emptySet();
public volatile Set<String> table_properties_disallowed = Collections.emptySet();
public volatile Set<ConsistencyLevel> read_consistency_levels_warned = Collections.emptySet();
public volatile Set<ConsistencyLevel> read_consistency_levels_disallowed = Collections.emptySet();
public volatile Set<ConsistencyLevel> write_consistency_levels_warned = Collections.emptySet();
public volatile Set<ConsistencyLevel> write_consistency_levels_disallowed = Collections.emptySet();
public volatile boolean user_timestamps_enabled = true;
public volatile boolean read_before_write_list_operations_enabled = true;

View File

@ -18,6 +18,7 @@
package org.apache.cassandra.config;
import java.util.Collections;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Supplier;
@ -27,6 +28,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.cql3.statements.schema.TableAttributes;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.guardrails.GuardrailsConfig;
@ -64,12 +66,17 @@ public class GuardrailsOptions implements GuardrailsConfig
validateIntThreshold(config.columns_per_table_warn_threshold, config.columns_per_table_fail_threshold, "columns_per_table");
validateIntThreshold(config.secondary_indexes_per_table_warn_threshold, config.secondary_indexes_per_table_fail_threshold, "secondary_indexes_per_table");
validateIntThreshold(config.materialized_views_per_table_warn_threshold, config.materialized_views_per_table_fail_threshold, "materialized_views_per_table");
config.table_properties_warned = validateTableProperties(config.table_properties_warned, "table_properties_warned");
config.table_properties_ignored = validateTableProperties(config.table_properties_ignored, "table_properties_ignored");
config.table_properties_disallowed = validateTableProperties(config.table_properties_disallowed, "table_properties_disallowed");
validateIntThreshold(config.page_size_warn_threshold, config.page_size_fail_threshold, "page_size");
validateIntThreshold(config.partition_keys_in_select_warn_threshold,
config.partition_keys_in_select_fail_threshold, "partition_keys_in_select");
validateIntThreshold(config.in_select_cartesian_product_warn_threshold, config.in_select_cartesian_product_fail_threshold, "in_select_cartesian_product");
config.read_consistency_levels_warned = validateConsistencyLevels(config.read_consistency_levels_warned, "read_consistency_levels_warned");
config.read_consistency_levels_disallowed = validateConsistencyLevels(config.read_consistency_levels_disallowed, "read_consistency_levels_disallowed");
config.write_consistency_levels_warned = validateConsistencyLevels(config.write_consistency_levels_warned, "write_consistency_levels_warned");
config.write_consistency_levels_disallowed = validateConsistencyLevels(config.write_consistency_levels_disallowed, "write_consistency_levels_disallowed");
}
@Override
@ -266,6 +273,20 @@ public class GuardrailsOptions implements GuardrailsConfig
x -> config.page_size_fail_threshold = x);
}
@Override
public Set<String> getTablePropertiesWarned()
{
return config.table_properties_warned;
}
public void setTablePropertiesWarned(Set<String> properties)
{
updatePropertyWithLogging("table_properties_warned",
validateTableProperties(properties, "table_properties_warned"),
() -> config.table_properties_warned,
x -> config.table_properties_warned = x);
}
@Override
public Set<String> getTablePropertiesIgnored()
{
@ -347,6 +368,61 @@ public class GuardrailsOptions implements GuardrailsConfig
x -> config.in_select_cartesian_product_fail_threshold = x);
}
public Set<ConsistencyLevel> getReadConsistencyLevelsWarned()
{
return config.read_consistency_levels_warned;
}
public void setReadConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels)
{
updatePropertyWithLogging("read_consistency_levels_warned",
validateConsistencyLevels(consistencyLevels, "read_consistency_levels_warned"),
() -> config.read_consistency_levels_warned,
x -> config.read_consistency_levels_warned = x);
}
@Override
public Set<ConsistencyLevel> getReadConsistencyLevelsDisallowed()
{
return config.read_consistency_levels_disallowed;
}
public void setReadConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels)
{
updatePropertyWithLogging("read_consistency_levels_disallowed",
validateConsistencyLevels(consistencyLevels, "read_consistency_levels_disallowed"),
() -> config.read_consistency_levels_disallowed,
x -> config.read_consistency_levels_disallowed = x);
}
@Override
public Set<ConsistencyLevel> getWriteConsistencyLevelsWarned()
{
return config.write_consistency_levels_warned;
}
public void setWriteConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels)
{
updatePropertyWithLogging("write_consistency_levels_warned",
validateConsistencyLevels(consistencyLevels, "write_consistency_levels_warned"),
() -> config.write_consistency_levels_warned,
x -> config.write_consistency_levels_warned = x);
}
@Override
public Set<ConsistencyLevel> getWriteConsistencyLevelsDisallowed()
{
return config.write_consistency_levels_disallowed;
}
public void setWriteConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels)
{
updatePropertyWithLogging("write_consistency_levels_disallowed",
validateConsistencyLevels(consistencyLevels, "write_consistency_levels_disallowed"),
() -> config.write_consistency_levels_disallowed,
x -> config.write_consistency_levels_disallowed = x);
}
private static <T> void updatePropertyWithLogging(String propertyName, T newValue, Supplier<T> getter, Consumer<T> setter)
{
T oldValue = getter.get();
@ -415,4 +491,12 @@ public class GuardrailsOptions implements GuardrailsConfig
return lowerCaseProperties;
}
private static Set<ConsistencyLevel> validateConsistencyLevels(Set<ConsistencyLevel> consistencyLevels, String name)
{
if (consistencyLevels == null)
throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
return consistencyLevels.isEmpty() ? Collections.emptySet() : Sets.immutableEnumSet(consistencyLevels);
}
}

View File

@ -31,6 +31,7 @@ import org.slf4j.helpers.MessageFormatter;
import org.apache.cassandra.audit.AuditLogContext;
import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.schema.TableId;
import org.apache.cassandra.schema.TableMetadata;
import org.apache.cassandra.schema.ColumnMetadata;
@ -410,6 +411,9 @@ public class BatchStatement implements CQLStatement
if (options.getSerialConsistency() == null)
throw new InvalidRequestException("Invalid empty serial consistency level");
Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()),
queryState.getClientState());
if (hasConditions)
return executeWithConditions(options, queryState, queryStartNanoTime);

View File

@ -460,6 +460,9 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
if (options.getConsistency() == null)
throw new InvalidRequestException("Invalid empty consistency level");
Guardrails.writeConsistencyLevels.guard(EnumSet.of(options.getConsistency(), options.getSerialConsistency()),
queryState.getClientState());
return hasConditions()
? executeWithCondition(queryState, options, queryStartNanoTime)
: executeWithoutCondition(queryState, options, queryStartNanoTime);

View File

@ -240,6 +240,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
checkNotNull(cl, "Invalid empty consistency level");
cl.validateForRead();
Guardrails.readConsistencyLevels.guard(EnumSet.of(cl), state.getClientState());
int nowInSec = options.getNowInSeconds(state);
int userLimit = getLimit(options);

View File

@ -18,6 +18,8 @@
package org.apache.cassandra.db;
import java.util.Locale;
import com.carrotsearch.hppc.ObjectIntHashMap;
import org.apache.cassandra.locator.Endpoints;
import org.apache.cassandra.locator.InOurDc;
@ -81,6 +83,11 @@ public enum ConsistencyLevel
return codeIdx[code];
}
public static ConsistencyLevel fromString(String str)
{
return valueOf(str.toUpperCase(Locale.US));
}
public static int quorumFor(AbstractReplicationStrategy replicationStrategy)
{
return (replicationStrategy.getReplicationFactor().allReplicas / 2) + 1;

View File

@ -18,13 +18,18 @@
package org.apache.cassandra.db.guardrails;
import java.util.Collections;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.GuardrailsOptions;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.utils.MBeanWrapper;
@ -101,10 +106,11 @@ public final class Guardrails implements GuardrailsMBean
threshold, what));
/**
* Guardrail ignoring/disallowing the usage of certain table properties.
* Guardrail warning about, ignoring or rejecting the usage of certain table properties.
*/
public static final Values<String> tableProperties =
new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesIgnored(),
new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesWarned(),
state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesIgnored(),
state -> CONFIG_PROVIDER.getOrCreate(state).getTablePropertiesDisallowed(),
"Table Properties");
@ -161,6 +167,23 @@ public final class Guardrails implements GuardrailsMBean
: format("Aborting query because the cartesian product of the IN restrictions on %s " +
"produces %d values, this exceeds fail threshold of %s.",
what, value, threshold));
/**
* Guardrail on read consistency levels.
*/
public static final Values<ConsistencyLevel> readConsistencyLevels =
new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getReadConsistencyLevelsWarned(),
state -> Collections.emptySet(),
state -> CONFIG_PROVIDER.getOrCreate(state).getReadConsistencyLevelsDisallowed(),
"read consistency levels");
/**
* Guardrail on write consistency levels.
*/
public static final Values<ConsistencyLevel> writeConsistencyLevels =
new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getWriteConsistencyLevelsWarned(),
state -> Collections.emptySet(),
state -> CONFIG_PROVIDER.getOrCreate(state).getWriteConsistencyLevelsDisallowed(),
"write consistency levels");
private Guardrails()
{
@ -280,6 +303,35 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setMaterializedViewsPerTableThreshold(warn, fail);
}
@Override
public Set<String> getTablePropertiesWarned()
{
return DEFAULT_CONFIG.getTablePropertiesWarned();
}
@Override
public String getTablePropertiesWarnedCSV()
{
return toCSV(DEFAULT_CONFIG.getTablePropertiesWarned());
}
public void setTablePropertiesWarned(String... properties)
{
setTablePropertiesWarned(ImmutableSet.copyOf(properties));
}
@Override
public void setTablePropertiesWarned(Set<String> properties)
{
DEFAULT_CONFIG.setTablePropertiesWarned(properties);
}
@Override
public void setTablePropertiesWarnedCSV(String properties)
{
setTablePropertiesWarned(fromCSV(properties));
}
@Override
public Set<String> getTablePropertiesDisallowed()
{
@ -368,6 +420,7 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setPageSizeThreshold(warn, fail);
}
@Override
public boolean getReadBeforeWriteListOperationsEnabled()
{
return DEFAULT_CONFIG.getReadBeforeWriteListOperationsEnabled();
@ -415,13 +468,118 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setInSelectCartesianProductThreshold(warn, fail);
}
public Set<ConsistencyLevel> getReadConsistencyLevelsWarned()
{
return DEFAULT_CONFIG.getReadConsistencyLevelsWarned();
}
@Override
public String getReadConsistencyLevelsWarnedCSV()
{
return toCSV(DEFAULT_CONFIG.getReadConsistencyLevelsWarned(), ConsistencyLevel::toString);
}
@Override
public void setReadConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels)
{
DEFAULT_CONFIG.setReadConsistencyLevelsWarned(consistencyLevels);
}
@Override
public void setReadConsistencyLevelsWarnedCSV(String consistencyLevels)
{
DEFAULT_CONFIG.setReadConsistencyLevelsWarned(fromCSV(consistencyLevels, ConsistencyLevel::fromString));
}
@Override
public Set<ConsistencyLevel> getReadConsistencyLevelsDisallowed()
{
return DEFAULT_CONFIG.getReadConsistencyLevelsDisallowed();
}
@Override
public String getReadConsistencyLevelsDisallowedCSV()
{
return toCSV(DEFAULT_CONFIG.getReadConsistencyLevelsDisallowed(), ConsistencyLevel::toString);
}
@Override
public void setReadConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels)
{
DEFAULT_CONFIG.setReadConsistencyLevelsDisallowed(consistencyLevels);
}
@Override
public void setReadConsistencyLevelsDisallowedCSV(String consistencyLevels)
{
DEFAULT_CONFIG.setReadConsistencyLevelsDisallowed(fromCSV(consistencyLevels, ConsistencyLevel::fromString));
}
@Override
public Set<ConsistencyLevel> getWriteConsistencyLevelsWarned()
{
return DEFAULT_CONFIG.getWriteConsistencyLevelsWarned();
}
@Override
public String getWriteConsistencyLevelsWarnedCSV()
{
return toCSV(DEFAULT_CONFIG.getWriteConsistencyLevelsWarned(), ConsistencyLevel::toString);
}
@Override
public void setWriteConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels)
{
DEFAULT_CONFIG.setWriteConsistencyLevelsWarned(consistencyLevels);
}
@Override
public void setWriteConsistencyLevelsWarnedCSV(String consistencyLevels)
{
DEFAULT_CONFIG.setWriteConsistencyLevelsWarned(fromCSV(consistencyLevels, ConsistencyLevel::fromString));
}
@Override
public Set<ConsistencyLevel> getWriteConsistencyLevelsDisallowed()
{
return DEFAULT_CONFIG.getWriteConsistencyLevelsDisallowed();
}
@Override
public String getWriteConsistencyLevelsDisallowedCSV()
{
return toCSV(DEFAULT_CONFIG.getWriteConsistencyLevelsDisallowed(), ConsistencyLevel::toString);
}
@Override
public void setWriteConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels)
{
DEFAULT_CONFIG.setWriteConsistencyLevelsDisallowed(consistencyLevels);
}
@Override
public void setWriteConsistencyLevelsDisallowedCSV(String consistencyLevels)
{
DEFAULT_CONFIG.setWriteConsistencyLevelsDisallowed(fromCSV(consistencyLevels, ConsistencyLevel::fromString));
}
private static String toCSV(Set<String> values)
{
return values == null ? "" : String.join(",", values);
return values == null || values.isEmpty() ? "" : String.join(",", values);
}
private static <T> String toCSV(Set<T> values, Function<T, String> formatter)
{
return values == null || values.isEmpty() ? "" : values.stream().map(formatter).collect(Collectors.joining(","));
}
private static Set<String> fromCSV(String csv)
{
return csv == null ? null : ImmutableSet.copyOf(csv.split(","));
return StringUtils.isEmpty(csv) ? Collections.emptySet() : ImmutableSet.copyOf(csv.split(","));
}
private static <T> Set<T> fromCSV(String csv, Function<String, T> parser)
{
return StringUtils.isEmpty(csv) ? Collections.emptySet() : fromCSV(csv).stream().map(parser).collect(Collectors.toSet());
}
}

View File

@ -20,6 +20,8 @@ package org.apache.cassandra.db.guardrails;
import java.util.Set;
import org.apache.cassandra.db.ConsistencyLevel;
/**
* Configuration settings for guardrails.
*
@ -111,6 +113,11 @@ public interface GuardrailsConfig
*/
int getMaterializedViewsPerTableFailThreshold();
/**
* @return The table properties that are warned about when creating or altering a table.
*/
Set<String> getTablePropertiesWarned();
/**
* @return The table properties that are ignored when creating or altering a table.
*/
@ -156,4 +163,24 @@ public interface GuardrailsConfig
* -1 means disabled.
*/
public int getInSelectCartesianProductFailThreshold();
/**
* @return The consistency levels that are warned about when reading.
*/
Set<ConsistencyLevel> getReadConsistencyLevelsWarned();
/**
* @return The consistency levels that are disallowed when reading.
*/
Set<ConsistencyLevel> getReadConsistencyLevelsDisallowed();
/**
* @return The consistency levels that are warned about when writing.
*/
Set<ConsistencyLevel> getWriteConsistencyLevelsWarned();
/**
* @return The consistency levels that are disallowed when writing.
*/
Set<ConsistencyLevel> getWriteConsistencyLevelsDisallowed();
}

View File

@ -20,6 +20,8 @@ package org.apache.cassandra.db.guardrails;
import java.util.Set;
import org.apache.cassandra.db.ConsistencyLevel;
/**
* JMX entrypoint for updating the default guardrails configuration parsed from {@code cassandra.yaml}.
* <p>
@ -135,6 +137,26 @@ public interface GuardrailsMBean
*/
void setMaterializedViewsPerTableThreshold(int warn, int fail);
/**
* @return properties that are warned about when creating or altering a table.
*/
Set<String> getTablePropertiesWarned();
/**
* @return Comma-separated list of properties that are warned about when creating or altering a table.
*/
String getTablePropertiesWarnedCSV();
/**
* @param properties properties that are warned about when creating or altering a table.
*/
void setTablePropertiesWarned(Set<String> properties);
/**
* @param properties Comma-separated list of properties that are warned about when creating or altering a table.
*/
void setTablePropertiesWarnedCSV(String properties);
/**
* @return properties that are not allowed when creating or altering a table.
*/
@ -260,4 +282,84 @@ public interface GuardrailsMBean
* -1 means disabled.
*/
public void setInSelectCartesianProductThreshold(int warn, int fail);
/**
* @return consistency levels that are warned about when reading.
*/
Set<ConsistencyLevel> getReadConsistencyLevelsWarned();
/**
* @return Comma-separated list of consistency levels that are warned about when reading.
*/
String getReadConsistencyLevelsWarnedCSV();
/**
* @param consistencyLevels consistency levels that are warned about when reading.
*/
void setReadConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels);
/**
* @param consistencyLevels Comma-separated list of consistency levels that are warned about when reading.
*/
void setReadConsistencyLevelsWarnedCSV(String consistencyLevels);
/**
* @return consistency levels that are not allowed when reading.
*/
Set<ConsistencyLevel> getReadConsistencyLevelsDisallowed();
/**
* @return Comma-separated list of consistency levels that are not allowed when reading.
*/
String getReadConsistencyLevelsDisallowedCSV();
/**
* @param consistencyLevels consistency levels that are not allowed when reading.
*/
void setReadConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels);
/**
* @param consistencyLevels Comma-separated list of consistency levels that are not allowed when reading.
*/
void setReadConsistencyLevelsDisallowedCSV(String consistencyLevels);
/**
* @return consistency levels that are warned about when writing.
*/
Set<ConsistencyLevel> getWriteConsistencyLevelsWarned();
/**
* @return Comma-separated list of consistency levels that are warned about when writing.
*/
String getWriteConsistencyLevelsWarnedCSV();
/**
* @param consistencyLevels consistency levels that are warned about when writing.
*/
void setWriteConsistencyLevelsWarned(Set<ConsistencyLevel> consistencyLevels);
/**
* @param consistencyLevels Comma-separated list of consistency levels that are warned about when writing.
*/
void setWriteConsistencyLevelsWarnedCSV(String consistencyLevels);
/**
* @return consistency levels that are not allowed when writing.
*/
Set<ConsistencyLevel> getWriteConsistencyLevelsDisallowed();
/**
* @return Comma-separated list of consistency levels that are not allowed when writing.
*/
String getWriteConsistencyLevelsDisallowedCSV();
/**
* @param consistencyLevels consistency levels that are not allowed when writing.
*/
void setWriteConsistencyLevelsDisallowed(Set<ConsistencyLevel> consistencyLevels);
/**
* @param consistencyLevels Comma-separated list of consistency levels that are not allowed when writing.
*/
void setWriteConsistencyLevelsDisallowedCSV(String consistencyLevels);
}

View File

@ -31,12 +31,14 @@ import org.apache.cassandra.service.ClientState;
import static java.lang.String.format;
/**
* A guardrail that warns about but ignores some specific values, and rejects the use of some other values.
* A guardrail that warns about some specific values, warns about but ignores some other values, and/or rejects the use
* of some other values.
*
* @param <T> The type of the values of which certain are disallowed.
* @param <T> The type of the values of which certain are warned, ignored and/or disallowed.
*/
public class Values<T> extends Guardrail
{
private final Function<ClientState, Set<T>> warnedValues;
private final Function<ClientState, Set<T>> ignoredValues;
private final Function<ClientState, Set<T>> disallowedValues;
private final String what;
@ -44,22 +46,45 @@ public class Values<T> extends Guardrail
/**
* Creates a new values guardrail.
*
* @param ignoredValues a {@link ClientState}-based of the values that are ignored.
* @param disallowedValues a {@link ClientState}-based of the values that are disallowed.
* @param warnedValues a {@link ClientState}-based provider of the values for which a warning is triggered.
* @param ignoredValues a {@link ClientState}-based provider of the values that are ignored.
* @param disallowedValues a {@link ClientState}-based provider of the values that are disallowed.
* @param what The feature that is guarded by this guardrail (for reporting in error messages).
*/
public Values(Function<ClientState, Set<T>> ignoredValues,
public Values(Function<ClientState, Set<T>> warnedValues,
Function<ClientState, Set<T>> ignoredValues,
Function<ClientState, Set<T>> disallowedValues,
String what)
{
this.warnedValues = warnedValues;
this.ignoredValues = ignoredValues;
this.disallowedValues = disallowedValues;
this.what = what;
}
/**
* Triggers a warning for each of the provided values that are disallowed by this guardrail and triggers an action
* to ignore them. If the values are disallowed it will abort the operation.
* Triggers a warning for each of the provided values that is discouraged by this guardrail. If any of the values
* is disallowed it will abort the operation.
* <p>
* This assumes that there aren't any values to be ignored, thus it doesn't require an ignore action. If this is
* not the case and the provided value is set up to be ignored this will throw an assertion error.
*
* @param values The values to check.
* @param state The client state, used to skip the check if the query is internal or is done by a superuser.
* A {@code null} value means that the check should be done regardless of the query.
*/
public void guard(Set<T> values, @Nullable ClientState state)
{
guard(values, x -> {
throw new AssertionError(format("There isn't an ignore action for %s, but value %s is setup to be ignored",
what, x));
}, state);
}
/**
* Triggers a warning for each of the provided values that is discouraged by this guardrail. Also triggers a warning
* for each of the provided values that is ignored by this guardrail and triggers the provided action to ignore it.
* If any of the values is disallowed it will abort the operation.
*
* @param values The values to check.
* @param ignoreAction An action called on the subset of {@code values} that should be ignored. This action
@ -86,5 +111,11 @@ public class Values<T> extends Guardrail
toIgnore.stream().sorted().collect(Collectors.toList()), what, ignored));
toIgnore.forEach(ignoreAction);
}
Set<T> warned = warnedValues.apply(state);
Set<T> toWarn = Sets.intersection(values, warned);
if (!toWarn.isEmpty())
warn(format("Provided values %s are not recommended for %s (warned values are: %s)",
toWarn.stream().sorted().collect(Collectors.toList()), what, warned));
}
}

View File

@ -97,10 +97,12 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$OutgoingEncryptedPortSource",
"org.apache.cassandra.db.guardrails.GuardrailsConfig",
"org.apache.cassandra.db.guardrails.GuardrailsConfigMBean",
"org.apache.cassandra.db.guardrails.GuardrailsConfig$ConsistencyLevels",
"org.apache.cassandra.db.guardrails.GuardrailsConfig$IntThreshold",
"org.apache.cassandra.db.guardrails.GuardrailsConfig$TableProperties",
"org.apache.cassandra.config.GuardrailsOptions",
"org.apache.cassandra.config.GuardrailsOptions$Config",
"org.apache.cassandra.config.GuardrailsOptions$ConsistencyLevels",
"org.apache.cassandra.config.GuardrailsOptions$IntThreshold",
"org.apache.cassandra.config.GuardrailsOptions$TableProperties",
"org.apache.cassandra.config.GuardrailsOptions$Threshold",

View File

@ -0,0 +1,131 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.db.ConsistencyLevel;
/**
* Utilty class for testing the guardrails for read/write consistency levels.
*/
public abstract class GuardrailConsistencyLevelsTester extends GuardrailTester
{
private final String warnedPropertyName;
private final String disallowePropertyName;
private final Function<Guardrails, Set<ConsistencyLevel>> warnedGetter;
private final Function<Guardrails, Set<ConsistencyLevel>> disallowedGetter;
private final Function<Guardrails, String> warnedCSVGetter;
private final Function<Guardrails, String> disallowedCSVGetter;
private final BiConsumer<Guardrails, Set<ConsistencyLevel>> warnedSetter;
private final BiConsumer<Guardrails, Set<ConsistencyLevel>> disallowedSetter;
private final BiConsumer<Guardrails, String> warnedCSVSetter;
private final BiConsumer<Guardrails, String> disallowedCSVSetter;
public GuardrailConsistencyLevelsTester(String warnedPropertyName,
String disallowePropertyName,
Function<Guardrails, Set<ConsistencyLevel>> warnedGetter,
Function<Guardrails, Set<ConsistencyLevel>> disallowedGetter,
Function<Guardrails, String> warnedCSVGetter,
Function<Guardrails, String> disallowedCSVGetter,
BiConsumer<Guardrails, Set<ConsistencyLevel>> warnedSetter,
BiConsumer<Guardrails, Set<ConsistencyLevel>> disallowedSetter,
BiConsumer<Guardrails, String> warnedCSVSetter,
BiConsumer<Guardrails, String> disallowedCSVSetter)
{
this.warnedPropertyName = warnedPropertyName;
this.disallowePropertyName = disallowePropertyName;
this.warnedGetter = warnedGetter;
this.disallowedGetter = disallowedGetter;
this.warnedCSVGetter = g -> sortCSV(warnedCSVGetter.apply(g));
this.disallowedCSVGetter = g -> sortCSV(disallowedCSVGetter.apply(g));
this.warnedSetter = warnedSetter;
this.disallowedSetter = disallowedSetter;
this.warnedCSVSetter = warnedCSVSetter;
this.disallowedCSVSetter = disallowedCSVSetter;
}
@Before
public void before()
{
warnConsistencyLevels();
disableConsistencyLevels();
}
protected void warnConsistencyLevels(ConsistencyLevel... consistencyLevels)
{
warnedSetter.accept(guardrails(), ImmutableSet.copyOf(consistencyLevels));
}
protected void disableConsistencyLevels(ConsistencyLevel... consistencyLevels)
{
disallowedSetter.accept(guardrails(), ImmutableSet.copyOf(consistencyLevels));
}
@Test
public void testConfigValidation()
{
String message = "Invalid value for %s: null is not allowed";
assertInvalidProperty(warnedSetter, null, message, warnedPropertyName);
assertInvalidProperty(disallowedSetter, null, message, disallowePropertyName);
assertValidProperty(Collections.emptySet());
assertValidProperty(EnumSet.allOf(ConsistencyLevel.class));
assertValidPropertyCSV("");
assertValidPropertyCSV(EnumSet.allOf(ConsistencyLevel.class)
.stream()
.map(ConsistencyLevel::toString)
.collect(Collectors.joining(",")));
assertInvalidPropertyCSV("invalid", "INVALID");
assertInvalidPropertyCSV("ONE,invalid1,invalid2", "INVALID1");
assertInvalidPropertyCSV("invalid1,invalid2,ONE", "INVALID1");
assertInvalidPropertyCSV("invalid1,ONE,invalid2", "INVALID1");
}
private void assertValidProperty(Set<ConsistencyLevel> properties)
{
assertValidProperty(warnedSetter, warnedGetter, properties);
assertValidProperty(disallowedSetter, disallowedGetter, properties);
}
private void assertValidPropertyCSV(String csv)
{
csv = sortCSV(csv);
assertValidProperty(warnedCSVSetter, warnedCSVGetter, csv);
assertValidProperty(disallowedCSVSetter, disallowedCSVGetter, csv);
}
private void assertInvalidPropertyCSV(String properties, String rejected)
{
String message = "No enum constant org.apache.cassandra.db.ConsistencyLevel.%s";
assertInvalidProperty(warnedCSVSetter, properties, message, rejected);
assertInvalidProperty(disallowedCSVSetter, properties, message, rejected);
}
}

View File

@ -0,0 +1,118 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import org.junit.Test;
import org.apache.cassandra.db.ConsistencyLevel;
import static java.lang.String.format;
import static org.apache.cassandra.db.ConsistencyLevel.ALL;
import static org.apache.cassandra.db.ConsistencyLevel.EACH_QUORUM;
import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_ONE;
import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_QUORUM;
import static org.apache.cassandra.db.ConsistencyLevel.ONE;
import static org.apache.cassandra.db.ConsistencyLevel.QUORUM;
/**
* Tests the guardrail for read consistency levels, {@link Guardrails#readConsistencyLevels}.
*/
public class GuardrailReadConsistencyLevelsTest extends GuardrailConsistencyLevelsTester
{
public GuardrailReadConsistencyLevelsTest()
{
super("read_consistency_levels_warned",
"read_consistency_levels_disallowed",
Guardrails::getReadConsistencyLevelsWarned,
Guardrails::getReadConsistencyLevelsDisallowed,
Guardrails::getReadConsistencyLevelsWarnedCSV,
Guardrails::getReadConsistencyLevelsDisallowedCSV,
Guardrails::setReadConsistencyLevelsWarned,
Guardrails::setReadConsistencyLevelsDisallowed,
Guardrails::setReadConsistencyLevelsWarnedCSV,
Guardrails::setReadConsistencyLevelsDisallowedCSV);
}
@Test
public void testSelect() throws Throwable
{
createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v INT, PRIMARY KEY(k, c))");
execute("INSERT INTO %s (k, c, v) VALUES (0, 0, 0)");
execute("INSERT INTO %s (k, c, v) VALUES (0, 1, 1)");
execute("INSERT INTO %s (k, c, v) VALUES (1, 0, 2)");
execute("INSERT INTO %s (k, c, v) VALUES (1, 1, 3)");
testQuery("SELECT * FROM %s");
testQuery("SELECT * FROM %s WHERE k = 0");
testQuery("SELECT * FROM %s WHERE k = 0 AND c = 0");
}
private void testQuery(String query) throws Throwable
{
testQuery(query, ONE);
testQuery(query, ALL);
testQuery(query, QUORUM);
testQuery(query, EACH_QUORUM);
testQuery(query, LOCAL_ONE);
testQuery(query, LOCAL_QUORUM);
}
private void testQuery(String query, ConsistencyLevel cl) throws Throwable
{
warnConsistencyLevels();
disableConsistencyLevels();
assertValid(query, cl);
warnConsistencyLevels(cl);
assertWarns(query, cl);
disableConsistencyLevels(cl);
assertFails(query, cl);
}
private void assertValid(String query, ConsistencyLevel cl) throws Throwable
{
assertValid(() -> execute(userClientState, query, cl));
}
private void assertWarns(String query, ConsistencyLevel cl) throws Throwable
{
assertWarns(() -> execute(userClientState, query, cl),
format("Provided values [%s] are not recommended for read consistency levels (warned values are: %s)",
cl, guardrails().getReadConsistencyLevelsWarned()));
assertExcludedUsers(query, cl);
}
private void assertFails(String query, ConsistencyLevel cl) throws Throwable
{
assertFails(() -> execute(userClientState, query, cl),
format("Provided values [%s] are not allowed for read consistency levels (disallowed values are: %s)",
cl, guardrails().getReadConsistencyLevelsDisallowed()));
assertExcludedUsers(query, cl);
}
private void assertExcludedUsers(String query, ConsistencyLevel cl) throws Throwable
{
assertValid(() -> execute(superClientState, query, cl));
assertValid(() -> execute(systemClientState, query, cl));
}
}

View File

@ -45,51 +45,81 @@ public class GuardrailTablePropertiesTest extends GuardrailTester
"WHERE pk IS NOT null and ck IS NOT null PRIMARY KEY(ck, pk) %s";
private static final String ALTER_VIEW = "ALTER MATERIALIZED VIEW %s.%s WITH %s";
private static final String WARNED_PROPERTY_NAME = "table_properties_warned";
private static final String IGNORED_PROPERTY_NAME = "table_properties_ignored";
private static final String DISALLOWED_PROPERTY_NAME = "table_properties_disallowed";
@Before
public void before()
{
// only allow "gc_grace_seconds" and "comments"
Set<String> allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment"));
// only allow "gc_grace_seconds", "comments" and "default_time_to_live"
Set<String> allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment", "default_time_to_live"));
guardrails().setTablePropertiesDisallowed(TableAttributes.validKeywords()
.stream()
.filter(p -> !allowed.contains(p))
.map(String::toUpperCase)
.collect(Collectors.toSet()));
// but actually warn about "comment"
guardrails().setTablePropertiesIgnored(Collections.singleton("comment"));
// but actually ignore "comment" and warn about "default_time_to_live"
guardrails().setTablePropertiesIgnored("comment");
guardrails().setTablePropertiesWarned("default_time_to_live");
}
@Test
public void testConfigValidation()
{
String message = "Invalid value for %s: null is not allowed";
assertInvalidProperty(Guardrails::setTablePropertiesWarned, (Set<String>) null, message, WARNED_PROPERTY_NAME);
assertInvalidProperty(Guardrails::setTablePropertiesIgnored, (Set<String>) null, message, IGNORED_PROPERTY_NAME);
assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, (Set<String>) null, message, DISALLOWED_PROPERTY_NAME);
assertValidProperty(Collections.emptySet());
assertValidProperty(TableAttributes.allKeywords());
assertValidPropertyCSV("");
assertValidPropertyCSV(String.join(",", TableAttributes.allKeywords()));
assertInvalidProperty(Collections.singleton("invalid"), Collections.singleton("invalid"));
assertInvalidProperty(ImmutableSet.of("comment", "invalid1", "invalid2"), ImmutableSet.of("invalid1", "invalid2"));
assertInvalidProperty(ImmutableSet.of("invalid1", "invalid2", "comment"), ImmutableSet.of("invalid1", "invalid2"));
assertInvalidProperty(ImmutableSet.of("invalid1", "comment", "invalid2"), ImmutableSet.of("invalid1", "invalid2"));
assertInvalidPropertyCSV("invalid", "[invalid]");
assertInvalidPropertyCSV("comment,invalid1,invalid2", "[invalid1, invalid2]");
assertInvalidPropertyCSV("invalid1,invalid2,comment", "[invalid1, invalid2]");
assertInvalidPropertyCSV("invalid1,comment,invalid2", "[invalid1, invalid2]");
}
private void assertValidProperty(Set<String> properties)
{
assertValidProperty(Guardrails::setTablePropertiesIgnored, properties);
assertValidProperty(Guardrails::setTablePropertiesDisallowed, properties);
assertValidProperty(Guardrails::setTablePropertiesWarned, Guardrails::getTablePropertiesWarned, properties);
assertValidProperty(Guardrails::setTablePropertiesIgnored, Guardrails::getTablePropertiesIgnored, properties);
assertValidProperty(Guardrails::setTablePropertiesDisallowed, Guardrails::getTablePropertiesDisallowed, properties);
}
private void assertValidPropertyCSV(String csv)
{
csv = sortCSV(csv);
assertValidProperty(Guardrails::setTablePropertiesWarnedCSV, g -> sortCSV(g.getTablePropertiesWarnedCSV()), csv);
assertValidProperty(Guardrails::setTablePropertiesIgnoredCSV, g -> sortCSV(g.getTablePropertiesIgnoredCSV()), csv);
assertValidProperty(Guardrails::setTablePropertiesDisallowedCSV, g -> sortCSV(g.getTablePropertiesDisallowedCSV()), csv);
}
private void assertInvalidProperty(Set<String> properties, Set<String> rejected)
{
String message = "Invalid value for %s: '%s' do not parse as valid table properties";
assertInvalidProperty(Guardrails::setTablePropertiesWarned, properties, message, WARNED_PROPERTY_NAME, rejected);
assertInvalidProperty(Guardrails::setTablePropertiesIgnored, properties, message, IGNORED_PROPERTY_NAME, rejected);
assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, properties, message, DISALLOWED_PROPERTY_NAME, rejected);
}
private void assertInvalidPropertyCSV(String properties, String rejected)
{
String message = "Invalid value for %s: '%s' do not parse as valid table properties";
assertInvalidProperty(Guardrails::setTablePropertiesWarnedCSV, properties, message, WARNED_PROPERTY_NAME, rejected);
assertInvalidProperty(Guardrails::setTablePropertiesIgnoredCSV, properties, message, IGNORED_PROPERTY_NAME, rejected);
assertInvalidProperty(Guardrails::setTablePropertiesDisallowedCSV, properties, message, DISALLOWED_PROPERTY_NAME, rejected);
}
@Test
public void testTableProperties() throws Throwable
{
@ -112,6 +142,13 @@ public class GuardrailTablePropertiesTest extends GuardrailTester
keyspace(),
tableName.get()).one().getString("comment"));
// default_time_to_live is "warned". So it should warn, and getting the default ttl on the created table should
// not be empty, since we don't ignore it.
assertWarns(() -> tableName.set(createTableWithProperties("with default_time_to_live = 1000")), "[default_time_to_live]");
assertEquals(1000, executeNet("SELECT default_time_to_live FROM system_schema.tables WHERE keyspace_name=? AND table_name=?",
keyspace(),
tableName.get()).one().getInt("default_time_to_live"));
// alter column is allowed
assertValid(this::createTableWithProperties);
assertValid("ALTER TABLE %s ADD v1 int");
@ -130,8 +167,6 @@ public class GuardrailTablePropertiesTest extends GuardrailTester
// alter mv properties except "gc_grace_seconds" is not allowed
assertValid(() -> alterViewWithProperties("gc_grace_seconds = 1000"));
assertFails(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND default_time_to_live = 1"),
"[compaction, default_time_to_live]");
assertFails(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND crc_check_chance = 1"),
"[compaction, crc_check_chance]");
}

View File

@ -22,11 +22,15 @@ import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.List;
import java.util.TreeSet;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableSet;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
@ -37,13 +41,16 @@ import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.view.View;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.sasi.SASIIndex;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.ClientWarn;
import org.apache.cassandra.service.QueryState;
import org.apache.cassandra.transport.ProtocolVersion;
import org.apache.cassandra.transport.messages.ResultMessage;
import org.apache.cassandra.utils.Clock;
import org.assertj.core.api.Assertions;
import static java.lang.String.format;
@ -105,6 +112,12 @@ public abstract class GuardrailTester extends CQLTester
setter.accept(guardrails(), value);
}
protected <T> void assertValidProperty(BiConsumer<Guardrails, T> setter, Function<Guardrails, T> getter, T value)
{
setter.accept(guardrails(), value);
assertEquals(value, getter.apply(guardrails()));
}
protected <T> void assertInvalidProperty(BiConsumer<Guardrails, T> setter,
T value,
String message,
@ -222,6 +235,23 @@ public abstract class GuardrailTester extends CQLTester
assertFails(() -> execute(userClientState, query), messages);
}
protected void assertThrows(CheckedFunction function, Class<? extends Throwable> exception, String message)
{
try
{
function.apply();
fail("Expected to fail, but it did not");
}
catch (Throwable e)
{
if (!exception.isAssignableFrom(e.getClass()))
Assert.fail(format("Expected to fail with %s but got %s", exception.getName(), e.getClass().getName()));
assertTrue(format("Error message '%s' does not contain expected message '%s'", e.getMessage(), message),
e.getMessage().contains(message));
}
}
private void assertWarnings(String... messages)
{
List<String> warnings = getWarnings();
@ -282,6 +312,32 @@ public abstract class GuardrailTester extends CQLTester
}
protected ResultMessage execute(ClientState state, String query, List<ByteBuffer> values)
{
QueryOptions options = QueryOptions.forInternalCalls(values);
return execute(state, query, options);
}
protected ResultMessage execute(ClientState state, String query, ConsistencyLevel cl)
{
return execute(state, query, cl, null);
}
protected ResultMessage execute(ClientState state, String query, ConsistencyLevel cl, ConsistencyLevel serialCl)
{
QueryOptions options = QueryOptions.create(cl,
Collections.emptyList(),
false,
10,
null,
serialCl,
ProtocolVersion.CURRENT,
KEYSPACE);
return execute(state, query, options);
}
protected ResultMessage execute(ClientState state, String query, QueryOptions options)
{
QueryState queryState = new QueryState(state);
@ -289,8 +345,11 @@ public abstract class GuardrailTester extends CQLTester
CQLStatement statement = QueryProcessor.parseStatement(formattedQuery, queryState.getClientState());
statement.validate(state);
QueryOptions options = QueryOptions.forInternalCalls(values);
return statement.execute(queryState, options, Clock.Global.nanoTime());
}
return statement.executeLocally(queryState, options);
protected static String sortCSV(String csv)
{
return String.join(",", (new TreeSet<>(ImmutableSet.copyOf((csv.split(","))))));
}
}

View File

@ -0,0 +1,208 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cassandra.db.guardrails;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.db.ConsistencyLevel;
import static java.lang.String.format;
import static org.apache.cassandra.db.ConsistencyLevel.ALL;
import static org.apache.cassandra.db.ConsistencyLevel.ANY;
import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_ONE;
import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_QUORUM;
import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_SERIAL;
import static org.apache.cassandra.db.ConsistencyLevel.ONE;
import static org.apache.cassandra.db.ConsistencyLevel.QUORUM;
import static org.apache.cassandra.db.ConsistencyLevel.SERIAL;
/**
* Tests the guardrail for write consistency levels, {@link Guardrails#writeConsistencyLevels}.
*/
public class GuardrailWriteConsistencyLevelsTest extends GuardrailConsistencyLevelsTester
{
public GuardrailWriteConsistencyLevelsTest()
{
super("write_consistency_levels_warned",
"write_consistency_levels_disallowed",
Guardrails::getWriteConsistencyLevelsWarned,
Guardrails::getWriteConsistencyLevelsDisallowed,
Guardrails::getWriteConsistencyLevelsWarnedCSV,
Guardrails::getWriteConsistencyLevelsDisallowedCSV,
Guardrails::setWriteConsistencyLevelsWarned,
Guardrails::setWriteConsistencyLevelsDisallowed,
Guardrails::setWriteConsistencyLevelsWarnedCSV,
Guardrails::setWriteConsistencyLevelsDisallowedCSV);
}
@Before
public void before()
{
super.before();
createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))");
}
@Test
public void testInsert() throws Throwable
{
testQuery("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val')");
testLWTQuery("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') IF NOT EXISTS");
}
@Test
public void testUpdate() throws Throwable
{
testQuery("UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2");
testLWTQuery("UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 IF EXISTS");
}
@Test
public void testDelete() throws Throwable
{
testQuery("DELETE FROM %s WHERE k=1");
testLWTQuery("DELETE FROM %s WHERE k=1 AND c=2 IF EXISTS");
}
@Test
public void testBatch() throws Throwable
{
testQuery("BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') APPLY BATCH");
testQuery("BEGIN BATCH UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 APPLY BATCH");
testQuery("BEGIN BATCH DELETE FROM %s WHERE k=1 APPLY BATCH");
testLWTQuery("BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') IF NOT EXISTS APPLY BATCH");
testLWTQuery("BEGIN BATCH UPDATE %s SET v = 'val2' WHERE k = 1 AND c = 2 IF EXISTS APPLY BATCH");
testLWTQuery("BEGIN BATCH DELETE FROM %s WHERE k=1 AND c=2 IF EXISTS APPLY BATCH");
}
private void testQuery(String query) throws Throwable
{
testQuery(query, ONE);
testQuery(query, ALL);
testQuery(query, ANY);
testQuery(query, QUORUM);
testQuery(query, LOCAL_ONE);
testQuery(query, LOCAL_QUORUM);
}
private void testQuery(String query, ConsistencyLevel cl) throws Throwable
{
warnConsistencyLevels();
disableConsistencyLevels();
assertValid(query, cl, null);
warnConsistencyLevels(cl);
assertWarns(query, cl, null, cl);
warnConsistencyLevels();
disableConsistencyLevels(cl);
assertFails(query, cl, null, cl);
}
private void testLWTQuery(String query) throws Throwable
{
testLWTQuery(query, ONE);
testLWTQuery(query, ALL);
testLWTQuery(query, QUORUM);
testLWTQuery(query, LOCAL_ONE);
testLWTQuery(query, LOCAL_QUORUM);
}
private void testLWTQuery(String query, ConsistencyLevel cl) throws Throwable
{
disableConsistencyLevels();
warnConsistencyLevels();
assertValid(query, cl, SERIAL);
assertValid(query, cl, LOCAL_SERIAL);
assertValid(query, cl, null);
warnConsistencyLevels(cl);
assertWarns(query, cl, SERIAL, cl);
assertWarns(query, cl, LOCAL_SERIAL, cl);
assertWarns(query, cl, null, cl);
warnConsistencyLevels(SERIAL);
assertWarns(query, cl, SERIAL, SERIAL);
assertValid(query, cl, LOCAL_SERIAL);
assertWarns(query, cl, null, SERIAL);
warnConsistencyLevels(LOCAL_SERIAL);
assertValid(query, cl, SERIAL);
assertWarns(query, cl, LOCAL_SERIAL, LOCAL_SERIAL);
assertValid(query, cl, null);
warnConsistencyLevels(SERIAL, LOCAL_SERIAL);
assertWarns(query, cl, SERIAL, SERIAL);
assertWarns(query, cl, LOCAL_SERIAL, LOCAL_SERIAL);
assertWarns(query, cl, null, SERIAL);
warnConsistencyLevels();
disableConsistencyLevels(cl);
assertFails(query, cl, SERIAL, cl);
assertFails(query, cl, LOCAL_SERIAL, cl);
assertFails(query, cl, null, cl);
disableConsistencyLevels(SERIAL);
assertFails(query, cl, SERIAL, SERIAL);
assertValid(query, cl, LOCAL_SERIAL);
assertFails(query, cl, null, SERIAL);
disableConsistencyLevels(LOCAL_SERIAL);
assertValid(query, cl, SERIAL);
assertFails(query, cl, LOCAL_SERIAL, LOCAL_SERIAL);
assertValid(query, cl, null);
disableConsistencyLevels(SERIAL, LOCAL_SERIAL);
assertFails(query, cl, SERIAL, SERIAL);
assertFails(query, cl, LOCAL_SERIAL, LOCAL_SERIAL);
assertFails(query, cl, null, SERIAL);
}
private void assertValid(String query, ConsistencyLevel cl, ConsistencyLevel serialCl) throws Throwable
{
assertValid(() -> execute(userClientState, query, cl, serialCl));
}
private void assertWarns(String query, ConsistencyLevel cl, ConsistencyLevel serialCl, ConsistencyLevel warnedCl) throws Throwable
{
assertWarns(() -> execute(userClientState, query, cl, serialCl),
format("Provided values [%s] are not recommended for write consistency levels (warned values are: %s)",
warnedCl, guardrails().getWriteConsistencyLevelsWarned()));
assertExcludedUsers(query, cl, serialCl);
}
private void assertFails(String query, ConsistencyLevel cl, ConsistencyLevel serialCl, ConsistencyLevel rejectedCl) throws Throwable
{
assertFails(() -> execute(userClientState, query, cl, serialCl),
format("Provided values [%s] are not allowed for write consistency levels (disallowed values are: %s)",
rejectedCl, guardrails().getWriteConsistencyLevelsDisallowed()));
assertExcludedUsers(query, cl, serialCl);
}
private void assertExcludedUsers(String query, ConsistencyLevel cl, ConsistencyLevel serialCl) throws Throwable
{
assertValid(() -> execute(superClientState, query, cl, serialCl));
assertValid(() -> execute(systemClientState, query, cl, serialCl));
}
}

View File

@ -18,10 +18,12 @@
package org.apache.cassandra.db.guardrails;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
@ -155,67 +157,30 @@ public class GuardrailsTest extends GuardrailTester
}
@Test
public void testDisallowedValues() throws Throwable
public void testValuesWarned() throws Throwable
{
// Using a sorted set below to ensure the order in the error message checked below are not random
Values<Integer> disallowed = new Values<>(state -> Collections.emptySet(),
state -> insertionOrderedSet(4, 6, 20),
"integer");
// Using a sorted set below to ensure the order in the warning message checked below is not random
Values<Integer> warned = new Values<>(state -> insertionOrderedSet(4, 6, 20),
state -> Collections.emptySet(),
state -> Collections.emptySet(),
"integer");
Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
assertValid(() -> disallowed.guard(set(3), action, userClientState));
assertFails(() -> disallowed.guard(set(4), action, userClientState),
"Provided values [4] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertValid(() -> disallowed.guard(set(10), action, userClientState));
assertFails(() -> disallowed.guard(set(20), action, userClientState),
"Provided values [20] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertValid(() -> disallowed.guard(set(200), action, userClientState));
assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState));
assertFails(() -> disallowed.guard(set(4, 6), action, null),
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null),
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertValid(() -> warned.guard(set(3), action, userClientState));
assertWarns(() -> warned.guard(set(4), action, userClientState),
"Provided values [4] are not recommended for integer (warned values are: [4, 6, 20])");
assertWarns(() -> warned.guard(set(4, 6), action, null),
"Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])");
assertWarns(() -> warned.guard(set(4, 5, 6, 7), action, null),
"Provided values [4, 6] are not recommended for integer (warned values are: [4, 6, 20])");
}
@Test
public void testDisallowedValuesUsers() throws Throwable
{
Values<Integer> disallowed = new Values<>(state -> Collections.emptySet(),
state -> Collections.singleton(2),
"integer");
Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
assertValid(() -> disallowed.guard(set(1), action, null));
assertValid(() -> disallowed.guard(set(1), action, userClientState));
assertValid(() -> disallowed.guard(set(1), action, systemClientState));
assertValid(() -> disallowed.guard(set(1), action, superClientState));
String message = "Provided values [2] are not allowed for integer (disallowed values are: [2])";
assertFails(() -> disallowed.guard(set(2), action, null), message);
assertFails(() -> disallowed.guard(set(2), action, userClientState), message);
assertValid(() -> disallowed.guard(set(2), action, systemClientState));
assertValid(() -> disallowed.guard(set(2), action, superClientState));
Set<Integer> allowedValues = set(1);
assertValid(() -> disallowed.guard(allowedValues, action, null));
assertValid(() -> disallowed.guard(allowedValues, action, userClientState));
assertValid(() -> disallowed.guard(allowedValues, action, systemClientState));
assertValid(() -> disallowed.guard(allowedValues, action, superClientState));
Set<Integer> disallowedValues = set(2);
message = "Provided values [2] are not allowed for integer (disallowed values are: [2])";
assertFails(() -> disallowed.guard(disallowedValues, action, null), message);
assertFails(() -> disallowed.guard(disallowedValues, action, userClientState), message);
assertValid(() -> disallowed.guard(disallowedValues, action, systemClientState));
assertValid(() -> disallowed.guard(disallowedValues, action, superClientState));
}
@Test
public void testIgnoredValues() throws Throwable
public void testValuesIgnored() throws Throwable
{
// Using a sorted set below to ensure the order in the error message checked below are not random
Values<Integer> ignored = new Values<>(state -> insertionOrderedSet(4, 6, 20),
Values<Integer> ignored = new Values<>(state -> Collections.emptySet(),
state -> insertionOrderedSet(4, 6, 20),
state -> Collections.emptySet(),
"integer");
@ -237,6 +202,71 @@ public class GuardrailsTest extends GuardrailTester
"Ignoring provided values [4, 6] as they are not supported for integer (ignored values are: [4, 6, 20])");
assertEquals(set(4, 6), triggeredOn);
triggeredOn.clear();
assertThrows(() -> ignored.guard(set(4), userClientState),
AssertionError.class,
"There isn't an ignore action for integer, but value 4 is setup to be ignored");
}
@Test
public void testValuesDisallowed() throws Throwable
{
// Using a sorted set below to ensure the order in the error message checked below are not random
Values<Integer> disallowed = new Values<>(state -> Collections.emptySet(),
state -> Collections.emptySet(),
state -> insertionOrderedSet(4, 6, 20),
"integer");
Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
assertValid(() -> disallowed.guard(set(3), action, userClientState));
assertFails(() -> disallowed.guard(set(4), action, userClientState),
"Provided values [4] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertValid(() -> disallowed.guard(set(10), action, userClientState));
assertFails(() -> disallowed.guard(set(20), action, userClientState),
"Provided values [20] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertValid(() -> disallowed.guard(set(200), action, userClientState));
assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState));
assertFails(() -> disallowed.guard(set(4, 6), action, null),
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null),
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
}
@Test
public void testValuesUsers() throws Throwable
{
Values<Integer> disallowed = new Values<>(state -> Collections.singleton(2),
state -> Collections.singleton(3),
state -> Collections.singleton(4),
"integer");
Consumer<Integer> action = i -> Assert.fail("The ignore action shouldn't have been triggered");
assertValid(() -> disallowed.guard(set(1), action, null));
assertValid(() -> disallowed.guard(set(1), action, userClientState));
assertValid(() -> disallowed.guard(set(1), action, systemClientState));
assertValid(() -> disallowed.guard(set(1), action, superClientState));
String message = "Provided values [2] are not recommended for integer (warned values are: [2])";
assertWarns(() -> disallowed.guard(set(2), action, null), message);
assertWarns(() -> disallowed.guard(set(2), action, userClientState), message);
assertValid(() -> disallowed.guard(set(2), action, systemClientState));
assertValid(() -> disallowed.guard(set(2), action, superClientState));
message = "Ignoring provided values [3] as they are not supported for integer (ignored values are: [3])";
List<Integer> triggeredOn = new ArrayList<>();
assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, null), message);
assertWarns(() -> disallowed.guard(set(3), triggeredOn::add, userClientState), message);
assertValid(() -> disallowed.guard(set(3), triggeredOn::add, systemClientState));
assertValid(() -> disallowed.guard(set(3), triggeredOn::add, superClientState));
Assert.assertEquals(list(3, 3), triggeredOn);
message = "Provided values [4] are not allowed for integer (disallowed values are: [4])";
assertFails(() -> disallowed.guard(set(4), action, null), message);
assertFails(() -> disallowed.guard(set(4), action, userClientState), message);
assertValid(() -> disallowed.guard(set(4), action, systemClientState));
assertValid(() -> disallowed.guard(set(4), action, superClientState));
}
private static Set<Integer> set(Integer value)