You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ad...@apache.org on 2021/12/03 15:28:31 UTC
[cassandra] branch trunk updated: Add guardrails framework prototype
This is an automated email from the ASF dual-hosted git repository.
adelapena pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git
The following commit(s) were added to refs/heads/trunk by this push:
new 7871a28 Add guardrails framework prototype
7871a28 is described below
commit 7871a2854217acc89d2f885619243a442cf68e22
Author: Andrés de la Peña <a....@gmail.com>
AuthorDate: Wed Nov 3 10:10:02 2021 +0000
Add guardrails framework prototype
patch by Andrés de la Peña; reviewed by David Capwell and Stefan Miklosovic for CASSANDRA-17147
Co-authored-by: Sylvain Lebresne <le...@gmail.com>
Co-authored-by: Andrés de la Peña <a....@gmail.com>
Co-authored-by: Aleksandr Sorokoumov <al...@gmail.com>
Co-authored-by: Zhao Yang <ja...@gmail.com>
Co-authored-by: Jakub Żytka <ja...@datastax.com>
---
CHANGES.txt | 1 +
conf/cassandra.yaml | 31 +++
.../cassandra/auth/CassandraRoleManager.java | 2 +-
src/java/org/apache/cassandra/config/Config.java | 3 +
.../cassandra/config/DatabaseDescriptor.java | 19 ++
.../apache/cassandra/config/GuardrailsOptions.java | 262 ++++++++++++++++++
.../cql3/statements/ModificationStatement.java | 4 +
.../cql3/statements/PropertyDefinitions.java | 13 +
.../statements/schema/AlterSchemaStatement.java | 2 +-
.../statements/schema/AlterTableStatement.java | 22 ++
.../cql3/statements/schema/AlterViewStatement.java | 14 +
.../statements/schema/CreateIndexStatement.java | 20 ++
.../statements/schema/CreateTableStatement.java | 30 +++
.../statements/schema/CreateViewStatement.java | 22 ++
.../cql3/statements/schema/TableAttributes.java | 11 +
.../cassandra/db/guardrails/DisableFlag.java | 81 ++++++
.../apache/cassandra/db/guardrails/Guardrail.java | 82 ++++++
.../apache/cassandra/db/guardrails/Guardrails.java | 280 +++++++++++++++++++
.../cassandra/db/guardrails/GuardrailsConfig.java | 82 ++++++
.../db/guardrails/GuardrailsConfigProvider.java | 78 ++++++
.../cassandra/db/guardrails/GuardrailsMBean.java | 173 ++++++++++++
.../apache/cassandra/db/guardrails/Threshold.java | 153 +++++++++++
.../org/apache/cassandra/db/guardrails/Values.java | 103 +++++++
.../org/apache/cassandra/schema/TableMetadata.java | 5 +
.../org/apache/cassandra/service/ClientState.java | 31 ++-
.../config/DatabaseDescriptorRefTest.java | 8 +
.../guardrails/GuardrailColumnsPerTableTest.java | 190 +++++++++++++
.../GuardrailSecondaryIndexesPerTable.java | 122 +++++++++
.../guardrails/GuardrailTablePropertiesTest.java | 181 +++++++++++++
.../db/guardrails/GuardrailTablesLimitTest.java | 115 ++++++++
.../cassandra/db/guardrails/GuardrailTester.java | 284 +++++++++++++++++++
.../db/guardrails/GuardrailUserTimestampsTest.java | 124 +++++++++
.../db/guardrails/GuardrailViewsPerTableTest.java | 125 +++++++++
.../guardrails/GuardrailsConfigProviderTest.java | 79 ++++++
.../cassandra/db/guardrails/GuardrailsTest.java | 300 +++++++++++++++++++++
.../cassandra/db/guardrails/ThresholdTester.java | 188 +++++++++++++
36 files changed, 3236 insertions(+), 4 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index c8cc544..14a1623 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
4.1
+ * Add guardrails framework (CASSANDRA-17147)
* Harden resource management on SSTable components to prevent future leaks (CASSANDRA-17174)
* Make nodes more resilient to local unrelated files during startup (CASSANDRA-17082)
* repair prepare message would produce a wrong error message if network timeout happened rather than reply wait timeout (CASSANDRA-16992)
diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml
index ecacfed..a9a1f9c 100644
--- a/conf/cassandra.yaml
+++ b/conf/cassandra.yaml
@@ -1571,3 +1571,34 @@ enable_drop_compact_storage: false
# row_index_size:
# warn_threshold_kb: 0
# abort_threshold_kb: 0
+
+# Guardrails settings.
+# guardrails:
+# Whether guardrails are enabled or not. Guardrails are disabled by default.
+# enabled: false
+# Guardrail to warn or abort when creating more user tables than threshold.
+# The two thresholds default to -1 to disable.
+# tables:
+# warn_threshold: -1
+# abort_threshold: -1
+# Guardrail to warn or abort when creating/altering a table with more columns per table than threshold.
+# The two thresholds default to -1 to disable.
+# columns_per_table:
+# warn_threshold: -1
+# abort_threshold: -1
+# Guardrail to warn or abort when creating more secondary indexes per table than threshold.
+# The two thresholds default to -1 to disable.
+# secondary_indexes_per_table:
+# warn_threshold: -1
+# abort_threshold: -1
+# Guardrail to warn or abort when creating more materialized views per table than threshold.
+# The two thresholds default to -1 to disable.
+# materialized_views_per_table:
+# warn_threshold: -1
+# abort_threshold: -1
+# Guardrail to ignore or reject properties when creating tables. By default all properties are allowed.
+# table_properties:
+# ignored: []
+# disallowed: []
+# Guardrail to allow/disallow user-provided timestamps. Defaults to true.
+# user_timestamps_enabled: true
diff --git a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
index b813b55..802b998 100644
--- a/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
+++ b/src/java/org/apache/cassandra/auth/CassandraRoleManager.java
@@ -80,7 +80,7 @@ public class CassandraRoleManager implements IRoleManager
{
private static final Logger logger = LoggerFactory.getLogger(CassandraRoleManager.class);
- static final String DEFAULT_SUPERUSER_NAME = "cassandra";
+ public static final String DEFAULT_SUPERUSER_NAME = "cassandra";
static final String DEFAULT_SUPERUSER_PASSWORD = "cassandra";
/**
diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java
index 38fba4a..cdf83b8 100644
--- a/src/java/org/apache/cassandra/config/Config.java
+++ b/src/java/org/apache/cassandra/config/Config.java
@@ -563,6 +563,9 @@ public class Config
*/
public volatile double range_tombstone_list_growth_factor = 1.5;
+ /** The configuration for guardrails. */
+ public final GuardrailsOptions guardrails = new GuardrailsOptions();
+
/**
* @deprecated migrate to {@link DatabaseDescriptor#isClientInitialized()}
*/
diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
index 8265fd2..8ed8612 100644
--- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
+++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java
@@ -365,6 +365,8 @@ public class DatabaseDescriptor
applyEncryptionContext();
applySslContext();
+
+ applyGuardrails();
}
private static void applySimpleConfig()
@@ -872,6 +874,23 @@ public class DatabaseDescriptor
config.track_warnings.validate("track_warnings");
}
+ public static GuardrailsOptions getGuardrailsConfig()
+ {
+ return conf.guardrails;
+ }
+
+ private static void applyGuardrails()
+ {
+ try
+ {
+ conf.guardrails.validate();
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw new ConfigurationException("Invalid guardrails configuration: " + e.getMessage(), e);
+ }
+ }
+
private static String storagedirFor(String type)
{
return storagedir(type + "_directory") + File.pathSeparator() + type;
diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
new file mode 100644
index 0000000..a6892d4
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java
@@ -0,0 +1,262 @@
+/*
+ * 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.config;
+
+import java.util.Collections;
+import java.util.Set;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.Sets;
+
+import org.apache.cassandra.cql3.statements.schema.TableAttributes;
+import org.apache.cassandra.db.guardrails.Guardrails;
+import org.apache.cassandra.db.guardrails.GuardrailsConfig;
+import org.apache.cassandra.db.guardrails.Values;
+
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toSet;
+
+/**
+ * Configuration settings for guardrails populated from the Yaml file.
+ *
+ * <p>Note that the settings here must only be used to build the {@link GuardrailsConfig} class and not directly by the
+ * code checking each guarded constraint. That code should use the higher level abstractions defined in
+ * {@link Guardrails}).
+ *
+ * <p>This contains a main setting, {@code enabled}, controlling if guardrails are globally active or not, and
+ * individual settings to control each guardrail.
+ *
+ * <p>We have 2 variants of guardrails, soft (warn) and hard (abort) limits, each guardrail having either one of the
+ * variants or both. Note in particular that hard limits only make sense for guardrails triggering during query
+ * execution. For other guardrails, say one triggering during compaction, aborting that compaction does not make sense.
+ *
+ * <p>Additionally, each individual setting should have a specific value (typically -1 for numeric settings),
+ * that allows to disable the corresponding guardrail.
+ */
+public class GuardrailsOptions implements GuardrailsConfig
+{
+ public volatile boolean enabled = false;
+ public final IntThreshold tables = new IntThreshold();
+ public final IntThreshold columns_per_table = new IntThreshold();
+ public final IntThreshold secondary_indexes_per_table = new IntThreshold();
+ public final IntThreshold materialized_views_per_table = new IntThreshold();
+ public final TableProperties table_properties = new TableProperties();
+ public volatile boolean user_timestamps_enabled = true;
+
+ public void validate()
+ {
+ tables.validate("guardrails.tables");
+ columns_per_table.validate("guardrails.columns_per_table");
+ secondary_indexes_per_table.validate("guardrails.secondary_indexes_per_table");
+ materialized_views_per_table.validate("guardrails.materialized_views_per_table");
+ table_properties.validate("guardrails.table_properties");
+ }
+
+ @Override
+ public boolean getEnabled()
+ {
+ return enabled;
+ }
+
+ /**
+ * Enable/disable guardrails.
+ *
+ * @param enabled {@code true} for enabling guardrails, {@code false} for disabling them.
+ */
+ public void setEnabled(boolean enabled)
+ {
+ this.enabled = enabled;
+ }
+
+ @Override
+ public IntThreshold getTables()
+ {
+ return tables;
+ }
+
+ @Override
+ public IntThreshold getColumnsPerTable()
+ {
+ return columns_per_table;
+ }
+
+ @Override
+ public IntThreshold getSecondaryIndexesPerTable()
+ {
+ return secondary_indexes_per_table;
+ }
+
+ @Override
+ public IntThreshold getMaterializedViewsPerTable()
+ {
+ return materialized_views_per_table;
+ }
+
+ @Override
+ public TableProperties getTableProperties()
+ {
+ return table_properties;
+ }
+
+ @Override
+ public boolean getUserTimestampsEnabled()
+ {
+ return user_timestamps_enabled;
+ }
+
+ public void setUserTimestampsEnabled(boolean enabled)
+ {
+ user_timestamps_enabled = enabled;
+ }
+
+ public static abstract class Threshold implements org.apache.cassandra.db.guardrails.Threshold.Config
+ {
+ public static final long DISABLED = -1;
+
+ public volatile long warn_threshold = DISABLED;
+ public volatile long abort_threshold = DISABLED;
+
+ @Override
+ public long getWarnThreshold()
+ {
+ return warn_threshold;
+ }
+
+ @Override
+ public long getAbortThreshold()
+ {
+ return abort_threshold;
+ }
+
+ public void setThresholds(long warn, long abort)
+ {
+ validateStrictlyPositive(warn, "warn threshold");
+ validateStrictlyPositive(abort, "abort threshold");
+ validateWarnLowerThanAbort(warn, abort, null);
+ warn_threshold = warn;
+ abort_threshold = abort;
+ }
+
+ public void validate(String name)
+ {
+ validateStrictlyPositive(warn_threshold, name + ".warn_threshold");
+ validateStrictlyPositive(abort_threshold, name + ".abort_threshold");
+ validateWarnLowerThanAbort(warn_threshold, abort_threshold, name);
+ }
+
+ public abstract long maxValue();
+ public abstract boolean allowZero();
+
+ private void validateStrictlyPositive(long value, String name)
+ {
+ if (value > maxValue())
+ throw new IllegalArgumentException(format("Invalid value %d for %s: maximum allowed value is %d",
+ value, name, maxValue()));
+
+ if (value == 0 && !allowZero())
+ throw new IllegalArgumentException(format("Invalid value for %s: 0 is not allowed; " +
+ "if attempting to disable use %d",
+ name, DISABLED));
+
+ // We allow -1 as a general "disabling" flag. But reject anything lower to avoid mistakes.
+ if (value < DISABLED)
+ throw new IllegalArgumentException(format("Invalid value %d for %s: negative values are not allowed, " +
+ "outside of %d which disables the guardrail",
+ value, name, DISABLED));
+ }
+
+ private void validateWarnLowerThanAbort(long warnValue, long abortValue, @Nullable String name)
+ {
+ if (warnValue == DISABLED || abortValue == DISABLED)
+ return;
+
+ if (abortValue < warnValue)
+ throw new IllegalArgumentException(format("The warn threshold %d%s should be lower than the abort " +
+ "threshold %d",
+ warnValue,
+ name == null ? "" : " for " + name,
+ abortValue));
+ }
+ }
+
+ public static class IntThreshold extends Threshold
+ {
+ @Override
+ public long maxValue()
+ {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public boolean allowZero()
+ {
+ return false;
+ }
+ }
+
+ public static class TableProperties implements Values.Config<String>
+ {
+ public volatile Set<String> ignored = Collections.emptySet();
+ public volatile Set<String> disallowed = Collections.emptySet();
+
+ @Override
+ public Set<String> getIgnored()
+ {
+ return ignored;
+ }
+
+ @Override
+ public Set<String> getDisallowed()
+ {
+ return disallowed;
+ }
+
+ public void setIgnoredValues(Set<String> values)
+ {
+ this.ignored = validateTableProperties(values, "ignored properties");
+ }
+
+ public void setDisallowedValues(Set<String> values)
+ {
+ this.disallowed = validateTableProperties(values, "disallowed properties");
+ }
+
+ private Set<String> validateTableProperties(Set<String> properties, String name)
+ {
+ if (properties == null)
+ throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
+
+ Set<String> lowerCaseProperties = properties.stream().map(String::toLowerCase).collect(toSet());
+
+ Set<String> diff = Sets.difference(lowerCaseProperties, TableAttributes.allKeywords());
+
+ if (!diff.isEmpty())
+ throw new IllegalArgumentException(format("Invalid value for %s: '%s' do not parse as valid table properties",
+ name, diff.toString()));
+
+ return lowerCaseProperties;
+ }
+
+ public void validate(String name)
+ {
+ validateTableProperties(ignored, name + ".ignored");
+ validateTableProperties(disallowed, name + ".disallowed");
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
index 21ddf52..b6abdca 100644
--- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java
@@ -26,6 +26,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.cassandra.auth.Permission;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.marshal.ValueAccessor;
import org.apache.cassandra.schema.ColumnMetadata;
import org.apache.cassandra.schema.Schema;
@@ -269,6 +270,9 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
checkFalse(isVirtual() && attrs.isTimestampSet(), "Custom timestamp is not supported by virtual tables");
checkFalse(isVirtual() && attrs.isTimeToLiveSet(), "Expiring columns are not supported by virtual tables");
checkFalse(isVirtual() && hasConditions(), "Conditional updates are not supported by virtual tables");
+
+ if (attrs.isTimestampSet())
+ Guardrails.userTimestampsEnabled.ensureEnabled(state);
}
public RegularAndStaticColumns updatedColumns()
diff --git a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
index 590910f..c777172 100644
--- a/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
+++ b/src/java/org/apache/cassandra/cql3/statements/PropertyDefinitions.java
@@ -59,6 +59,19 @@ public class PropertyDefinitions
}
}
+ /**
+ * Returns the name of all the properties that are updated by this object.
+ */
+ public Set<String> updatedProperties()
+ {
+ return properties.keySet();
+ }
+
+ public void removeProperty(String name)
+ {
+ properties.remove(name);
+ }
+
protected String getSimple(String name) throws SyntaxException
{
Object val = properties.get(name);
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
index 124d04c..3b40c68 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterSchemaStatement.java
@@ -44,7 +44,7 @@ abstract public class AlterSchemaStatement implements CQLStatement.SingleKeyspac
this.keyspaceName = keyspaceName;
}
- public final void validate(ClientState state)
+ public void validate(ClientState state)
{
// no-op; validation is performed while executing the statement, in apply()
}
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
index c8cf045..386dbae 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterTableStatement.java
@@ -42,6 +42,7 @@ import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.QualifiedName;
import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.exceptions.InvalidRequestException;
@@ -163,6 +164,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
}
private final Collection<Column> newColumns;
+ private ClientState state;
private AddColumns(String keyspaceName, String tableName, Collection<Column> newColumns)
{
@@ -170,11 +172,23 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
this.newColumns = newColumns;
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ // save the query state to use it for guardrails validation in #apply
+ this.state = state;
+ }
+
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
{
TableMetadata.Builder tableBuilder = table.unbuild();
Views.Builder viewsBuilder = keyspace.views.unbuild();
newColumns.forEach(c -> addColumn(keyspace, table, c, tableBuilder, viewsBuilder));
+
+ Guardrails.columnsPerTable.guard(tableBuilder.numColumns(), tableName, state);
+
TableMetadata tableMetadata = tableBuilder.build();
tableMetadata.validate();
@@ -391,6 +405,14 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
this.attrs = attrs;
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+ }
+
public KeyspaceMetadata apply(KeyspaceMetadata keyspace, TableMetadata table)
{
attrs.validate();
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
index 3493eb0..d6a0a35 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/AlterViewStatement.java
@@ -22,6 +22,7 @@ import org.apache.cassandra.audit.AuditLogEntryType;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.QualifiedName;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.schema.*;
import org.apache.cassandra.schema.Keyspaces.KeyspacesDiff;
import org.apache.cassandra.service.ClientState;
@@ -33,6 +34,7 @@ public final class AlterViewStatement extends AlterSchemaStatement
{
private final String viewName;
private final TableAttributes attrs;
+ private ClientState state;
public AlterViewStatement(String keyspaceName, String viewName, TableAttributes attrs)
{
@@ -41,6 +43,15 @@ public final class AlterViewStatement extends AlterSchemaStatement
this.attrs = attrs;
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ // save the query state to use it for guardrails validation in #apply
+ this.state = state;
+ }
+
public Keyspaces apply(Keyspaces schema)
{
KeyspaceMetadata keyspace = schema.getNullable(keyspaceName);
@@ -54,6 +65,9 @@ public final class AlterViewStatement extends AlterSchemaStatement
attrs.validate();
+ // Guardrails on table properties
+ Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
TableParams params = attrs.asAlteredTableParams(view.metadata.params);
if (params.gcGraceSeconds == 0)
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
index d501423..ae60f24 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateIndexStatement.java
@@ -19,6 +19,7 @@ package org.apache.cassandra.cql3.statements.schema;
import java.util.*;
+import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
@@ -31,6 +32,7 @@ import org.apache.cassandra.cql3.ColumnIdentifier;
import org.apache.cassandra.cql3.QualifiedName;
import org.apache.cassandra.cql3.statements.schema.IndexTarget.Type;
import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.exceptions.InvalidRequestException;
import org.apache.cassandra.index.sasi.SASIIndex;
@@ -52,6 +54,8 @@ public final class CreateIndexStatement extends AlterSchemaStatement
private final IndexAttributes attrs;
private final boolean ifNotExists;
+ private ClientState state;
+
public CreateIndexStatement(String keyspaceName,
String tableName,
String indexName,
@@ -67,6 +71,15 @@ public final class CreateIndexStatement extends AlterSchemaStatement
this.ifNotExists = ifNotExists;
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ // save the query state to use it for guardrails validation in #apply
+ this.state = state;
+ }
+
public Keyspaces apply(Keyspaces schema)
{
attrs.validate();
@@ -99,6 +112,13 @@ public final class CreateIndexStatement extends AlterSchemaStatement
if (Keyspace.open(table.keyspace).getReplicationStrategy().hasTransientReplicas())
throw new InvalidRequestException("Secondary indexes are not supported on transiently replicated keyspaces");
+ // guardrails to limit number of secondary indexes per table.
+ Guardrails.secondaryIndexesPerTable.guard(table.indexes.size() + 1,
+ Strings.isNullOrEmpty(indexName)
+ ? String.format("on table %s", table.name)
+ : String.format("%s on table %s", indexName, table.name),
+ state);
+
List<IndexTarget> indexTargets = Lists.newArrayList(transform(rawIndexTargets, t -> t.prepare(table)));
if (indexTargets.isEmpty() && !attrs.isCustom)
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
index b2a8e96..c765696 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateTableStatement.java
@@ -33,6 +33,8 @@ import org.apache.cassandra.auth.IResource;
import org.apache.cassandra.auth.Permission;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.*;
+import org.apache.cassandra.db.Keyspace;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.marshal.*;
import org.apache.cassandra.exceptions.AlreadyExistsException;
import org.apache.cassandra.schema.*;
@@ -118,6 +120,34 @@ public final class CreateTableStatement extends AlterSchemaStatement
return schema.withAddedOrUpdated(keyspace.withSwapped(keyspace.tables.with(table)));
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ // since there are multiple guardrails guarding this, we first check that guardrails are globally enabled as an
+ // optimization for the case where they are disabled, so we don't have to do the same check on every guardrail
+ if (Guardrails.enabled(state))
+ {
+ // Guardrails on table properties
+ Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
+ // Guardrail on columns per table
+ Guardrails.columnsPerTable.guard(rawColumns.size(), tableName, state);
+
+ // Guardrails on number of tables
+ if (Guardrails.tablesLimit.enabled(state))
+ {
+ int totalUserTables = Schema.instance.getUserKeyspaces()
+ .stream()
+ .map(Keyspace::open)
+ .mapToInt(keyspace -> keyspace.getColumnFamilyStores().size())
+ .sum();
+ Guardrails.tablesLimit.guard(totalUserTables + 1, tableName, state);
+ }
+ }
+ }
+
SchemaChange schemaChangeEvent(KeyspacesDiff diff)
{
return new SchemaChange(Change.CREATED, Target.TABLE, keyspaceName, tableName);
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
index 9d6bac3..26ffeee 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/CreateViewStatement.java
@@ -20,6 +20,7 @@ package org.apache.cassandra.cql3.statements.schema;
import java.util.*;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.apache.cassandra.audit.AuditLogContext;
@@ -31,6 +32,7 @@ import org.apache.cassandra.cql3.restrictions.StatementRestrictions;
import org.apache.cassandra.cql3.selection.RawSelector;
import org.apache.cassandra.cql3.selection.Selectable;
import org.apache.cassandra.cql3.statements.StatementType;
+import org.apache.cassandra.db.guardrails.Guardrails;
import org.apache.cassandra.db.marshal.AbstractType;
import org.apache.cassandra.db.marshal.ReversedType;
import org.apache.cassandra.db.view.View;
@@ -65,6 +67,8 @@ public final class CreateViewStatement extends AlterSchemaStatement
private final boolean ifNotExists;
+ private ClientState state;
+
public CreateViewStatement(String keyspaceName,
String tableName,
String viewName,
@@ -96,6 +100,15 @@ public final class CreateViewStatement extends AlterSchemaStatement
this.ifNotExists = ifNotExists;
}
+ @Override
+ public void validate(ClientState state)
+ {
+ super.validate(state);
+
+ // save the query state to use it for guardrails validation in #apply
+ this.state = state;
+ }
+
public Keyspaces apply(Keyspaces schema)
{
if (!DatabaseDescriptor.getEnableMaterializedViews())
@@ -137,6 +150,15 @@ public final class CreateViewStatement extends AlterSchemaStatement
if (table.isView())
throw ire("Materialized views cannot be created against other materialized views");
+ // Guardrails on table properties
+ Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
+
+ // Guardrail to limit number of mvs per table
+ Iterable<ViewMetadata> tableViews = keyspace.views.forTable(table.id);
+ Guardrails.materializedViewsPerTable.guard(Iterables.size(tableViews) + 1,
+ String.format("%s on table %s", viewName, table.name),
+ state);
+
if (table.params.gcGraceSeconds == 0)
{
throw ire("Cannot create materialized view '%s' for base table " +
diff --git a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
index 126e6d7..caeb720 100644
--- a/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
+++ b/src/java/org/apache/cassandra/cql3/statements/schema/TableAttributes.java
@@ -21,6 +21,7 @@ import java.util.Map;
import java.util.Set;
import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
import org.apache.cassandra.cql3.statements.PropertyDefinitions;
import org.apache.cassandra.exceptions.ConfigurationException;
@@ -83,6 +84,16 @@ public final class TableAttributes extends PropertyDefinitions
}
}
+ public static Set<String> validKeywords()
+ {
+ return ImmutableSet.copyOf(validKeywords);
+ }
+
+ public static Set<String> allKeywords()
+ {
+ return Sets.union(validKeywords, obsoleteKeywords);
+ }
+
private TableParams build(TableParams.Builder builder)
{
if (hasOption(Option.BLOOM_FILTER_FP_CHANCE))
diff --git a/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java b/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java
new file mode 100644
index 0000000..1d34514
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/DisableFlag.java
@@ -0,0 +1,81 @@
+/*
+ * 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.function.Predicate;
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.service.ClientState;
+
+/**
+ * A guardrail that completely disables the use of a particular feature.
+ *
+ * <p>Note that this guardrail only aborts operations (if the feature is disabled) so is only meant for
+ * query-based guardrails (we're happy to reject queries deemed dangerous, but we don't want to create a guardrail
+ * that breaks compaction for instance).
+ */
+public class DisableFlag extends Guardrail
+{
+ private final Predicate<ClientState> disabled;
+ private final String what;
+
+ /**
+ * Creates a new {@link DisableFlag} guardrail.
+ *
+ * @param disabled a {@link ClientState}-based supplier of boolean indicating whether the feature guarded by this
+ * guardrail must be disabled.
+ * @param what The feature that is guarded by this guardrail (for reporting in error messages),
+ * {@link DisableFlag#ensureEnabled(String, ClientState)} can specify a different {@code what}.
+ */
+ public DisableFlag(Predicate<ClientState> disabled, String what)
+ {
+ this.disabled = disabled;
+ this.what = what;
+ }
+
+ /**
+ * Aborts the operation if this guardrail is disabled.
+ *
+ * <p>This must be called when the feature guarded by this guardrail is used to ensure such use is in fact
+ * allowed.
+ *
+ * @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 ensureEnabled(@Nullable ClientState state)
+ {
+ ensureEnabled(what, state);
+ }
+
+ /**
+ * Aborts the operation if this guardrail is disabled.
+ *
+ * <p>This must be called when the feature guarded by this guardrail is used to ensure such use is in fact
+ * allowed.
+ *
+ * @param what The feature that is guarded by this guardrail (for reporting in error messages).
+ * @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 ensureEnabled(String what, @Nullable ClientState state)
+ {
+ if (enabled(state) && disabled.test(state))
+ abort(what + " is not allowed");
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrail.java b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java
new file mode 100644
index 0000000..749fddf
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrail.java
@@ -0,0 +1,82 @@
+/*
+ * 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.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+
+import org.slf4j.LoggerFactory;
+
+import org.apache.cassandra.exceptions.InvalidRequestException;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.service.ClientWarn;
+import org.apache.cassandra.tracing.Tracing;
+import org.apache.cassandra.utils.NoSpamLogger;
+
+/**
+ * General class defining a given guardrail that guards against some particular usage/condition.
+ * <p>
+ * Some guardrails only emit warnings when triggered, while others abort the query that triggers them. Some may do one
+ * or the other based on specific threshold. The queries are aborted with an {@link InvalidRequestException}.
+ * <p>
+ * Note that all the defined classes support live updates, which is why each guardrail class constructor takes
+ * suppliers of the condition the guardrail acts on rather than the condition itself. This implies that said suppliers
+ * should be fast and non-blocking to avoid surprises.
+ */
+public abstract class Guardrail
+{
+ protected static final NoSpamLogger logger = NoSpamLogger.getLogger(LoggerFactory.getLogger(Guardrail.class),
+ 10, TimeUnit.MINUTES);
+
+ /**
+ * Checks whether this guardrail is enabled or not. This will be enabled if guardrails are enabled
+ * ({@link Guardrails#enabled(ClientState)}) and if the authenticated user (if specified) is not system nor
+ * superuser.
+ *
+ * @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.
+ * @return {@code true} if this guardrail is enabled, {@code false} otherwise.
+ */
+ public boolean enabled(@Nullable ClientState state)
+ {
+ return Guardrails.enabled(state) && (state == null || state.isOrdinaryUser());
+ }
+
+ protected void warn(String message)
+ {
+ logger.warn(message);
+ // Note that ClientWarn will simply ignore the message if we're not running this as part of a user query
+ // (the internal "state" will be null)
+ ClientWarn.instance.warn(message);
+ // Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
+ Tracing.trace(message);
+ }
+
+ protected void abort(String message)
+ {
+ logger.error(message);
+ // Note that ClientWarn will simply ignore the message if we're not running this as part of a user query
+ // (the internal "state" will be null)
+ ClientWarn.instance.warn(message);
+ // Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
+ Tracing.trace(message);
+
+ throw new InvalidRequestException(message);
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
new file mode 100644
index 0000000..62b01ab
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java
@@ -0,0 +1,280 @@
+/*
+ * 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.Set;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableSet;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.config.GuardrailsOptions;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.MBeanWrapper;
+
+import static java.lang.String.format;
+
+/**
+ * Entry point for Guardrails, storing the defined guardrails and providing a few global methods over them.
+ */
+public final class Guardrails implements GuardrailsMBean
+{
+ public static final String MBEAN_NAME = "org.apache.cassandra.db:type=Guardrails";
+
+ private static final GuardrailsConfigProvider CONFIG_PROVIDER = GuardrailsConfigProvider.instance;
+ private static final GuardrailsOptions DEFAULT_CONFIG = DatabaseDescriptor.getGuardrailsConfig();
+
+ @VisibleForTesting
+ static final Guardrails instance = new Guardrails();
+
+ /**
+ * Guardrail on the total number of tables on user keyspaces.
+ */
+ public static final Threshold tablesLimit =
+ new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getTables(),
+ (isWarning, what, value, threshold) ->
+ isWarning ? format("Creating table %s, current number of tables %s exceeds warning threshold of %s.",
+ what, value, threshold)
+ : format("Cannot have more than %s tables, aborting the creation of table %s",
+ threshold, what));
+
+ /**
+ * Guardrail on the number of columns per table.
+ */
+ public static final Threshold columnsPerTable =
+ new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getColumnsPerTable(),
+ (isWarning, what, value, threshold) ->
+ isWarning ? format("The table %s has %s columns, this exceeds the warning threshold of %s.",
+ what, value, threshold)
+ : format("Tables cannot have more than %s columns, but %s provided for table %s",
+ threshold, value, what));
+
+ public static final Threshold secondaryIndexesPerTable =
+ new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getSecondaryIndexesPerTable(),
+ (isWarning, what, value, threshold) ->
+ isWarning ? format("Creating secondary index %s, current number of indexes %s exceeds warning threshold of %s.",
+ what, value, threshold)
+ : format("Tables cannot have more than %s secondary indexes, aborting the creation of secondary index %s",
+ threshold, what));
+
+ /**
+ * Guardrail on the number of materialized views per table.
+ */
+ public static final Threshold materializedViewsPerTable =
+ new Threshold(state -> CONFIG_PROVIDER.getOrCreate(state).getMaterializedViewsPerTable(),
+ (isWarning, what, value, threshold) ->
+ isWarning ? format("Creating materialized view %s, current number of views %s exceeds warning threshold of %s.",
+ what, value, threshold)
+ : format("Tables cannot have more than %s materialized views, aborting the creation of materialized view %s",
+ threshold, what));
+
+ /**
+ * Guardrail ignoring/disallowing the usage of certain table properties.
+ */
+ public static final Values<String> tableProperties =
+ new Values<>(state -> CONFIG_PROVIDER.getOrCreate(state).getTableProperties(),
+ "Table Properties");
+
+ /**
+ * Guardrail disabling user-provided timestamps.
+ */
+ public static final DisableFlag userTimestampsEnabled =
+ new DisableFlag(state -> !CONFIG_PROVIDER.getOrCreate(state).getUserTimestampsEnabled(),
+ "User provided timestamps (USING TIMESTAMP)");
+
+ private Guardrails()
+ {
+ MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
+ }
+
+ /**
+ * Whether guardrails are enabled.
+ *
+ * @return {@code true} if guardrails are enabled and daemon is initialized,
+ * {@code false} otherwise (in which case no guardrail will trigger).
+ */
+ public static boolean enabled(ClientState state)
+ {
+ return CONFIG_PROVIDER.getOrCreate(state).getEnabled() && DatabaseDescriptor.isDaemonInitialized();
+ }
+
+ @Override
+ public boolean getEnabled()
+ {
+ return DEFAULT_CONFIG.getEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled)
+ {
+ DEFAULT_CONFIG.setEnabled(enabled);
+ }
+
+ @Override
+ public int getTablesWarnThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getTables().getWarnThreshold();
+ }
+
+ @Override
+ public int getTablesAbortThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getTables().getAbortThreshold();
+ }
+
+ @Override
+ public void setTablesThreshold(int warn, int abort)
+ {
+ DEFAULT_CONFIG.getTables().setThresholds(warn, abort);
+ }
+
+ @Override
+ public int getColumnsPerTableWarnThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getColumnsPerTable().getWarnThreshold();
+ }
+
+ @Override
+ public int getColumnsPerTableAbortThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getColumnsPerTable().getAbortThreshold();
+ }
+
+ @Override
+ public void setColumnsPerTableThreshold(int warn, int abort)
+ {
+ DEFAULT_CONFIG.getColumnsPerTable().setThresholds(warn, abort);
+ }
+
+ @Override
+ public int getSecondaryIndexesPerTableWarnThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getSecondaryIndexesPerTable().getWarnThreshold();
+ }
+
+ @Override
+ public int getSecondaryIndexesPerTableAbortThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getSecondaryIndexesPerTable().getAbortThreshold();
+ }
+
+ @Override
+ public void setSecondaryIndexesPerTableThreshold(int warn, int abort)
+ {
+ DEFAULT_CONFIG.getSecondaryIndexesPerTable().setThresholds(warn, abort);
+ }
+
+ @Override
+ public int getMaterializedViewsPerTableWarnThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getMaterializedViewsPerTable().getWarnThreshold();
+ }
+
+ @Override
+ public int getMaterializedViewsPerTableAbortThreshold()
+ {
+ return (int) DEFAULT_CONFIG.getMaterializedViewsPerTable().getAbortThreshold();
+ }
+
+ @Override
+ public void setMaterializedViewsPerTableThreshold(int warn, int abort)
+ {
+ DEFAULT_CONFIG.getMaterializedViewsPerTable().setThresholds(warn, abort);
+ }
+
+ @Override
+ public Set<String> getTablePropertiesDisallowed()
+ {
+ return DEFAULT_CONFIG.getTableProperties().getDisallowed();
+ }
+
+ @Override
+ public String getTablePropertiesDisallowedCSV()
+ {
+ return toCSV(DEFAULT_CONFIG.getTableProperties().getDisallowed());
+ }
+
+ public void setTablePropertiesDisallowed(String... properties)
+ {
+ setTablePropertiesDisallowed(ImmutableSet.copyOf(properties));
+ }
+
+ @Override
+ public void setTablePropertiesDisallowed(Set<String> properties)
+ {
+ DEFAULT_CONFIG.getTableProperties().setDisallowedValues(properties);
+ }
+
+ @Override
+ public void setTablePropertiesDisallowedCSV(String properties)
+ {
+ setTablePropertiesDisallowed(fromCSV(properties));
+ }
+
+ @Override
+ public Set<String> getTablePropertiesIgnored()
+ {
+ return DEFAULT_CONFIG.getTableProperties().getIgnored();
+ }
+
+ @Override
+ public String getTablePropertiesIgnoredCSV()
+ {
+ return toCSV(DEFAULT_CONFIG.getTableProperties().getIgnored());
+ }
+
+ public void setTablePropertiesIgnored(String... properties)
+ {
+ setTablePropertiesIgnored(ImmutableSet.copyOf(properties));
+ }
+
+ @Override
+ public void setTablePropertiesIgnored(Set<String> properties)
+ {
+ DEFAULT_CONFIG.getTableProperties().setIgnoredValues(properties);
+ }
+
+ @Override
+ public void setTablePropertiesIgnoredCSV(String properties)
+ {
+ setTablePropertiesIgnored(fromCSV(properties));
+ }
+
+ @Override
+ public boolean getUserTimestampsEnabled()
+ {
+ return DEFAULT_CONFIG.getUserTimestampsEnabled();
+ }
+
+ @Override
+ public void setUserTimestampsEnabled(boolean enabled)
+ {
+ DEFAULT_CONFIG.setUserTimestampsEnabled(enabled);
+ }
+
+ private static String toCSV(Set<String> values)
+ {
+ return values == null ? "" : String.join(",", values);
+ }
+
+ private static Set<String> fromCSV(String csv)
+ {
+ return csv == null ? null : ImmutableSet.copyOf(csv.split(","));
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
new file mode 100644
index 0000000..8f64638
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java
@@ -0,0 +1,82 @@
+/*
+ * 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;
+
+/**
+ * Configuration settings for guardrails.
+ *
+ * <p>Note that the settings here must only be used by the {@link Guardrails} class and not directly by the code
+ * checking each guarded constraint (which, again, should use the higher level abstractions defined in
+ * {@link Guardrails}).
+ *
+ * <p>This contains a main setting, {@code enabled}, controlling if guardrails are globally active or not, and
+ * individual settings to control each guardrail.
+ *
+ * <p>We have 2 variants of guardrails, soft (warn) and hard (abort) limits, each guardrail having either one of the
+ * variants or both. Note in particular that hard limits only make sense for guardrails triggering during query
+ * execution. For other guardrails, say one triggering during compaction, aborting that compaction does not make sense.
+ *
+ * <p>Additionally, each individual setting should have a specific value (typically -1 for numeric settings),
+ * that allows to disable the corresponding guardrail.
+ * <p>
+ * This configuration is offered as an interface so different implementations of {@link GuardrailsConfigProvider} can
+ * provide different implementations of this config. However, this mechanism for guardrails config pluggability is not
+ * officially supported and this interface may change in a minor release.
+ */
+public interface GuardrailsConfig
+{
+ /**
+ * Whether guardrails are enabled or not.
+ *
+ * @return {@code true} if guardrails are enabled, {@code false} otherwise
+ */
+ boolean getEnabled();
+
+ /**
+ * @return The threshold to warn or abort when creating more tables than threshold.
+ */
+ Threshold.Config getTables();
+
+ /**
+ * @return The threshold to warn or abort when creating more columns per table than threshold.
+ */
+ Threshold.Config getColumnsPerTable();
+
+ /**
+ * @return The threshold to warn or abort when creating more secondary indexes per table than threshold.
+ */
+ Threshold.Config getSecondaryIndexesPerTable();
+
+ /**
+ * @return The threshold to warn or abort when creating more materialized views per table than threshold.
+ */
+ Threshold.Config getMaterializedViewsPerTable();
+
+ /**
+ * @return The table properties that are ignored/disallowed when creating or altering a table.
+ */
+ Values.Config<String> getTableProperties();
+
+ /**
+ * Returns whether user-provided timestamps are allowed.
+ *
+ * @return {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+ */
+ boolean getUserTimestampsEnabled();
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java
new file mode 100644
index 0000000..6128764
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfigProvider.java
@@ -0,0 +1,78 @@
+/*
+ * 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 javax.annotation.Nullable;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+import org.apache.cassandra.service.ClientState;
+import org.apache.cassandra.utils.FBUtilities;
+
+/**
+ * Provider of {@link GuardrailsConfig}s for a {@link ClientState}.
+ * <p>
+ * The {@link Default} implementation always retuns the {@link GuardrailsConfig} parsed from {@code cassandra.yaml},
+ * but different implementations can return different configurations based on the specified {@link ClientState}.
+ * <p>
+ * Custom implementations can be specified at runtime with the system property {@link #CUSTOM_IMPLEMENTATION_PROPERTY}.
+ * These configurations can be used to read the guardrails configuration from some other source, and provide different
+ * configurations depending on the {@link ClientState} or some other factors. However, this mechanism for pluggability
+ * and the related {@link GuardrailsConfig} interface are not officially supported and may change in a minor release.
+ */
+public interface GuardrailsConfigProvider
+{
+ public static final String CUSTOM_IMPLEMENTATION_PROPERTY = "cassandra.custom_guardrails_config_provider_class";
+
+ static final GuardrailsConfigProvider instance = System.getProperty(CUSTOM_IMPLEMENTATION_PROPERTY) == null
+ ? new Default()
+ : build(System.getProperty(CUSTOM_IMPLEMENTATION_PROPERTY));
+
+ /**
+ * Returns the {@link GuardrailsConfig} to be used for the specified {@link ClientState}.
+ *
+ * @param state a client state, maybe {@code null} if the guardrails check for which we are getting the config is
+ * for a background process that is not associated to a user query.
+ * @return the configuration to be used for {@code state}
+ */
+ GuardrailsConfig getOrCreate(@Nullable ClientState state);
+
+ /**
+ * Creates an instance of the custom guardrails config provider of the given class.
+ *
+ * @param customImpl the fully qualified classname of the guardrails config provider to be instantiated
+ * @return a new instance of the specified custom guardrails config provider of the given class.
+ */
+ static GuardrailsConfigProvider build(String customImpl)
+ {
+ return FBUtilities.construct(customImpl, "custom guardrails config provider");
+ }
+
+ /**
+ * Default implementation of {@link GuardrailsConfigProvider} that always returns the {@link GuardrailsConfig}
+ * parsed from {@code cassandra.yaml}, independently of the {@link ClientState}.
+ */
+ class Default implements GuardrailsConfigProvider
+ {
+ @Override
+ public GuardrailsConfig getOrCreate(@Nullable ClientState state)
+ {
+ return DatabaseDescriptor.getGuardrailsConfig();
+ }
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
new file mode 100644
index 0000000..a5f6c00
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java
@@ -0,0 +1,173 @@
+/*
+ * 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.Set;
+
+/**
+ * JMX entrypoint for updating the default guardrails configuration parsed from {@code cassandra.yaml}.
+ * <p>
+ * This is different to just exposing {@link GuardrailsConfig} in that the methods here should be JMX-friendly.
+ *
+ * <p>For consistency, guardrails based on a simple numeric threshold should use the naming scheme
+ * {@code <whatIsGuarded>WarnThreshold} for soft limits and {@code <whatIsGuarded>AbortThreshold} for hard
+ * ones, and if the value has a unit, that unit should be added at the end (for instance,
+ * {@code <whatIsGuarded>AbortThresholdInKb}). For "boolean" guardrails that disable a feature, use
+ * {@code <whatIsGuardedEnabled}. Other type of guardrails can use appropriate suffixes but should start with
+ * {@code <whatIsGuarded>}.
+ */
+public interface GuardrailsMBean
+{
+ /**
+ * Whether guardrails are enabled or not.
+ *
+ * @return {@code true} if guardrails are enabled, {@code false} otherwise
+ */
+ boolean getEnabled();
+
+ /**
+ * Enable/disable guardrails.
+ *
+ * @param enabled {@code true} for enabling guardrails, {@code false} for disabling them.
+ */
+ void setEnabled(boolean enabled);
+
+ /**
+ * @return The threshold to warn when creating more tables than threshold.
+ * -1 means disabled.
+ */
+ int getTablesWarnThreshold();
+
+ /**
+ * @return The threshold to prevent creating more tables than threshold.
+ * -1 means disabled.
+ */
+ int getTablesAbortThreshold();
+
+ /**
+ * @param warn The threshold to warn when creating more tables than threshold. -1 means disabled.
+ * @param abort The threshold to prevent creating more tables than threshold. -1 means disabled.
+ */
+ void setTablesThreshold(int warn, int abort);
+
+ /**
+ * @return The threshold to warn when having more columns per table than threshold.
+ * -1 means disabled.
+ */
+ int getColumnsPerTableWarnThreshold();
+
+ /**
+ * @return The threshold to prevent having more columns per table than threshold. -1 means disabled.
+ */
+ int getColumnsPerTableAbortThreshold();
+
+ /**
+ * @param warn The threshold to warn when having more columns per table than threshold. -1 means disabled.
+ * @param abort The threshold to prevent having more columns per table than threshold. -1 means disabled.
+ */
+ void setColumnsPerTableThreshold(int warn, int abort);
+
+ /**
+ * @return The threshold to warn when creating more secondary indexes per table than threshold. -1 means disabled.
+ */
+ int getSecondaryIndexesPerTableWarnThreshold();
+
+ /**
+ * @return The threshold to prevent creating more secondary indexes per table than threshold. -1 means disabled.
+ */
+ int getSecondaryIndexesPerTableAbortThreshold();
+
+ /**
+ * @param warn The threshold to warn when creating more secondary indexes per table than threshold. -1 means disabled.
+ * @param abort The threshold to prevent creating more secondary indexes per table than threshold. -1 means disabled.
+ */
+ void setSecondaryIndexesPerTableThreshold(int warn, int abort);
+
+ /**
+ * @return The threshold to warn when creating more materialized views per table than threshold.
+ * -1 means disabled.
+ */
+ int getMaterializedViewsPerTableWarnThreshold();
+
+ /**
+ * @return The threshold to prevent creating more materialized views per table than threshold.
+ * -1 means disabled.
+ */
+ int getMaterializedViewsPerTableAbortThreshold();
+
+ /**
+ * @param warn The threshold to warn when creating more materialized views per table than threshold. -1 means disabled.
+ * @param abort The threshold to prevent creating more materialized views per table than threshold. -1 means disabled.
+ */
+ void setMaterializedViewsPerTableThreshold(int warn, int abort);
+
+ /**
+ * @return properties that are not allowed when creating or altering a table.
+ */
+ Set<String> getTablePropertiesDisallowed();
+
+ /**
+ * @return Comma-separated list of properties that are not allowed when creating or altering a table.
+ */
+ String getTablePropertiesDisallowedCSV();
+
+ /**
+ * @param properties properties that are not allowed when creating or altering a table.
+ */
+ void setTablePropertiesDisallowed(Set<String> properties);
+
+ /**
+ * @param properties Comma-separated list of properties that are not allowed when creating or altering a table.
+ */
+ void setTablePropertiesDisallowedCSV(String properties);
+
+ /**
+ * @return properties that are ignored when creating or altering a table.
+ */
+ Set<String> getTablePropertiesIgnored();
+
+ /**
+ * @return Comma-separated list of properties that are ignored when creating or altering a table.
+ */
+ String getTablePropertiesIgnoredCSV();
+
+ /**
+ * @param properties properties that are ignored when creating or altering a table.
+ */
+ void setTablePropertiesIgnored(Set<String> properties);
+
+ /**
+ * @param properties Comma-separated list of properties that are ignored when creating or altering a table.
+ */
+ void setTablePropertiesIgnoredCSV(String properties);
+
+ /**
+ * Returns whether user-provided timestamps are allowed.
+ *
+ * @return {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+ */
+ boolean getUserTimestampsEnabled();
+
+ /**
+ * Sets whether user-provided timestamps are allowed.
+ *
+ * @param enabled {@code true} if user-provided timestamps are allowed, {@code false} otherwise.
+ */
+ void setUserTimestampsEnabled(boolean enabled);
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Threshold.java b/src/java/org/apache/cassandra/db/guardrails/Threshold.java
new file mode 100644
index 0000000..7115b9c
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Threshold.java
@@ -0,0 +1,153 @@
+/*
+ * 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.function.Function;
+import javax.annotation.Nullable;
+
+import org.apache.cassandra.service.ClientState;
+
+/**
+ * A guardrail based on numeric threshold(s).
+ *
+ * <p>A {@link Threshold} guardrail defines (up to) 2 thresholds, one at which a warning is issued, and a higher one
+ * at which the operation is aborted with an exception. Only one of those thresholds can be activated if desired.
+ *
+ * <p>This guardrail only handles guarding positive values.
+ */
+public class Threshold extends Guardrail
+{
+ private final Function<ClientState, Config> configProvider;
+ private final ErrorMessageProvider messageProvider;
+
+ /**
+ * Creates a new threshold guardrail.
+ *
+ * @param configProvider a {@link ClientState}-based provider of {@link Config}s.
+ * @param messageProvider a function to generate the warning or error message if the guardrail is triggered
+ */
+ public Threshold(Function<ClientState, Config> configProvider, ErrorMessageProvider messageProvider)
+ {
+ this.configProvider = configProvider;
+ this.messageProvider = messageProvider;
+ }
+
+ private String errMsg(boolean isWarning, String what, long value, long thresholdValue)
+ {
+ return messageProvider.createMessage(isWarning,
+ what,
+ value,
+ thresholdValue);
+ }
+
+ private long abortValue(Config config)
+ {
+ long abortValue = config.getAbortThreshold();
+ return abortValue < 0 ? Long.MAX_VALUE : abortValue;
+ }
+
+ private long warnValue(Config config)
+ {
+ long warnValue = config.getWarnThreshold();
+ return warnValue < 0 ? Long.MAX_VALUE : warnValue;
+ }
+
+ @Override
+ public boolean enabled(@Nullable ClientState state)
+ {
+ if (!super.enabled(state))
+ return false;
+
+ Config config = configProvider.apply(state);
+ return config.getAbortThreshold() >= 0 || config.getWarnThreshold() >= 0;
+ }
+
+ /**
+ * Apply the guardrail to the provided value, warning or aborting if appropriate.
+ *
+ * @param value The value to check.
+ * @param what A string describing what {@code value} is a value of. This is used in the error message if the
+ * guardrail is triggered. For instance, say the guardrail guards the size of column values, then this
+ * argument must describe which column of which row is triggering the guardrail for convenience.
+ * @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(long value, String what, @Nullable ClientState state)
+ {
+ if (!enabled(state))
+ return;
+
+ Config config = configProvider.apply(state);
+
+ long abortValue = abortValue(config);
+ if (value > abortValue)
+ {
+ triggerAbort(value, abortValue, what);
+ return;
+ }
+
+ long warnValue = warnValue(config);
+ if (value > warnValue)
+ triggerWarn(value, warnValue, what);
+ }
+
+ private void triggerAbort(long value, long abortValue, String what)
+ {
+ abort(errMsg(false, what, value, abortValue));
+ }
+
+ private void triggerWarn(long value, long warnValue, String what)
+ {
+ warn(errMsg(true, what, value, warnValue));
+ }
+
+ /**
+ * A function used to build the error message of a triggered {@link Threshold} guardrail.
+ */
+ interface ErrorMessageProvider
+ {
+ /**
+ * Called when the guardrail is triggered to build the corresponding error message.
+ *
+ * @param isWarning Whether the trigger is a warning one; otherwise it is an abort one.
+ * @param what A string, provided by the call to the {@link #guard} method, describing what the guardrail
+ * has been applied to (and that has triggered it).
+ * @param value The value that triggered the guardrail (as a string).
+ * @param threshold The threshold that was passed to trigger the guardrail (as a string).
+ */
+ String createMessage(boolean isWarning, String what, long value, long threshold);
+ }
+
+ /**
+ * Configuration class containing the thresholds to be used to check if the guarded value should trigger a warning
+ * or abort the operation.
+ */
+ public interface Config
+ {
+ /**
+ * @return The threshold to warn when the guarded value exceeds it. A negative value means disabled.
+ */
+ public long getWarnThreshold();
+
+ /**
+ * @return The threshold to abort the operation when the guarded value exceeds it. A negative value means disabled.
+ */
+ public long getAbortThreshold();
+ }
+}
diff --git a/src/java/org/apache/cassandra/db/guardrails/Values.java b/src/java/org/apache/cassandra/db/guardrails/Values.java
new file mode 100644
index 0000000..969b1f6
--- /dev/null
+++ b/src/java/org/apache/cassandra/db/guardrails/Values.java
@@ -0,0 +1,103 @@
+/*
+ * 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.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+
+import com.google.common.collect.Sets;
+
+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.
+ *
+ * @param <T> The type of the values of which certain are disallowed.
+ */
+public class Values<T> extends Guardrail
+{
+ private final Function<ClientState, Config<T>> configProvider;
+ private final String what;
+
+ /**
+ * Creates a new values guardrail.
+ *
+ * @param configProvider a {@link ClientState}-based provider of {@link Config}s.
+ * @param what The feature that is guarded by this guardrail (for reporting in error messages).
+ */
+ public Values(Function<ClientState, Config<T>> configProvider, String what)
+ {
+ this.configProvider = configProvider;
+ 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.
+ *
+ * @param values The values to check.
+ * @param ignoreAction An action called on the subset of {@code values} that should be ignored. This action
+ * should do whatever is necessary to make sure the value is ignored.
+ * @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, Consumer<T> ignoreAction, @Nullable ClientState state)
+ {
+ if (!enabled(state))
+ return;
+
+ Config<T> config = configProvider.apply(state);
+
+ Set<T> disallowed = config.getDisallowed();
+ Set<T> toDisallow = Sets.intersection(values, disallowed);
+ if (!toDisallow.isEmpty())
+ abort(format("Provided values %s are not allowed for %s (disallowed values are: %s)",
+ toDisallow.stream().sorted().collect(Collectors.toList()), what, disallowed.toString()));
+
+ Set<T> ignored = config.getIgnored();
+ Set<T> toIgnore = Sets.intersection(values, ignored);
+ if (!toIgnore.isEmpty())
+ {
+ warn(format("Ignoring provided values %s as they are not supported for %s (ignored values are: %s)",
+ toIgnore.stream().sorted().collect(Collectors.toList()), what, ignored.toString()));
+ toIgnore.forEach(ignoreAction);
+ }
+ }
+
+ /**
+ * Configuration class containing the sets of values to ignore and/or reject.
+ */
+ public interface Config<T>
+ {
+ /**
+ * @return The values to be ignored.
+ */
+ Set<T> getIgnored();
+
+ /**
+ * @return The values to be rejected.
+ */
+ Set<T> getDisallowed();
+ }
+}
diff --git a/src/java/org/apache/cassandra/schema/TableMetadata.java b/src/java/org/apache/cassandra/schema/TableMetadata.java
index 1e85d88..7205b94 100644
--- a/src/java/org/apache/cassandra/schema/TableMetadata.java
+++ b/src/java/org/apache/cassandra/schema/TableMetadata.java
@@ -987,6 +987,11 @@ public class TableMetadata implements SchemaElement
return columns.values();
}
+ public int numColumns()
+ {
+ return columns.size();
+ }
+
public Set<String> columnNames()
{
return columns.values().stream().map(c -> c.name.toString()).collect(toSet());
diff --git a/src/java/org/apache/cassandra/service/ClientState.java b/src/java/org/apache/cassandra/service/ClientState.java
index 5cd4d84..14a2ccf 100644
--- a/src/java/org/apache/cassandra/service/ClientState.java
+++ b/src/java/org/apache/cassandra/service/ClientState.java
@@ -23,7 +23,6 @@ import java.net.SocketAddress;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
-import java.util.ListIterator;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
@@ -492,9 +491,37 @@ public class ClientState
throw new UnauthorizedException("You have to be logged in and not anonymous to perform this request");
}
+ /**
+ * Checks if this user is an ordinary user (not a super or system user).
+ *
+ * @return {@code true} if this user is an ordinary user, {@code false} otherwise.
+ */
+ public boolean isOrdinaryUser()
+ {
+ return !isSuper() && !isSystem();
+ }
+
+ /**
+ * Checks if this user is a super user.
+ */
+ public boolean isSuper()
+ {
+ return !DatabaseDescriptor.getAuthenticator().requireAuthentication() || (user != null && user.isSuper());
+ }
+
+ /**
+ * Checks if the user is the system user.
+ *
+ * @return {@code true} if this user is the system user, {@code false} otherwise.
+ */
+ public boolean isSystem()
+ {
+ return isInternal;
+ }
+
public void ensureIsSuperuser(String message)
{
- if (DatabaseDescriptor.getAuthenticator().requireAuthentication() && (user == null || !user.isSuper()))
+ if (!isSuper())
throw new UnauthorizedException(message);
}
diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
index 921ec04..7c398c4 100644
--- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
+++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java
@@ -91,6 +91,12 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions",
"org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$InternodeEncryption",
"org.apache.cassandra.config.EncryptionOptions$ServerEncryptionOptions$OutgoingEncryptedPortSource",
+ "org.apache.cassandra.db.guardrails.GuardrailsConfig",
+ "org.apache.cassandra.db.guardrails.GuardrailsConfigMBean",
+ "org.apache.cassandra.config.GuardrailsOptions",
+ "org.apache.cassandra.config.GuardrailsOptions$IntThreshold",
+ "org.apache.cassandra.config.GuardrailsOptions$TableProperties",
+ "org.apache.cassandra.config.GuardrailsOptions$Threshold",
"org.apache.cassandra.config.ReplicaFilteringProtectionOptions",
"org.apache.cassandra.config.YamlConfigurationLoader",
"org.apache.cassandra.config.YamlConfigurationLoader$PropertiesChecker",
@@ -109,6 +115,8 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.db.commitlog.CommitLogSegmentManagerStandard",
"org.apache.cassandra.db.commitlog.CommitLog",
"org.apache.cassandra.db.commitlog.CommitLogMBean",
+ "org.apache.cassandra.db.guardrails.Threshold$Config",
+ "org.apache.cassandra.db.guardrails.Values$Config",
"org.apache.cassandra.dht.IPartitioner",
"org.apache.cassandra.distributed.api.IInstance",
"org.apache.cassandra.distributed.api.IIsolatedExecutor",
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java
new file mode 100644
index 0000000..cd2338b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnsPerTableTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of columns in a table, {@link Guardrails#columnsPerTable}.
+ */
+public class GuardrailColumnsPerTableTest extends ThresholdTester
+{
+ private static final int COLUMNS_PER_TABLE_WARN_THRESHOLD = 2;
+ private static final int COLUMNS_PER_TABLE_ABORT_THRESHOLD = 4;
+
+ public GuardrailColumnsPerTableTest()
+ {
+ super(COLUMNS_PER_TABLE_WARN_THRESHOLD,
+ COLUMNS_PER_TABLE_ABORT_THRESHOLD,
+ DatabaseDescriptor.getGuardrailsConfig().getColumnsPerTable(),
+ Guardrails::setColumnsPerTableThreshold,
+ Guardrails::getColumnsPerTableWarnThreshold,
+ Guardrails::getColumnsPerTableAbortThreshold);
+ }
+
+ @Override
+ protected long currentValue()
+ {
+ return getCurrentColumnFamilyStore().metadata().columns().size();
+ }
+
+ @Test
+ public void testCreateTable() throws Throwable
+ {
+ // partition key on skinny table
+ assertCreateTableValid("CREATE TABLE %s (k1 int, v int, PRIMARY KEY((k1)))");
+ assertCreateTableWarns(3, "CREATE TABLE %s (k1 int, k2 int, v int, PRIMARY KEY((k1, k2)))");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k1 int, k2 int, k3 int, v int, PRIMARY KEY((k1, k2, k3)))");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, v int, PRIMARY KEY((k1, k2, k3, k4)))");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, k5 int, v int, PRIMARY KEY((k1, k2, k3, k4, k5)))");
+
+ // partition key on wide table
+ assertCreateTableWarns(3, "CREATE TABLE %s (k1 int, c int, v int, PRIMARY KEY(k1, c))");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k1 int, k2 int, c int, v int, PRIMARY KEY((k1, k2), c))");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k1 int, k2 int, k3 int, c int, v int, PRIMARY KEY((k1, k2, k3), c))");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k1 int, k2 int, k3 int, k4 int, c int, v int, PRIMARY KEY((k1, k2, k3, k4), c))");
+
+ // clustering key
+ assertCreateTableWarns(3, "CREATE TABLE %s (k int, c1 int, v int, PRIMARY KEY(k, c1))");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k int, c1 int, c2 int, v int, PRIMARY KEY(k, c1, c2))");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k int, c1 int, c2 int, c3 int, v int, PRIMARY KEY(k, c1, c2, c3))");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k int, c1 int, c2 int, c3 int, c4 int, v int, PRIMARY KEY(k, c1, c2, c3, c4))");
+
+ // static column
+ assertCreateTableWarns(3, "CREATE TABLE %s (k int, c int, s1 int STATIC, PRIMARY KEY(k, c))");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, PRIMARY KEY(k, c))");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, s3 int STATIC, PRIMARY KEY(k, c))");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k int, c int, s1 int STATIC, s2 int STATIC, s3 int STATIC, s4 int STATIC, PRIMARY KEY(k, c))");
+
+ // regular column on skinny table
+ assertCreateTableValid("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+ assertCreateTableWarns(3, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int)");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int, v5 int)");
+
+ // regular column on wide table
+ assertCreateTableWarns(3, "CREATE TABLE %s (k int, c int, v1 int, PRIMARY KEY(k, c))");
+ assertCreateTableWarns(4, "CREATE TABLE %s (k int, c int, v1 int, v2 int, PRIMARY KEY(k, c))");
+ assertCreateTableAborts(5, "CREATE TABLE %s (k int, c int, v1 int, v2 int, v3 int, PRIMARY KEY(k, c))");
+ assertCreateTableAborts(6, "CREATE TABLE %s (k int, c int, v1 int, v2 int, v3 int, v4 int, PRIMARY KEY(k, c))");
+
+ // udt
+ String udt = createType("CREATE TYPE %s (a int, b int, c int, d int)");
+ assertValid(format("CREATE TABLE %s (k int PRIMARY KEY, v %s)", createTableName(), udt));
+ }
+
+ @Test
+ public void testAlterTableAddColumn() throws Throwable
+ {
+ // skinny table under threshold
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+ assertAddColumnWarns("ALTER TABLE %s ADD v2 int");
+ assertAddColumnWarns("ALTER TABLE %s ADD v3 int");
+ assertAddColumnAborts("ALTER TABLE %s ADD v4 int");
+
+ // skinny table at threshold
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+ assertAddColumnAborts("ALTER TABLE %s ADD v4 int");
+
+ // wide table
+ createTable("CREATE TABLE %s (k int, c int, v1 int, v2 int, PRIMARY KEY(k, c))");
+ assertAddColumnAborts("ALTER TABLE %s ADD v3 int");
+ assertAddColumnAborts("ALTER TABLE %s ADD s int STATIC");
+
+ // udt
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int)");
+ String udt = createType("CREATE TYPE %s (a int, b int, c int, d int)");
+ assertAddColumnWarns("ALTER TABLE %s ADD v2 " + udt);
+ }
+
+ /**
+ * Verifies that its possible to drop columns from a table that has more columns than the current threshold.
+ */
+ @Test
+ public void testAlterTableDropColumn() throws Throwable
+ {
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int)");
+ guardrails().setColumnsPerTableThreshold(2, 2);
+ assertDropColumnValid("ALTER TABLE %s DROP v2");
+ assertDropColumnValid("ALTER TABLE %s DROP v3");
+ assertAddColumnAborts("ALTER TABLE %s ADD v2 int");
+ }
+
+ @Test
+ public void testExcludedUsers() throws Throwable
+ {
+ String table = keyspace() + '.' + createTableName();
+ testExcludedUsers(() -> format("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)", table),
+ () -> format("ALTER TABLE %s ADD v5 int", table),
+ () -> format("DROP TABLE %s", table));
+ }
+
+ private void assertCreateTableValid(String query) throws Throwable
+ {
+ assertThresholdValid(format(query, keyspace() + '.' + createTableName()));
+ }
+
+ private void assertDropColumnValid(String query) throws Throwable
+ {
+ assertValid(format(query, keyspace() + '.' + currentTable()));
+ }
+
+ private void assertCreateTableWarns(int numColumns, String query) throws Throwable
+ {
+ assertWarns(numColumns, query, createTableName());
+ }
+
+ private void assertAddColumnWarns(String query) throws Throwable
+ {
+ assertWarns(currentValue() + 1, query, currentTable());
+ }
+
+ private void assertWarns(long numColumns, String query, String tableName) throws Throwable
+ {
+ assertThresholdWarns(format("The table %s has %s columns, this exceeds the warning threshold of %s.",
+ tableName,
+ numColumns,
+ guardrails().getColumnsPerTableWarnThreshold()),
+ format(query, keyspace() + '.' + tableName));
+ }
+
+ private void assertAddColumnAborts(String query) throws Throwable
+ {
+ assertAborts(currentValue() + 1, query, currentTable());
+ }
+
+ private void assertCreateTableAborts(long numColumns, String query) throws Throwable
+ {
+ assertAborts(numColumns, query, ABORT_TABLE);
+ }
+
+ private void assertAborts(long numColumns, String query, String tableName) throws Throwable
+ {
+ assertThresholdAborts(format("Tables cannot have more than %s columns, but %s provided for table %s",
+ guardrails().getColumnsPerTableAbortThreshold(),
+ numColumns,
+ tableName),
+ format(query, keyspace() + '.' + tableName));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java
new file mode 100644
index 0000000..debbfb1
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailSecondaryIndexesPerTable.java
@@ -0,0 +1,122 @@
+/*
+ * 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 com.google.common.base.Strings;
+import org.junit.Test;
+
+import org.apache.cassandra.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of secondary indexes in a table, {@link Guardrails#secondaryIndexesPerTable}.
+ */
+public class GuardrailSecondaryIndexesPerTable extends ThresholdTester
+{
+ private static final int INDEXES_PER_TABLE_WARN_THRESHOLD = 1;
+ private static final int INDEXES_PER_TABLE_ABORT_THRESHOLD = 3;
+
+ public GuardrailSecondaryIndexesPerTable()
+ {
+ super(INDEXES_PER_TABLE_WARN_THRESHOLD,
+ INDEXES_PER_TABLE_ABORT_THRESHOLD,
+ DatabaseDescriptor.getGuardrailsConfig().getSecondaryIndexesPerTable(),
+ Guardrails::setSecondaryIndexesPerTableThreshold,
+ Guardrails::getSecondaryIndexesPerTableWarnThreshold,
+ Guardrails::getSecondaryIndexesPerTableAbortThreshold);
+ }
+
+ @Override
+ protected long currentValue()
+ {
+ return getCurrentColumnFamilyStore().indexManager.listIndexes().size();
+ }
+
+ @Test
+ public void testCreateIndex() throws Throwable
+ {
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+ assertCreateIndexSucceeds("v1", "v1_idx");
+ assertCurrentValue(1);
+
+ assertCreateIndexWarns("v2", "");
+ assertCreateIndexWarns("v3", "v3_idx");
+ assertCreateIndexAborts("v4", "");
+ assertCreateIndexAborts("v2", "v2_idx");
+ assertCurrentValue(3);
+
+ // 2i guardrail will also affect custom indexes
+ assertCreateCustomIndexAborts("v2");
+
+ // drop the two first indexes, we should be able to create new indexes again
+ dropIndex(format("DROP INDEX %s.%s", keyspace(), "v3_idx"));
+ assertCurrentValue(2);
+
+ assertCreateIndexWarns("v3", "");
+ assertCreateCustomIndexAborts("v4");
+ assertCurrentValue(3);
+
+ // previous guardrail should not apply to another base table
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int, v3 int, v4 int)");
+ assertCreateIndexSucceeds("v4", "");
+ assertCreateIndexWarns("v3", "");
+ assertCreateIndexWarns("v2", "");
+ assertCreateIndexAborts("v1", "");
+ assertCurrentValue(3);
+ }
+
+ @Test
+ public void testExcludedUsers() throws Throwable
+ {
+ createTable("CREATE TABLE %s (k int primary key, v1 int, v2 int)");
+ testExcludedUsers(() -> "CREATE INDEX excluded_1 ON %s(v1)",
+ () -> "CREATE INDEX excluded_2 ON %s(v2)",
+ () -> "DROP INDEX excluded_1",
+ () -> "DROP INDEX excluded_2");
+ }
+
+ private void assertCreateIndexSucceeds(String column, String indexName) throws Throwable
+ {
+ assertThresholdValid(format("CREATE INDEX %s ON %s.%s(%s)", indexName, keyspace(), currentTable(), column));
+ }
+
+ private void assertCreateIndexWarns(String column, String indexName) throws Throwable
+ {
+ assertThresholdWarns(format("Creating secondary index %son table %s, current number of indexes %s exceeds warning threshold of %s.",
+ (Strings.isNullOrEmpty(indexName) ? "" : indexName + " "),
+ currentTable(),
+ currentValue() + 1,
+ guardrails().getSecondaryIndexesPerTableWarnThreshold()),
+ format("CREATE INDEX %s ON %%s(%s)", indexName, column));
+ }
+
+ private void assertCreateIndexAborts(String column, String indexName) throws Throwable
+ {
+ assertThresholdAborts(format("aborting the creation of secondary index %son table %s",
+ Strings.isNullOrEmpty(indexName) ? "" : indexName + " ", currentTable()),
+ format("CREATE INDEX %s ON %%s(%s)", indexName, column));
+ }
+
+ private void assertCreateCustomIndexAborts(String column) throws Throwable
+ {
+ assertThresholdAborts(format("aborting the creation of secondary index on table %s", currentTable()),
+ format("CREATE CUSTOM INDEX ON %%s (%s) USING 'org.apache.cassandra.index.sasi.SASIIndex'", column));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java
new file mode 100644
index 0000000..818a0b6
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablePropertiesTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import com.google.common.collect.ImmutableSet;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.cql3.statements.schema.TableAttributes;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Tests the guardrail for table properties, {@link Guardrails#tableProperties}.
+ */
+public class GuardrailTablePropertiesTest extends GuardrailTester
+{
+ private static final String CREATE_TABLE = "CREATE TABLE %s.%s(pk int, ck int, v int, PRIMARY KEY(pk, ck)) %s";
+ private static final String CREATE_VIEW = "CREATE MATERIALIZED VIEW %s.%s as SELECT * FROM %s.%s " +
+ "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";
+
+ @Before
+ public void before()
+ {
+ // only allow "gc_grace_seconds" and "comments"
+ Set<String> allowed = new HashSet<>(Arrays.asList("gc_grace_seconds", "comment"));
+ 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"));
+ }
+
+ @Test
+ public void testConfigValidation()
+ {
+ String message = "Invalid value for %s properties: null is not allowed";
+ assertInvalidProperty(Guardrails::setTablePropertiesIgnored, (Set<String>) null, message, "ignored");
+ assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, (Set<String>) null, message, "disallowed");
+
+ assertValidProperty(Collections.emptySet());
+ assertValidProperty(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"));
+ }
+
+ private void assertValidProperty(Set<String> properties)
+ {
+ assertValidProperty(Guardrails::setTablePropertiesIgnored, properties);
+ assertValidProperty(Guardrails::setTablePropertiesDisallowed, properties);
+ }
+
+ private void assertInvalidProperty(Set<String> properties, Set<String> rejected)
+ {
+ String message = "Invalid value for %s properties: '%s' do not parse as valid table properties";
+ assertInvalidProperty(Guardrails::setTablePropertiesIgnored, properties, message, "ignored", rejected);
+ assertInvalidProperty(Guardrails::setTablePropertiesDisallowed, properties, message, "disallowed", rejected);
+ }
+
+ @Test
+ public void testTableProperties() throws Throwable
+ {
+ // most table properties are not allowed
+ assertValid(this::createTableWithProperties);
+ assertAborts(() -> createTableWithProperties("with id = " + UUID.randomUUID()), "[id]");
+ assertAborts(() -> createTableWithProperties("with compression = { 'enabled': 'false' }"), "[compression]");
+ assertAborts(() -> createTableWithProperties("with compression = { 'enabled': 'false' } AND id = " + UUID.randomUUID()), "[compression, id]");
+ assertAborts(() -> createTableWithProperties("with compaction = { 'class': 'SizeTieredCompactionStrategy' }"), "[compaction]");
+ assertAborts(() -> createTableWithProperties("with gc_grace_seconds = 1000 and compression = { 'enabled': 'false' }"), "[compression]");
+
+ // though gc_grace_seconds alone is
+ assertValid(() -> createTableWithProperties("with gc_grace_seconds = 1000"));
+
+ // and comment is "ignored". So it should warn, and getting the comment on the created table should be empty,
+ // not the one we set.
+ AtomicReference<String> tableName = new AtomicReference<>();
+ assertWarns(() -> tableName.set(createTableWithProperties("with comment = 'my table'")), "[comment]");
+ assertEquals("", executeNet("SELECT comment FROM system_schema.tables WHERE keyspace_name=? AND table_name=?",
+ keyspace(),
+ tableName.get()).one().getString("comment"));
+
+ // alter column is allowed
+ assertValid(this::createTableWithProperties);
+ assertValid("ALTER TABLE %s ADD v1 int");
+ assertValid("ALTER TABLE %s DROP v1");
+ assertValid("ALTER TABLE %s RENAME pk to pk1");
+ }
+
+ @Test
+ public void testViewProperties() throws Throwable
+ {
+ // view properties is not allowed
+ createTableWithProperties();
+ assertValid(() -> createViewWithProperties(""));
+ assertAborts(() -> createViewWithProperties("with compression = { 'enabled': 'false' }"), "[compression]");
+ assertValid(() -> createViewWithProperties("with gc_grace_seconds = 1000"));
+
+ // alter mv properties except "gc_grace_seconds" is not allowed
+ assertValid(() -> alterViewWithProperties("gc_grace_seconds = 1000"));
+ assertAborts(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND default_time_to_live = 1"),
+ "[compaction, default_time_to_live]");
+ assertAborts(() -> alterViewWithProperties("compaction = { 'class': 'SizeTieredCompactionStrategy' } AND crc_check_chance = 1"),
+ "[compaction, crc_check_chance]");
+ }
+
+ @Test
+ public void testInvalidTableProperties()
+ {
+ assertConfigFails(c -> c.setTablePropertiesDisallowed("ID1", "gc_grace_seconds"), "[id1]");
+ assertConfigFails(c -> c.setTablePropertiesDisallowed("ID2", "Gc_Grace_Seconds"), "[id2]");
+ assertConfigFails(c -> c.setTablePropertiesIgnored("ID3", "gc_grace_seconds"), "[id3]");
+ assertConfigFails(c -> c.setTablePropertiesIgnored("ID4", "Gc_Grace_Seconds"), "[id4]");
+ }
+
+ @Test
+ public void testExcludedUsers() throws Throwable
+ {
+ testExcludedUsers(
+ () -> format(CREATE_TABLE, keyspace(), createTableName(), "WITH compaction = { 'class': 'SizeTieredCompactionStrategy' }"),
+ () -> format(CREATE_TABLE, keyspace(), createTableName(), "WITH gc_grace_seconds = 1000"),
+ () -> "ALTER TABLE %s WITH gc_grace_seconds = 1000 and default_time_to_live = 1000",
+ () -> "ALTER TABLE %s WITH compaction = { 'class': 'SizeTieredCompactionStrategy' }",
+ () -> format(CREATE_VIEW, keyspace(), createViewName(), keyspace(), currentTable(), "with compression = { 'enabled': 'false' }"),
+ () -> format(ALTER_VIEW, keyspace(), currentView(), "compaction = { 'class': 'SizeTieredCompactionStrategy' }"),
+ () -> format(ALTER_VIEW, keyspace(), currentView(), "gc_grace_seconds = 1000"),
+ () -> format(ALTER_VIEW, keyspace(), currentView(), "gc_grace_seconds = 1000 and crc_check_chance = 1"),
+ () -> format(ALTER_VIEW, keyspace(), currentView(), "compaction = { 'class': 'SizeTieredCompactionStrategy' }"));
+ }
+
+ private void createTableWithProperties()
+ {
+ createTableWithProperties("");
+ }
+
+ private String createTableWithProperties(String withClause)
+ {
+ String name = createTableName();
+ execute(userClientState, format(CREATE_TABLE, keyspace(), name, withClause));
+ return name;
+ }
+
+ private void createViewWithProperties(String withClause)
+ {
+ execute(userClientState, format(CREATE_VIEW, keyspace(), createViewName(), keyspace(), currentTable(), withClause));
+ }
+
+ private void alterViewWithProperties(String withClause)
+ {
+ execute(userClientState, format(ALTER_VIEW, keyspace(), currentView(), withClause));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java
new file mode 100644
index 0000000..96f1681
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTablesLimitTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.config.DatabaseDescriptor;
+import org.apache.cassandra.db.Keyspace;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the max number of user tables, {@link Guardrails#tablesLimit}.
+ */
+public class GuardrailTablesLimitTest extends ThresholdTester
+{
+ private static final int TABLES_LIMIT_WARN_THRESHOLD = 1;
+ private static final int TABLES_LIMIT_ABORT_THRESHOLD = 2;
+
+ public GuardrailTablesLimitTest()
+ {
+ super(TABLES_LIMIT_WARN_THRESHOLD,
+ TABLES_LIMIT_ABORT_THRESHOLD,
+ DatabaseDescriptor.getGuardrailsConfig().getTables(),
+ Guardrails::setTablesThreshold,
+ Guardrails::getTablesWarnThreshold,
+ Guardrails::getTablesAbortThreshold);
+ }
+
+ @Override
+ protected long currentValue()
+ {
+ return Keyspace.open(keyspace()).getColumnFamilyStores().size();
+ }
+
+ @Test
+ public void testCreateTable() throws Throwable
+ {
+ // create tables until hitting the two warn/abort thresholds
+ String t1 = assertCreateTableValid();
+ String t2 = assertCreateTableWarns();
+ assertCreateTableAborts();
+
+ // drop a table and hit the warn/abort threshold again
+ dropTable(t2);
+ String t3 = assertCreateTableWarns();
+ assertCreateTableAborts();
+
+ // drop two tables and hit the warn/abort threshold again
+ dropTable(t1);
+ dropTable(t3);
+ assertCreateTableValid();
+ assertCreateTableWarns();
+ assertCreateTableAborts();
+
+ // test excluded users
+ testExcludedUsers(this::createTableQuery,
+ this::createTableQuery,
+ this::createTableQuery);
+ }
+
+ @Override
+ protected void dropTable(String tableName)
+ {
+ dropFormattedTable(format("DROP TABLE %s.%s", keyspace(), tableName));
+ }
+
+ private String assertCreateTableValid() throws Throwable
+ {
+ String tableName = createTableName();
+ assertThresholdValid(createTableQuery(tableName));
+ return tableName;
+ }
+
+ private String assertCreateTableWarns() throws Throwable
+ {
+ String tableName = createTableName();
+ assertThresholdWarns(format("Creating table %s, current number of tables 2 exceeds warning threshold of 1", tableName),
+ createTableQuery(tableName));
+ return tableName;
+ }
+
+ private void assertCreateTableAborts() throws Throwable
+ {
+ String tableName = createTableName();
+ assertThresholdAborts(format("Cannot have more than 2 tables, aborting the creation of table %s", tableName),
+ createTableQuery(tableName));
+ }
+
+ private String createTableQuery()
+ {
+ return createTableQuery(createTableName());
+ }
+
+ private String createTableQuery(String tableName)
+ {
+ return format("CREATE TABLE %s.%s (k1 int, v int, PRIMARY KEY((k1)))", keyspace(), tableName);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java
new file mode 100644
index 0000000..134b360
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailTester.java
@@ -0,0 +1,284 @@
+/*
+ * 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.net.InetSocketAddress;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+
+import com.datastax.driver.core.exceptions.InvalidQueryException;
+import org.apache.cassandra.auth.AuthenticatedUser;
+import org.apache.cassandra.auth.CassandraRoleManager;
+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.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.messages.ResultMessage;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public abstract class GuardrailTester extends CQLTester
+{
+ // Name used when testing CREATE TABLE that should be aborted (we need to provide it as assertAborts, which
+ // is used to assert the failure, does not know that it is a CREATE TABLE and would thus reuse the name of the
+ // previously created table, which is not what we want).
+ protected static final String ABORT_TABLE = "abort_table_creation_test";
+
+ private static final String USERNAME = "guardrail_user";
+ private static final String PASSWORD = "guardrail_password";
+
+ protected static ClientState systemClientState, userClientState, superClientState;
+
+ @BeforeClass
+ public static void setUpClass()
+ {
+ CQLTester.setUpClass();
+ requireAuthentication();
+ requireNetwork();
+ guardrails().setEnabled(true);
+
+ systemClientState = ClientState.forInternalCalls();
+ userClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 123));
+ superClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 321));
+ superClientState.login(new AuthenticatedUser(CassandraRoleManager.DEFAULT_SUPERUSER_NAME));
+ }
+
+ /**
+ * Creates an ordinary user that is not excluded from guardrails, that is, a user that is not super not internal.
+ */
+ @Before
+ public void beforeGuardrailTest() throws Throwable
+ {
+ useSuperUser();
+ executeNet(format("CREATE USER IF NOT EXISTS %s WITH PASSWORD '%s'", USERNAME, PASSWORD));
+ executeNet(format("GRANT ALL ON KEYSPACE %s TO %s", KEYSPACE, USERNAME));
+ useUser(USERNAME, PASSWORD);
+
+ String useKeyspaceQuery = "USE " + keyspace();
+ execute(userClientState, useKeyspaceQuery);
+ execute(systemClientState, useKeyspaceQuery);
+ execute(superClientState, useKeyspaceQuery);
+ }
+
+ static Guardrails guardrails()
+ {
+ return Guardrails.instance;
+ }
+
+ protected <T> void assertValidProperty(BiConsumer<Guardrails, T> setter, T value)
+ {
+ setter.accept(guardrails(), value);
+ }
+
+ protected <T> void assertInvalidProperty(BiConsumer<Guardrails, T> setter,
+ T value,
+ String message,
+ Object... messageArgs)
+ {
+ Assertions.assertThatThrownBy(() -> setter.accept(guardrails(), value))
+ .isInstanceOf(IllegalArgumentException.class)
+ .hasMessage(format(message, messageArgs));
+ }
+
+ @SafeVarargs
+ protected final void testExcludedUsers(Supplier<String>... queries) throws Throwable
+ {
+ execute("USE " + keyspace());
+ assertSuperuserIsExcluded(queries);
+ assertInternalQueriesAreExcluded(queries);
+ }
+
+ @SafeVarargs
+ private final void assertInternalQueriesAreExcluded(Supplier<String>... queries) throws Throwable
+ {
+ for (Supplier<String> query : queries)
+ {
+ assertValid(() -> execute(systemClientState, query.get()));
+ }
+ }
+
+ @SafeVarargs
+ private final void assertSuperuserIsExcluded(Supplier<String>... queries) throws Throwable
+ {
+ for (Supplier<String> query : queries)
+ {
+ assertValid(() -> execute(superClientState, query.get()));
+ }
+ }
+
+ protected void assertValid(CheckedFunction function) throws Throwable
+ {
+ ClientWarn.instance.captureWarnings();
+ try
+ {
+ function.apply();
+ assertEmptyWarnings();
+ }
+ catch (InvalidRequestException e)
+ {
+ fail("Expected not to fail, but failed with error message: " + e.getMessage());
+ }
+ finally
+ {
+ ClientWarn.instance.resetWarnings();
+ }
+ }
+
+ protected void assertValid(String query) throws Throwable
+ {
+ assertValid(() -> execute(userClientState, query));
+ }
+
+ protected void assertWarns(CheckedFunction function, String message) throws Throwable
+ {
+ // We use client warnings to check we properly warn as this is the most convenient. Technically,
+ // this doesn't validate we also log the warning, but that's probably fine ...
+ ClientWarn.instance.captureWarnings();
+ try
+ {
+ function.apply();
+ assertWarnings(message);
+ }
+ finally
+ {
+ ClientWarn.instance.resetWarnings();
+ }
+ }
+
+ protected void assertWarns(String message, String query) throws Throwable
+ {
+ assertWarns(() -> execute(userClientState, query), message);
+ }
+
+ protected void assertAborts(CheckedFunction function, String message) throws Throwable
+ {
+ assertAborts(function, message, true);
+ }
+
+ protected void assertAborts(CheckedFunction function, String message, boolean thrown) throws Throwable
+ {
+ ClientWarn.instance.captureWarnings();
+ try
+ {
+ function.apply();
+
+ if (thrown)
+ fail("Expected to fail, but it did not");
+ }
+ catch (InvalidRequestException | InvalidQueryException e)
+ {
+ assertTrue("Expect no exception thrown", thrown);
+
+ assertTrue(format("Full error message '%s' does not contain expected message '%s'", e.getMessage(), message),
+ e.getMessage().contains(message));
+
+ assertWarnings(message);
+ }
+ finally
+ {
+ ClientWarn.instance.resetWarnings();
+ }
+ }
+
+ protected void assertAborts(String message, String query) throws Throwable
+ {
+ assertAborts(() -> execute(userClientState, query), message);
+ }
+
+ private void assertWarnings(String message)
+ {
+ List<String> warnings = getWarnings();
+
+ assertFalse("Expected to warn, but no warning was received", warnings == null || warnings.isEmpty());
+ assertEquals(format("Got more thant 1 warning (got %d => %s)", warnings.size(), warnings),
+ 1,
+ warnings.size());
+
+ String warning = warnings.get(0);
+ assertTrue(format("Warning log message '%s' does not contain expected message '%s'", warning, message),
+ warning.contains(message));
+ }
+
+ private void assertEmptyWarnings()
+ {
+ List<String> warnings = getWarnings();
+
+ if (warnings == null) // will always be the case in practice currently, but being defensive if this change
+ warnings = Collections.emptyList();
+
+ assertTrue(format("Expect no warning messages but got %s", warnings), warnings.isEmpty());
+ }
+
+ private List<String> getWarnings()
+ {
+ List<String> warnings = ClientWarn.instance.getWarnings();
+
+ return warnings == null
+ ? Collections.emptyList()
+ : warnings.stream()
+ .filter(w -> !w.equals(View.USAGE_WARNING) && !w.equals(SASIIndex.USAGE_WARNING))
+ .collect(Collectors.toList());
+ }
+
+ protected void assertConfigFails(Consumer<Guardrails> consumer, String message)
+ {
+ try
+ {
+ consumer.accept(guardrails());
+ fail("Expected failure");
+ }
+ catch (IllegalArgumentException e)
+ {
+ String actualMessage = e.getMessage();
+ assertTrue(String.format("Failure message '%s' does not contain expected message '%s'", actualMessage, message),
+ actualMessage.contains(message));
+ }
+ }
+
+ protected ResultMessage execute(ClientState state, String query)
+ {
+ QueryState queryState = new QueryState(state);
+
+ String formattedQuery = formatQuery(query);
+ CQLStatement statement = QueryProcessor.parseStatement(formattedQuery, queryState.getClientState());
+ statement.validate(state);
+
+ QueryOptions options = QueryOptions.forInternalCalls(Collections.emptyList());
+
+ return statement.executeLocally(queryState, options);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java
new file mode 100644
index 0000000..20b6a52
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailUserTimestampsTest.java
@@ -0,0 +1,124 @@
+/*
+ * 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;
+
+/**
+ * Tests the guardrail for disabling user-provided timestamps, {@link Guardrails#userTimestampsEnabled}.
+ */
+public class GuardrailUserTimestampsTest extends GuardrailTester
+{
+ @Before
+ public void setupTest()
+ {
+ createTable("CREATE TABLE IF NOT EXISTS %s (k INT, c INT, v TEXT, PRIMARY KEY(k, c))");
+ }
+
+ private void setGuardrail(boolean userTimestampsEnabled)
+ {
+ guardrails().setUserTimestampsEnabled(userTimestampsEnabled);
+ }
+
+ @Test
+ public void testInsertWithDisabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(false);
+ assertAborts("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1");
+ }
+
+ @Test
+ public void testInsertWithEnabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(true);
+ assertValid("INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1");
+ }
+
+ @Test
+ public void testUpdateWithDisabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(false);
+ assertAborts("UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2");
+ }
+
+ @Test
+ public void testUpdateWithEnabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(true);
+ assertValid("UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2");
+ }
+
+ @Test
+ public void testDeleteWithDisabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(false);
+ assertAborts("DELETE FROM %s USING TIMESTAMP 1 WHERE k=1");
+ }
+
+ @Test
+ public void testDeleteWithEnabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(true);
+ assertValid("DELETE FROM %s USING TIMESTAMP 1 WHERE k=1");
+ }
+
+ @Test
+ public void testBatchWithDisabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(false);
+ assertValid("BEGIN BATCH USING TIMESTAMP 1 " +
+ "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') " +
+ "APPLY BATCH");
+ assertAborts("BEGIN BATCH " +
+ "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1 " +
+ "APPLY BATCH");
+ }
+
+ @Test
+ public void testBatchWithEnabledUserTimestamps() throws Throwable
+ {
+ setGuardrail(true);
+ assertValid("BEGIN BATCH USING TIMESTAMP 1 " +
+ "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') " +
+ "APPLY BATCH");
+ assertValid("BEGIN BATCH " +
+ "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1 " +
+ "APPLY BATCH");
+ }
+
+ @Test
+ public void testExcludedUsers() throws Throwable
+ {
+ for (boolean userTimestampsEnabled : new boolean[]{ false, true })
+ {
+ setGuardrail(userTimestampsEnabled);
+ testExcludedUsers(() -> "INSERT INTO %s (k, c, v) VALUES (1, 2, 'val') USING TIMESTAMP 1",
+ () -> "UPDATE %s USING TIMESTAMP 1 SET v = 'val2' WHERE k = 1 and c = 2",
+ () -> "DELETE FROM %s USING TIMESTAMP 1 WHERE k=1",
+ () -> "BEGIN BATCH USING TIMESTAMP 1 INSERT INTO %s (k, c, v) VALUES (1, 2, 'v'); APPLY BATCH",
+ () -> "BEGIN BATCH INSERT INTO %s (k, c, v) VALUES (1, 2, 'v') USING TIMESTAMP 1; APPLY BATCH");
+ }
+ }
+
+ private void assertAborts(String query) throws Throwable
+ {
+ assertAborts("User provided timestamps (USING TIMESTAMP) is not allowed", query);
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java
new file mode 100644
index 0000000..628e244
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailViewsPerTableTest.java
@@ -0,0 +1,125 @@
+/*
+ * 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.config.DatabaseDescriptor;
+
+import static java.lang.String.format;
+
+/**
+ * Tests the guardrail for the number of secondary indexes in a table, {@link Guardrails#secondaryIndexesPerTable}.
+ */
+public class GuardrailViewsPerTableTest extends ThresholdTester
+{
+ private static final int VIEWS_PER_TABLE_WARN_THRESHOLD = 1;
+ private static final int VIEWS_PER_TABLE_ABORT_THRESHOLD = 3;
+
+ private static final String CREATE_TABLE = "CREATE TABLE %s (k int PRIMARY KEY, v int)";
+ private static final String CREATE_VIEW = "CREATE MATERIALIZED VIEW %s AS SELECT * FROM %%s " +
+ "WHERE k IS NOT NULL AND v IS NOT NULL PRIMARY KEY (v, k)";
+
+ public GuardrailViewsPerTableTest()
+ {
+ super(VIEWS_PER_TABLE_WARN_THRESHOLD,
+ VIEWS_PER_TABLE_ABORT_THRESHOLD,
+ DatabaseDescriptor.getGuardrailsConfig().getMaterializedViewsPerTable(),
+ Guardrails::setMaterializedViewsPerTableThreshold,
+ Guardrails::getMaterializedViewsPerTableWarnThreshold,
+ Guardrails::getMaterializedViewsPerTableAbortThreshold);
+ }
+
+ @Override
+ protected long currentValue()
+ {
+ return getCurrentColumnFamilyStore().viewManager.size();
+ }
+
+ @Before
+ public void before()
+ {
+ super.before();
+ createTable(CREATE_TABLE);
+ }
+
+ @Test
+ public void testCreateView() throws Throwable
+ {
+ String view1 = assertCreateViewSucceeds();
+ assertCurrentValue(1);
+
+ assertCreateViewWarns();
+ assertCreateViewWarns();
+ assertCreateViewAborts();
+ assertCurrentValue(3);
+
+ // drop the first view, we should be able to create new MV again
+ dropView(view1);
+ assertCurrentValue(2);
+ assertCreateViewWarns();
+ assertCreateViewAborts();
+ assertCurrentValue(3);
+
+ // previous guardrail should not apply to another base table
+ createTable("CREATE TABLE %s (k int primary key, v int)");
+ assertCreateViewSucceeds();
+ assertCreateViewWarns();
+ assertCreateViewWarns();
+ assertCreateViewAborts();
+ assertCurrentValue(3);
+ }
+
+ @Test
+ public void testExcludedUsers() throws Throwable
+ {
+ createTable("CREATE TABLE %s (k int PRIMARY KEY, v1 int, v2 int)");
+
+ testExcludedUsers(() -> format("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
+ " WHERE k IS NOT NULL AND v1 IS NOT NULL PRIMARY KEY (v1, k)",
+ createViewName(), currentTable()),
+ () -> format("CREATE MATERIALIZED VIEW %s AS SELECT * FROM %s " +
+ " WHERE k IS NOT NULL AND v2 IS NOT NULL PRIMARY KEY (v2, k)",
+ createViewName(), currentTable()));
+ }
+
+ private String assertCreateViewSucceeds() throws Throwable
+ {
+ String viewName = createViewName();
+ assertThresholdValid(format(CREATE_VIEW, viewName));
+ return viewName;
+ }
+
+ private void assertCreateViewWarns() throws Throwable
+ {
+ String viewName = createViewName();
+ assertThresholdWarns(format("Creating materialized view %s on table %s, current number of views %s exceeds warning threshold of %s.",
+ viewName, currentTable(), currentValue() + 1, guardrails().getMaterializedViewsPerTableWarnThreshold()),
+ format(CREATE_VIEW, viewName));
+ }
+
+ private void assertCreateViewAborts() throws Throwable
+ {
+ String viewName = createViewName();
+ assertThresholdAborts(format("aborting the creation of materialized view %s on table %s",
+ viewName, currentTable()),
+ format(CREATE_VIEW, viewName));
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java
new file mode 100644
index 0000000..20e1aea
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsConfigProviderTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.config.GuardrailsOptions;
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.apache.cassandra.service.ClientState;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+
+public class GuardrailsConfigProviderTest extends GuardrailTester
+{
+ @Test
+ public void testBuildCustom() throws Throwable
+ {
+ String name = getClass().getCanonicalName() + '$' + CustomProvider.class.getSimpleName();
+ GuardrailsConfigProvider provider = GuardrailsConfigProvider.build(name);
+ Threshold guard = new Threshold(state -> provider.getOrCreate(state).getTables(),
+ (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+ isWarn ? "Warning" : "Aborting", what, v, t));
+
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+ assertWarns(() -> guard.guard(25, "A", userClientState), "Warning: for A, 25 > 10");
+ assertWarns(() -> guard.guard(100, "B", userClientState), "Warning: for B, 100 > 10");
+ assertAborts(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
+ assertAborts(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+
+ Assertions.assertThatThrownBy(() -> GuardrailsConfigProvider.build("unexistent_class"))
+ .isInstanceOf(ConfigurationException.class)
+ .hasMessageContaining("Unable to find custom guardrails config provider class 'unexistent_class'");
+ }
+
+ /**
+ * Custom {@link GuardrailsConfigProvider} implementation that simply duplicates the threshold values.
+ */
+ public static class CustomProvider extends GuardrailsConfigProvider.Default
+ {
+ public GuardrailsConfig getOrCreate(ClientState state)
+ {
+ return new CustomConfig();
+ }
+ }
+
+ public static class CustomConfig extends GuardrailsOptions
+ {
+ private final IntThreshold tables = new IntThreshold();
+
+ public CustomConfig()
+ {
+ tables.setThresholds(10, 100);
+ }
+
+ @Override
+ public IntThreshold getTables()
+ {
+ return tables;
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java
new file mode 100644
index 0000000..13d9ee3
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailsTest.java
@@ -0,0 +1,300 @@
+/*
+ * 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.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class GuardrailsTest extends GuardrailTester
+{
+ @Test
+ public void testDisabledThreshold() throws Throwable
+ {
+ Threshold.ErrorMessageProvider errorMessageProvider = (isWarn, what, v, t) -> "Should never trigger";
+ testDisabledThreshold(new Threshold(state -> new ThresholdConfig(), errorMessageProvider));
+ }
+
+ private void testDisabledThreshold(Threshold guard) throws Throwable
+ {
+ assertFalse(guard.enabled(userClientState));
+
+ assertValid(() -> guard.guard(5, "Z", null));
+ assertValid(() -> guard.guard(25, "A", userClientState));
+ assertValid(() -> guard.guard(100, "B", userClientState));
+ assertValid(() -> guard.guard(101, "X", userClientState));
+ assertValid(() -> guard.guard(200, "Y", userClientState));
+ }
+
+ @Test
+ public void testThreshold() throws Throwable
+ {
+ Threshold guard = new Threshold(state -> new ThresholdConfig(10, 100),
+ (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+ isWarn ? "Warning" : "Aborting", what, v, t));
+
+ assertTrue(guard.enabled(userClientState));
+
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+ assertWarns(() -> guard.guard(25, "A", userClientState), "Warning: for A, 25 > 10");
+ assertWarns(() -> guard.guard(100, "B", userClientState), "Warning: for B, 100 > 10");
+ assertAborts(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
+ assertAborts(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+ }
+
+ @Test
+ public void testWarnOnlyThreshold() throws Throwable
+ {
+ Threshold guard = new Threshold(state -> new ThresholdConfig(10, ThresholdConfig.DISABLED),
+ (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+ isWarn ? "Warning" : "Aborting", what, v, t));
+
+ assertTrue(guard.enabled(userClientState));
+
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+ assertWarns(() -> guard.guard(11, "A", userClientState), "Warning: for A, 11 > 10");
+ }
+
+ @Test
+ public void testAbortOnlyThreshold() throws Throwable
+ {
+ Threshold guard = new Threshold(state -> new ThresholdConfig(ThresholdConfig.DISABLED, 10),
+ (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+ isWarn ? "Warning" : "Aborting", what, v, t));
+
+ assertTrue(guard.enabled(userClientState));
+
+ assertValid(() -> guard.guard(5, "Z", userClientState));
+ assertAborts(() -> guard.guard(11, "A", userClientState), "Aborting: for A, 11 > 10");
+ }
+
+ @Test
+ public void testThresholdUsers() throws Throwable
+ {
+ Threshold guard = new Threshold(state -> new ThresholdConfig(10, 100),
+ (isWarn, what, v, t) -> format("%s: for %s, %s > %s",
+ isWarn ? "Warning" : "Aborting", what, v, t));
+
+ // value under both thresholds
+ assertValid(() -> guard.guard(5, "x", null));
+ assertValid(() -> guard.guard(5, "x", userClientState));
+ assertValid(() -> guard.guard(5, "x", systemClientState));
+ assertValid(() -> guard.guard(5, "x", superClientState));
+
+ // value over warning threshold
+ assertWarns(() -> guard.guard(100, "y", null), "Warning: for y, 100 > 10");
+ assertWarns(() -> guard.guard(100, "y", userClientState), "Warning: for y, 100 > 10");
+ assertValid(() -> guard.guard(100, "y", systemClientState));
+ assertValid(() -> guard.guard(100, "y", superClientState));
+
+ // value over abort threshold
+ assertAborts(() -> guard.guard(101, "z", null), "Aborting: for z, 101 > 100");
+ assertAborts(() -> guard.guard(101, "z", userClientState), "Aborting: for z, 101 > 100");
+ assertValid(() -> guard.guard(101, "z", systemClientState));
+ assertValid(() -> guard.guard(101, "z", superClientState));
+ }
+
+ @Test
+ public void testDisableFlag() throws Throwable
+ {
+ assertAborts(() -> new DisableFlag(state -> true, "X").ensureEnabled(userClientState), "X is not allowed");
+ assertValid(() -> new DisableFlag(state -> false, "X").ensureEnabled(userClientState));
+
+ assertAborts(() -> new DisableFlag(state -> true, "X").ensureEnabled("Y", userClientState), "Y is not allowed");
+ assertValid(() -> new DisableFlag(state -> false, "X").ensureEnabled("Y", userClientState));
+ }
+
+ @Test
+ public void testDisableFlagUsers() throws Throwable
+ {
+ DisableFlag enabled = new DisableFlag(state -> false, "X");
+ assertValid(() -> enabled.ensureEnabled(null));
+ assertValid(() -> enabled.ensureEnabled(userClientState));
+ assertValid(() -> enabled.ensureEnabled(systemClientState));
+ assertValid(() -> enabled.ensureEnabled(superClientState));
+
+ DisableFlag disabled = new DisableFlag(state -> true, "X");
+ assertAborts(() -> disabled.ensureEnabled(null), "X is not allowed");
+ assertAborts(() -> disabled.ensureEnabled(userClientState), "X is not allowed");
+ assertValid(() -> disabled.ensureEnabled(systemClientState));
+ assertValid(() -> disabled.ensureEnabled(superClientState));
+ }
+
+ @Test
+ public void testDisallowedValues() 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 -> new ValuesConfig(Collections.emptySet(), 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));
+ assertAborts(() -> 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));
+ assertAborts(() -> 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));
+
+ assertAborts(() -> disallowed.guard(set(4, 6), action, null),
+ "Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
+ assertAborts(() -> 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 testDisallowedValuesUsers() throws Throwable
+ {
+ Values<Integer> disallowed = new Values<>(state -> new ValuesConfig(Collections.emptySet(), 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])";
+ assertAborts(() -> disallowed.guard(set(2), action, null), message);
+ assertAborts(() -> 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])";
+ assertAborts(() -> disallowed.guard(disallowedValues, action, null), message);
+ assertAborts(() -> disallowed.guard(disallowedValues, action, userClientState), message);
+ assertValid(() -> disallowed.guard(disallowedValues, action, systemClientState));
+ assertValid(() -> disallowed.guard(disallowedValues, action, superClientState));
+ }
+
+ @Test
+ public void testIgnoredValues() 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 -> new ValuesConfig(insertionOrderedSet(4, 6, 20), Collections.emptySet()),
+ "integer");
+
+ Set<Integer> triggeredOn = set();
+ assertValid(() -> ignored.guard(set(3), triggeredOn::add, userClientState));
+ assertEquals(set(), triggeredOn);
+
+ assertWarns(() -> ignored.guard(set(4), triggeredOn::add, userClientState),
+ "Ignoring provided values [4] as they are not supported for integer (ignored values are: [4, 6, 20])");
+ assertEquals(set(4), triggeredOn);
+ triggeredOn.clear();
+
+ assertWarns(() -> ignored.guard(set(4, 6), triggeredOn::add, null),
+ "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();
+
+ assertWarns(() -> ignored.guard(set(4, 5, 6, 7), triggeredOn::add, null),
+ "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();
+ }
+
+ private static Set<Integer> set(Integer value)
+ {
+ return Collections.singleton(value);
+ }
+
+ private static Set<Integer> set(Integer... values)
+ {
+ return new HashSet<>(Arrays.asList(values));
+ }
+
+ @SafeVarargs
+ private static <T> Set<T> insertionOrderedSet(T... values)
+ {
+ return new LinkedHashSet<>(Arrays.asList(values));
+ }
+
+ private static class ThresholdConfig implements Threshold.Config
+ {
+ public static final int DISABLED = -1;
+
+ private final int warn;
+ private final int abort;
+
+ public ThresholdConfig()
+ {
+ this.warn = DISABLED;
+ this.abort = DISABLED;
+ }
+
+ public ThresholdConfig(int warn, int abort)
+ {
+ this.warn = warn;
+ this.abort = abort;
+ }
+
+ public long getWarnThreshold()
+ {
+ return warn;
+ }
+
+ public long getAbortThreshold()
+ {
+ return abort;
+ }
+ }
+
+ private static class ValuesConfig implements Values.Config<Integer>
+ {
+ private final Set<Integer> ignored;
+ private final Set<Integer> disallowed;
+
+ public ValuesConfig(Set<Integer> ignored, Set<Integer> disallowed)
+ {
+ this.ignored = ignored;
+ this.disallowed = disallowed;
+ }
+
+ public Set<Integer> getIgnored()
+ {
+ return ignored;
+ }
+
+ public Set<Integer> getDisallowed()
+ {
+ return disallowed;
+ }
+ }
+}
diff --git a/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java
new file mode 100644
index 0000000..fdaa50b
--- /dev/null
+++ b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java
@@ -0,0 +1,188 @@
+/*
+ * 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.function.BiConsumer;
+import java.util.function.ToIntFunction;
+import java.util.function.ToLongFunction;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.cassandra.config.GuardrailsOptions;
+import org.assertj.core.api.Assertions;
+
+import static java.lang.String.format;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Utility class for testing a {@link Threshold} guardrail.
+ */
+public abstract class ThresholdTester extends GuardrailTester
+{
+ private final long warnThreshold;
+ private final long abortThreshold;
+ private final GuardrailsOptions.Threshold config;
+ private final TriConsumer<Guardrails, Long, Long> setter;
+ private final ToLongFunction<Guardrails> warnGetter;
+ private final ToLongFunction<Guardrails> abortGetter;
+
+ protected ThresholdTester(long warnThreshold,
+ long abortThreshold,
+ GuardrailsOptions.Threshold config,
+ TriConsumer<Guardrails, Long, Long> setter,
+ ToLongFunction<Guardrails> warnGetter,
+ ToLongFunction<Guardrails> abortGetter)
+ {
+ this.warnThreshold = warnThreshold;
+ this.abortThreshold = abortThreshold;
+ this.config = config;
+ this.setter = setter;
+ this.warnGetter = warnGetter;
+ this.abortGetter = abortGetter;
+ }
+
+ protected ThresholdTester(int warnThreshold,
+ int abortThreshold,
+ GuardrailsOptions.IntThreshold config,
+ TriConsumer<Guardrails, Integer, Integer> setter,
+ ToIntFunction<Guardrails> warnGetter,
+ ToIntFunction<Guardrails> abortGetter)
+ {
+ this(warnThreshold,
+ abortThreshold,
+ (GuardrailsOptions.Threshold) config,
+ (g, w, a) -> setter.accept(g, w.intValue(), a.intValue()),
+ g -> (long) warnGetter.applyAsInt(g),
+ g -> (long) abortGetter.applyAsInt(g));
+ }
+
+ protected abstract long currentValue();
+
+ protected void assertCurrentValue(int count)
+ {
+ assertEquals(count, currentValue());
+ }
+
+ @Before
+ public void before()
+ {
+ setter.accept(guardrails(), warnThreshold, abortThreshold);
+ }
+
+ @Test
+ public void testConfigValidation()
+ {
+ testValidationOfThresholdProperties("warn threshold", "abort threshold");
+ }
+
+ protected void testValidationOfThresholdProperties(String warnName, String abortName)
+ {
+ setter.accept(guardrails(), -1L, -1L);
+
+ testValidationOfStrictlyPositiveProperty((g, a) -> setter.accept(g, -1L, a), abortName);
+ testValidationOfStrictlyPositiveProperty((g, w) -> setter.accept(g, w, -1L), warnName);
+
+ setter.accept(guardrails(), -1L, -1L);
+ Assertions.assertThatThrownBy(() -> setter.accept(guardrails(), 2L, 1L))
+ .hasMessageContaining("The warn threshold 2 should be lower than the abort threshold 1");
+ }
+
+ protected void assertThresholdValid(String query) throws Throwable
+ {
+ assertValid(query);
+
+ Assertions.assertThat(currentValue())
+ .isLessThanOrEqualTo(warnGetter.applyAsLong(guardrails()))
+ .isLessThanOrEqualTo(abortGetter.applyAsLong(guardrails()));
+ }
+
+ protected void assertThresholdWarns(String message, String query) throws Throwable
+ {
+ assertWarns(message, query);
+
+ Assertions.assertThat(currentValue())
+ .isGreaterThan(warnGetter.applyAsLong(guardrails()))
+ .isLessThanOrEqualTo(abortGetter.applyAsLong(guardrails()));
+ }
+
+ protected void assertThresholdAborts(String message, String query) throws Throwable
+ {
+ assertAborts(message, query);
+
+ Assertions.assertThat(currentValue())
+ .isGreaterThanOrEqualTo(warnGetter.applyAsLong(guardrails()))
+ .isEqualTo(abortGetter.applyAsLong(guardrails()));
+ }
+
+ private void assertInvalidPositiveProperty(BiConsumer<Guardrails, Long> setter,
+ long value,
+ long maxValue,
+ boolean allowZero,
+ String name)
+ {
+ try
+ {
+ assertValidProperty(setter, value);
+ fail(format("Expected exception for guardrails.%s value: %d", name, value));
+ }
+ catch (IllegalArgumentException e)
+ {
+ String expectedMessage = null;
+
+ if (value > maxValue)
+ expectedMessage = format("Invalid value %d for %s: maximum allowed value is %d",
+ value, name, maxValue);
+ if (value == 0 && !allowZero)
+ expectedMessage = format("Invalid value for %s: 0 is not allowed; if attempting to disable use %s",
+ name, GuardrailsOptions.Threshold.DISABLED);
+
+ if (value < GuardrailsOptions.Threshold.DISABLED)
+ expectedMessage = format("Invalid value %d for %s: negative values are not "
+ + "allowed, outside of %s which disables the guardrail",
+ value, name, GuardrailsOptions.Threshold.DISABLED);
+
+ assertEquals(format("Exception message '%s' does not contain '%s'", e.getMessage(), expectedMessage),
+ expectedMessage, e.getMessage());
+ }
+ }
+
+ private void assertInvalidStrictlyPositiveProperty(BiConsumer<Guardrails, Long> setter, long value, String name)
+ {
+ assertInvalidPositiveProperty(setter, value, config.maxValue(), config.allowZero(), name);
+ }
+
+ protected void testValidationOfStrictlyPositiveProperty(BiConsumer<Guardrails, Long> setter, String name)
+ {
+ assertInvalidStrictlyPositiveProperty(setter, Integer.MIN_VALUE, name);
+ assertInvalidStrictlyPositiveProperty(setter, -2, name);
+ assertValidProperty(setter, GuardrailsOptions.Threshold.DISABLED); // disabled
+ assertInvalidStrictlyPositiveProperty(setter, 0, name);
+ assertValidProperty(setter, 1L);
+ assertValidProperty(setter, 2L);
+ assertValidProperty(setter, config.maxValue());
+ }
+
+ @FunctionalInterface
+ public interface TriConsumer<T, U, V>
+ {
+ void accept(T t, U u, V v);
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org