Add diagnostic events for guardrails

patch by Andrés de la Peña; reviewed by Berenguer Blasi and Stefan Miklosovic for CASSANDRA-17197

Co-authored-by: Andrés de la Peña <a.penya.garcia@gmail.com>
Co-authored-by: Aleksandr Sorokoumov <aleksandr.sorokoumov@gmail.com>
This commit is contained in:
Andrés de la Peña 2022-03-03 18:17:38 +00:00
parent 2f84f492a2
commit 143a5e8b06
26 changed files with 544 additions and 105 deletions

View File

@ -1,4 +1,5 @@
4.1
* Add diagnostic events for guardrails (CASSANDRA-17197)
* Increase cqlsh version (CASSANDRA-17432)
* Update SUPPORTED_UPGRADE_PATHS to include 3.0 and 3.x to 4.1 paths and remove obsolete tests (CASSANDRA-17362)
* Support DELETE in CQLSSTableWriter (CASSANDRA-14797)

View File

@ -1583,7 +1583,7 @@ drop_compact_storage_enabled: false
# abort_threshold_kb: 0
# Whether guardrails are enabled or not. Guardrails are disabled by default.
# guardrails_enabled: true
# guardrails_enabled: false
# Guardrail to warn or fail when creating more user keyspaces than threshold.
# The two thresholds default to -1 to disable.
# keyspaces_warn_threshold: -1

View File

@ -513,7 +513,7 @@ public abstract class Lists
// Guardrails about collection size are only checked for the added elements without considering
// already existent elements. This is done so to avoid read-before-write, having additional checks
// during SSTable write.
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
int dataSize = 0;
for (ByteBuffer buffer : elements)
@ -522,13 +522,13 @@ public abstract class Lists
Cell<?> cell = params.addCell(column, CellPath.create(uuid), buffer);
dataSize += cell.dataSize();
}
Guardrails.collectionSize.guard(dataSize, column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(dataSize, column.name.toString(), false, params.clientState);
}
else
{
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
Cell<?> cell = params.addCell(column, value.get(ProtocolVersion.CURRENT));
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), false, params.clientState);
}
}
}

View File

@ -441,7 +441,7 @@ public abstract class Maps
// Guardrails about collection size are only checked for the added elements without considering
// already existent elements. This is done so to avoid read-before-write, having additional checks
// during SSTable write.
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
int dataSize = 0;
for (Map.Entry<ByteBuffer, ByteBuffer> entry : elements.entrySet())
@ -449,13 +449,13 @@ public abstract class Maps
Cell<?> cell = params.addCell(column, CellPath.create(entry.getKey()), entry.getValue());
dataSize += cell.dataSize();
}
Guardrails.collectionSize.guard(dataSize, column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(dataSize, column.name.toString(), false, params.clientState);
}
else
{
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
Cell<?> cell = params.addCell(column, value.get(ProtocolVersion.CURRENT));
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), false, params.clientState);
}
}
}

View File

@ -368,7 +368,7 @@ public abstract class Sets
// Guardrails about collection size are only checked for the added elements without considering
// already existent elements. This is done so to avoid read-before-write, having additional checks
// during SSTable write.
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
int dataSize = 0;
for (ByteBuffer bb : elements)
@ -379,13 +379,13 @@ public abstract class Sets
Cell<?> cell = params.addCell(column, CellPath.create(bb), ByteBufferUtil.EMPTY_BYTE_BUFFER);
dataSize += cell.dataSize();
}
Guardrails.collectionSize.guard(dataSize, column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(dataSize, column.name.toString(), false, params.clientState);
}
else
{
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), params.clientState);
Guardrails.itemsPerCollection.guard(elements.size(), column.name.toString(), false, params.clientState);
Cell<?> cell = params.addCell(column, value.get(ProtocolVersion.CURRENT));
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), params.clientState);
Guardrails.collectionSize.guard(cell.dataSize(), column.name.toString(), false, params.clientState);
}
}
}

View File

@ -111,7 +111,7 @@ final class ClusteringColumnRestrictions extends RestrictionSetWrapper
r.appendTo(builder, options);
if (hasIN() && Guardrails.inSelectCartesianProduct.enabled(state))
Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "clustering key", state);
Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "clustering key", false, state);
if (builder.hasMissingElements())
break;

View File

@ -88,7 +88,7 @@ final class PartitionKeySingleRestrictionSet extends RestrictionSetWrapper imple
r.appendTo(builder, options);
if (hasIN() && Guardrails.inSelectCartesianProduct.enabled(state))
Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "partition key", state);
Guardrails.inSelectCartesianProduct.guard(builder.buildSize(), "partition key", false, state);
if (builder.hasMissingElements())
break;

View File

@ -394,7 +394,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
int userLimit,
long queryStartNanoTime) throws RequestValidationException, RequestExecutionException
{
Guardrails.pageSize.guard(pageSize, table(), state.getClientState());
Guardrails.pageSize.guard(pageSize, table(), false, state.getClientState());
if (aggregationSpec != null)
{
@ -579,7 +579,7 @@ public class SelectStatement implements CQLStatement.SingleKeyspaceCqlStatement
if (restrictions.keyIsInRelation())
{
Guardrails.partitionKeysInSelect.guard(keys.size(), table.name, state);
Guardrails.partitionKeysInSelect.guard(keys.size(), table.name, false, state);
}
ClusteringIndexFilter filter = makeClusteringIndexFilter(options, state, columnFilter);

View File

@ -187,7 +187,7 @@ public abstract class AlterTableStatement extends AlterSchemaStatement
Views.Builder viewsBuilder = keyspace.views.unbuild();
newColumns.forEach(c -> addColumn(keyspace, table, c, tableBuilder, viewsBuilder));
Guardrails.columnsPerTable.guard(tableBuilder.numColumns(), tableName, state);
Guardrails.columnsPerTable.guard(tableBuilder.numColumns(), tableName, false, state);
TableMetadata tableMetadata = tableBuilder.build();
tableMetadata.validate();

View File

@ -117,6 +117,7 @@ public final class CreateIndexStatement extends AlterSchemaStatement
Strings.isNullOrEmpty(indexName)
? String.format("on table %s", table.name)
: String.format("%s on table %s", indexName, table.name),
false,
state);
List<IndexTarget> indexTargets = Lists.newArrayList(transform(rawIndexTargets, t -> t.prepare(table)));

View File

@ -117,7 +117,7 @@ public final class CreateKeyspaceStatement extends AlterSchemaStatement
super.validate(state);
// Guardrail on number of keyspaces
Guardrails.keyspaces.guard(Schema.instance.getUserKeyspaces().size() + 1, keyspaceName, state);
Guardrails.keyspaces.guard(Schema.instance.getUserKeyspaces().size() + 1, keyspaceName, false, state);
}
@Override

View File

@ -133,7 +133,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
Guardrails.tableProperties.guard(attrs.updatedProperties(), attrs::removeProperty, state);
// Guardrail on columns per table
Guardrails.columnsPerTable.guard(rawColumns.size(), tableName, state);
Guardrails.columnsPerTable.guard(rawColumns.size(), tableName, false, state);
// Guardrail on number of tables
if (Guardrails.tables.enabled(state))
@ -144,7 +144,7 @@ public final class CreateTableStatement extends AlterSchemaStatement
.map(Keyspace::open)
.mapToInt(keyspace -> keyspace.getColumnFamilyStores().size())
.sum();
Guardrails.tables.guard(totalUserTables + 1, tableName, state);
Guardrails.tables.guard(totalUserTables + 1, tableName, false, state);
}
}
}

View File

@ -157,6 +157,7 @@ public final class CreateViewStatement extends AlterSchemaStatement
Iterable<ViewMetadata> tableViews = keyspace.views.forTable(table.id);
Guardrails.materializedViewsPerTable.guard(Iterables.size(tableViews) + 1,
String.format("%s on table %s", viewName, table.name),
false,
state);
if (table.params.gcGraceSeconds == 0)

View File

@ -44,6 +44,7 @@ public abstract class Guardrail
{
protected static final NoSpamLogger logger = NoSpamLogger.getLogger(LoggerFactory.getLogger(Guardrail.class),
10, TimeUnit.MINUTES);
protected static final String REDACTED = "<redacted>";
/** A name identifying the guardrail (mainly for shipping with diagnostic events). */
public final String name;
@ -68,6 +69,11 @@ public abstract class Guardrail
}
protected void warn(String message)
{
warn(message, message);
}
protected void warn(String message, String redactedMessage)
{
message = decorateMessage(message);
@ -77,9 +83,15 @@ public abstract class Guardrail
ClientWarn.instance.warn(message);
// Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
Tracing.trace(message);
GuardrailsDiagnostics.warned(name, decorateMessage(redactedMessage));
}
protected void fail(String message, @Nullable ClientState state)
{
fail(message, message, state);
}
protected void fail(String message, String redactedMessage, @Nullable ClientState state)
{
message = decorateMessage(message);
@ -89,6 +101,7 @@ public abstract class Guardrail
ClientWarn.instance.warn(message);
// Similarly, tracing will also ignore the message if we're not running tracing on the current thread.
Tracing.trace(message);
GuardrailsDiagnostics.failed(name, decorateMessage(redactedMessage));
if (state != null)
throw new GuardrailViolatedException(message);

View File

@ -0,0 +1,73 @@
/*
* 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.io.Serializable;
import java.util.HashMap;
import org.apache.cassandra.diag.DiagnosticEvent;
/**
* {@link DiagnosticEvent} implementation for guardrail activation events.
*/
final class GuardrailEvent extends DiagnosticEvent
{
enum GuardrailEventType
{
WARNED, FAILED
}
/** The type of activation, which is a warning or a failure. */
private final GuardrailEventType type;
/** The name that identifies the activated guardrail. */
private final String name;
/** The warn/fail message emitted by the activated guardrail. */
private final String message;
/**
* Creates new guardrail activation event.
*
* @param type The type of activation, which is warning or a failure.
* @param name The name that identifies the activated guardrail.
* @param message The warn/fail message emitted by the activated guardrail.
*/
GuardrailEvent(GuardrailEventType type, String name, String message)
{
this.type = type;
this.name = name;
this.message = message;
}
@Override
public Enum<GuardrailEventType> getType()
{
return type;
}
@Override
public HashMap<String, Serializable> toMap()
{
HashMap<String, Serializable> ret = new HashMap<>();
ret.put("name", name);
ret.put("message", message);
return ret;
}
}

View File

@ -0,0 +1,63 @@
/*
* 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.apache.cassandra.db.guardrails.GuardrailEvent.GuardrailEventType;
import org.apache.cassandra.diag.DiagnosticEventService;
/**
* Utility methods for {@link GuardrailEvent} activities.
*/
final class GuardrailsDiagnostics
{
private static final DiagnosticEventService service = DiagnosticEventService.instance();
private GuardrailsDiagnostics()
{
}
/**
* Creates a new diagnostic event for the activation of the soft/warn limit of a guardrail.
*
* @param name The name that identifies the activated guardrail.
* @param message The warning message emitted by the activated guardrail.
*/
static void warned(String name, String message)
{
if (isEnabled(GuardrailEventType.WARNED))
service.publish(new GuardrailEvent(GuardrailEventType.WARNED, name, message));
}
/**
* Creates a new diagnostic event for the activation of the hard/fail limit of a guardrail.
*
* @param name The name that identifies the activated guardrail.
* @param message The failure message emitted by the activated guardrail.
*/
static void failed(String name, String message)
{
if (isEnabled(GuardrailEventType.FAILED))
service.publish(new GuardrailEvent(GuardrailEventType.FAILED, name, message));
}
private static boolean isEnabled(GuardrailEventType type)
{
return service.isEnabled(GuardrailEvent.class, type);
}
}

View File

@ -64,6 +64,11 @@ public class Threshold extends Guardrail
thresholdValue);
}
private String redactedErrMsg(boolean isWarning, long value, long thresholdValue)
{
return errMsg(isWarning, REDACTED, value, thresholdValue);
}
private long failValue(ClientState state)
{
long failValue = failThreshold.applyAsLong(state);
@ -106,17 +111,16 @@ public class Threshold extends Guardrail
/**
* Apply the guardrail to the provided value, warning or failing 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, although it won't
* throw any exception if the failure threshold is exceeded. This is so because checks without an
* associated client come from asynchronous processes such as compaction, and we don't want to
* interrupt such processes.
* @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 containsUserData whether the {@code what} contains user data that should be redacted on external systems.
* @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)
public void guard(long value, String what, boolean containsUserData, @Nullable ClientState state)
{
if (!enabled(state))
return;
@ -124,23 +128,25 @@ public class Threshold extends Guardrail
long failValue = failValue(state);
if (value > failValue)
{
triggerFail(value, failValue, what, state);
triggerFail(value, failValue, what, containsUserData, state);
return;
}
long warnValue = warnValue(state);
if (value > warnValue)
triggerWarn(value, warnValue, what);
triggerWarn(value, warnValue, what, containsUserData);
}
private void triggerFail(long value, long failValue, String what, ClientState state)
private void triggerFail(long value, long failValue, String what, boolean containsUserData, ClientState state)
{
fail(errMsg(false, what, value, failValue), state);
String fullMessage = errMsg(false, what, value, failValue);
fail(fullMessage, containsUserData ? redactedErrMsg(false, value, failValue) : fullMessage, state);
}
private void triggerWarn(long value, long warnValue, String what)
private void triggerWarn(long value, long warnValue, String what, boolean containsUserData)
{
warn(errMsg(true, what, value, warnValue));
String fullMessage = errMsg(true, what, value, warnValue);
warn(fullMessage, containsUserData ? redactedErrMsg(true, value, warnValue) : fullMessage);
}
/**

View File

@ -430,8 +430,8 @@ public abstract class SSTableWriter extends SSTable implements Transactional
column.name.toString(),
keyString,
metadata);
Guardrails.collectionSize.guard(cellsSize, msg, null);
Guardrails.itemsPerCollection.guard(cellsCount, msg, null);
Guardrails.collectionSize.guard(cellsSize, msg, true, null);
Guardrails.itemsPerCollection.guard(cellsCount, msg, true, null);
}
}
}

View File

@ -25,6 +25,7 @@ import java.util.Collections;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Test;
import org.apache.cassandra.db.marshal.BytesType;
@ -55,6 +56,14 @@ public class GuardrailCollectionSizeTest extends ThresholdTester
Guardrails::getCollectionSizeFailThresholdInKiB);
}
@After
public void after()
{
// immediately drop the created table so its async cleanup doesn't interfere with the next tests
if (currentTable() != null)
dropTable("DROP TABLE %s");
}
@Test
public void testSetSize() throws Throwable
{
@ -206,6 +215,14 @@ public class GuardrailCollectionSizeTest extends ThresholdTester
assertWarns("UPDATE %s SET v = v + ? WHERE k = 6", map(allocate(FAIL_THRESHOLD / 4 + 1), allocate(FAIL_THRESHOLD / 4)));
}
@Override
protected String createTable(String query)
{
String table = super.createTable(query);
disableCompaction();
return table;
}
private void assertValid(String query, ByteBuffer... values) throws Throwable
{
assertValid(execute(query, values));

View File

@ -19,6 +19,7 @@
package org.apache.cassandra.db.guardrails;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
@ -167,9 +168,9 @@ public class GuardrailInSelectCartesianProductTest extends ThresholdTester
else if (keys > WARN_THRESHOLD)
{
if (clusterings > FAIL_THRESHOLD)
assertFails(function, keysWarnMessage, clusteringsFailMessage);
assertFails(function, Arrays.asList(keysWarnMessage, clusteringsFailMessage));
else if (clusterings > WARN_THRESHOLD)
assertWarns(function, keysWarnMessage, clusteringsWarnMessage);
assertWarns(function, Arrays.asList(keysWarnMessage, clusteringsWarnMessage));
else
assertWarns(function, keysWarnMessage);
}

View File

@ -24,6 +24,7 @@ import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.junit.After;
import org.junit.Test;
import org.apache.cassandra.db.marshal.Int32Type;
@ -52,6 +53,14 @@ public class GuardrailItemsPerCollectionTest extends ThresholdTester
Guardrails::getItemsPerCollectionFailThreshold);
}
@After
public void after()
{
// immediately drop the created table so its async cleanup doesn't interfere with the next tests
if (currentTable() != null)
dropTable("DROP TABLE %s");
}
@Test
public void testSetSize() throws Throwable
{
@ -208,6 +217,14 @@ public class GuardrailItemsPerCollectionTest extends ThresholdTester
assertWarns("UPDATE %s SET v = v + ? WHERE k = 4", map(1, FAIL_THRESHOLD + 1), FAIL_THRESHOLD);
}
@Override
protected String createTable(String query)
{
String table = super.createTable(query);
disableCompaction();
return table;
}
private void assertValid(String query, ByteBuffer collection) throws Throwable
{
assertValid(execute(query, collection));

View File

@ -49,6 +49,11 @@ public class GuardrailTablePropertiesTest extends GuardrailTester
private static final String IGNORED_PROPERTY_NAME = "table_properties_ignored";
private static final String DISALLOWED_PROPERTY_NAME = "table_properties_disallowed";
public GuardrailTablePropertiesTest()
{
super(Guardrails.tableProperties);
}
@Before
public void before()
{

View File

@ -18,11 +18,15 @@
package org.apache.cassandra.db.guardrails;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
@ -31,18 +35,22 @@ import java.util.stream.Collectors;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.apache.cassandra.auth.AuthenticatedUser;
import org.apache.cassandra.auth.CassandraRoleManager;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.cql3.CQLStatement;
import org.apache.cassandra.cql3.CQLTester;
import org.apache.cassandra.cql3.QueryOptions;
import org.apache.cassandra.cql3.QueryProcessor;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.guardrails.GuardrailEvent.GuardrailEventType;
import org.apache.cassandra.db.view.View;
import org.apache.cassandra.diag.DiagnosticEventService;
import org.apache.cassandra.index.sasi.SASIIndex;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.ClientWarn;
@ -55,6 +63,7 @@ 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.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@ -70,12 +79,13 @@ public abstract class GuardrailTester extends CQLTester
protected static ClientState systemClientState, userClientState, superClientState;
/**
* The tested guardrail, if we are testing a specific one.
*/
/** The tested guardrail, if we are testing a specific one. */
@Nullable
protected final Guardrail guardrail;
/** A listener for emitted diagnostic events. */
protected final Listener listener;
public GuardrailTester()
{
this(null);
@ -84,6 +94,7 @@ public abstract class GuardrailTester extends CQLTester
public GuardrailTester(@Nullable Guardrail guardrail)
{
this.guardrail = guardrail;
this.listener = new Listener();
}
@BeforeClass
@ -93,6 +104,7 @@ public abstract class GuardrailTester extends CQLTester
requireAuthentication();
requireNetwork();
guardrails().setEnabled(true);
DatabaseDescriptor.setDiagnosticEventsEnabled(true);
systemClientState = ClientState.forInternalCalls();
userClientState = ClientState.forExternalCalls(InetSocketAddress.createUnresolved("127.0.0.1", 123));
@ -115,6 +127,14 @@ public abstract class GuardrailTester extends CQLTester
execute(userClientState, useKeyspaceQuery);
execute(systemClientState, useKeyspaceQuery);
execute(superClientState, useKeyspaceQuery);
DiagnosticEventService.instance().subscribe(GuardrailEvent.class, listener);
}
@After
public void afterGuardrailTest() throws Throwable
{
DiagnosticEventService.instance().unsubscribe(listener);
}
static Guardrails guardrails()
@ -176,6 +196,8 @@ public abstract class GuardrailTester extends CQLTester
{
function.apply();
assertEmptyWarnings();
listener.assertNotWarned();
listener.assertNotFailed();
}
catch (GuardrailViolatedException e)
{
@ -184,6 +206,7 @@ public abstract class GuardrailTester extends CQLTester
finally
{
ClientWarn.instance.resetWarnings();
listener.clear();
}
}
@ -192,7 +215,42 @@ public abstract class GuardrailTester extends CQLTester
assertValid(() -> execute(userClientState, query));
}
protected void assertWarns(CheckedFunction function, String... messages) throws Throwable
protected void assertWarns(String query, String message) throws Throwable
{
assertWarns(query, message, message);
}
protected void assertWarns(String query, String message, String redactedMessage) throws Throwable
{
assertWarns(query, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertWarns(String query, List<String> messages) throws Throwable
{
assertWarns(() -> execute(userClientState, query), messages, messages);
}
protected void assertWarns(String query, List<String> messages, List<String> redactedMessages) throws Throwable
{
assertWarns(() -> execute(userClientState, query), messages, redactedMessages);
}
protected void assertWarns(CheckedFunction function, String message) throws Throwable
{
assertWarns(function, message, message);
}
protected void assertWarns(CheckedFunction function, String message, String redactedMessage) throws Throwable
{
assertWarns(function, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertWarns(CheckedFunction function, List<String> messages) throws Throwable
{
assertWarns(function, messages, messages);
}
protected void assertWarns(CheckedFunction function, List<String> messages, List<String> redactedMessages) 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 ...
@ -201,24 +259,67 @@ public abstract class GuardrailTester extends CQLTester
{
function.apply();
assertWarnings(messages);
listener.assertWarned(redactedMessages);
listener.assertNotFailed();
}
finally
{
ClientWarn.instance.resetWarnings();
listener.clear();
}
}
protected void assertWarns(String query, String... messages) throws Throwable
protected void assertFails(String query, String message) throws Throwable
{
assertWarns(() -> execute(userClientState, query), messages);
assertFails(query, message, message);
}
protected void assertFails(CheckedFunction function, String... messages) throws Throwable
protected void assertFails(String query, String message, String redactedMessage) throws Throwable
{
assertFails(function, true, messages);
assertFails(query, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertFails(CheckedFunction function, boolean thrown, String... messages) throws Throwable
protected void assertFails(String query, List<String> messages) throws Throwable
{
assertFails(query, messages, messages);
}
protected void assertFails(String query, List<String> messages, List<String> redactedMessages) throws Throwable
{
assertFails(() -> execute(userClientState, query), messages, redactedMessages);
}
protected void assertFails(CheckedFunction function, String message) throws Throwable
{
assertFails(function, message, message);
}
protected void assertFails(CheckedFunction function, String message, String redactedMessage) throws Throwable
{
assertFails(function, true, message, redactedMessage);
}
protected void assertFails(CheckedFunction function, boolean thrown, String message) throws Throwable
{
assertFails(function, thrown, message, message);
}
protected void assertFails(CheckedFunction function, boolean thrown, String message, String redactedMessage) throws Throwable
{
assertFails(function, thrown, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertFails(CheckedFunction function, List<String> messages) throws Throwable
{
assertFails(function, messages, messages);
}
protected void assertFails(CheckedFunction function, List<String> messages, List<String> redactedMessages) throws Throwable
{
assertFails(function, true, messages, redactedMessages);
}
protected void assertFails(CheckedFunction function, boolean thrown, List<String> messages, List<String> redactedMessages) throws Throwable
{
ClientWarn.instance.captureWarnings();
try
@ -233,7 +334,7 @@ public abstract class GuardrailTester extends CQLTester
assertTrue("Expect no exception thrown", thrown);
// the last message is the one raising the guardrail failure, the previous messages are warnings
String failMessage = messages[messages.length - 1];
String failMessage = messages.get(messages.size() - 1);
if (guardrail != null)
{
@ -246,16 +347,22 @@ public abstract class GuardrailTester extends CQLTester
e.getMessage().contains(failMessage));
assertWarnings(messages);
if (messages.size() > 1)
listener.assertWarned(redactedMessages.subList(0, messages.size() - 1));
else
listener.assertNotWarned();
listener.assertFailed(redactedMessages.get(messages.size() - 1));
}
finally
{
ClientWarn.instance.resetWarnings();
listener.clear();
}
}
protected void assertFails(String query, String... messages) throws Throwable
{
assertFails(() -> execute(userClientState, query), messages);
assertFails(() -> execute(userClientState, query), Arrays.asList(messages));
}
protected void assertThrows(CheckedFunction function, Class<? extends Throwable> exception, String message)
@ -275,20 +382,19 @@ public abstract class GuardrailTester extends CQLTester
}
}
private void assertWarnings(String... messages)
private void assertWarnings(List<String> messages)
{
List<String> warnings = getWarnings();
assertFalse("Expected to warn, but no warning was received", warnings == null || warnings.isEmpty());
assertEquals(format("Expected %d warnings but got %d: %s", messages.length, warnings.size(), warnings),
messages.length,
assertEquals(format("Expected %d warnings but got %d: %s", messages.size(), warnings.size(), warnings),
messages.size(),
warnings.size());
for (int i = 0; i < messages.length; i++)
for (int i = 0; i < messages.size(); i++)
{
String message = messages.get(i);
String warning = warnings.get(i);
String message = messages[i];
if (guardrail != null)
{
String prefix = guardrail.decorateMessage("");
@ -383,4 +489,84 @@ public abstract class GuardrailTester extends CQLTester
{
return String.join(",", (new TreeSet<>(ImmutableSet.copyOf((csv.split(","))))));
}
/**
* A listener for guardrails diagnostic events.
*/
public class Listener implements Consumer<GuardrailEvent>
{
private final List<String> warnings = new CopyOnWriteArrayList<>();
private final List<String> failures = new CopyOnWriteArrayList<>();
@Override
public void accept(GuardrailEvent event)
{
assertNotNull(event);
Map<String, Serializable> map = event.toMap();
if (guardrail != null)
assertEquals(guardrail.name, map.get("name"));
GuardrailEventType type = (GuardrailEventType) event.getType();
String message = map.toString();
switch (type)
{
case WARNED:
warnings.add(message);
break;
case FAILED:
failures.add(message);
break;
default:
fail("Unexpected diagnostic event:" + type);
}
}
public void clear()
{
warnings.clear();
failures.clear();
}
public void assertNotWarned()
{
assertTrue(format("Expect no warning diagnostic events but got %s", warnings), warnings.isEmpty());
}
public void assertWarned(List<String> messages)
{
assertFalse("Expected to emit warning diagnostic event, but no warning was emitted", warnings.isEmpty());
assertEquals(format("Expected %d warning diagnostic events but got %d: %s)", messages.size(), warnings.size(), warnings),
messages.size(), warnings.size());
for (int i = 0; i < messages.size(); i++)
{
String message = messages.get(i);
String warning = warnings.get(i);
assertTrue(format("Warning diagnostic event '%s' does not contain expected message '%s'", warning, message),
warning.contains(message));
}
}
public void assertNotFailed()
{
assertTrue(format("Expect no failure diagnostic events but got %s", failures), failures.isEmpty());
}
public void assertFailed(String... messages)
{
assertFalse("Expected to emit failure diagnostic event, but no failure was emitted", failures.isEmpty());
assertEquals(format("Expected %d failure diagnostic events but got %d: %s)", messages.length, failures.size(), failures),
messages.length, failures.size());
for (int i = 0; i < messages.length; i++)
{
String message = messages[i];
String failure = failures.get(i);
assertTrue(format("Failure diagnostic event '%s' does not contain expected message '%s'", failure, message),
failure.contains(message));
}
}
}
}

View File

@ -42,12 +42,19 @@ public class GuardrailsConfigProviderTest extends GuardrailTester
(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");
assertFails(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
assertFails(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
assertValid(() -> guard.guard(5, "Z", userClientState));
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertWarns(() -> guard.guard(25, "A", false, userClientState), "Warning: for A, 25 > 10");
assertWarns(() -> guard.guard(100, "B", false, userClientState), "Warning: for B, 100 > 10");
assertFails(() -> guard.guard(101, "X", false, userClientState), "Aborting: for X, 101 > 100");
assertFails(() -> guard.guard(200, "Y", false, userClientState), "Aborting: for Y, 200 > 100");
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertValid(() -> guard.guard(5, "Z", true, userClientState));
assertWarns(() -> guard.guard(25, "A", true, userClientState), "Warning: for A, 25 > 10", "Warning: for <redacted>, 25 > 10");
assertWarns(() -> guard.guard(100, "B", true, userClientState), "Warning: for B, 100 > 10", "Warning: for <redacted>, 100 > 10");
assertFails(() -> guard.guard(101, "X", true, userClientState), "Aborting: for X, 101 > 100", "Aborting: for <redacted>, 101 > 100");
assertFails(() -> guard.guard(200, "Y", true, userClientState), "Aborting: for Y, 200 > 100", "Aborting: for <redacted>, 200 > 100");
assertValid(() -> guard.guard(5, "Z", true, userClientState));
Assertions.assertThatThrownBy(() -> GuardrailsConfigProvider.build("unexistent_class"))
.isInstanceOf(ConfigurationException.class)

View File

@ -50,17 +50,20 @@ public class GuardrailsTest extends GuardrailTester
{
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));
for (boolean containsUserData : Arrays.asList(true, false))
{
assertValid(() -> guard.guard(5, "Z", containsUserData, null));
assertValid(() -> guard.guard(25, "A", containsUserData, userClientState));
assertValid(() -> guard.guard(100, "B", containsUserData, userClientState));
assertValid(() -> guard.guard(101, "X", containsUserData, userClientState));
assertValid(() -> guard.guard(200, "Y", containsUserData, userClientState));
}
}
@Test
public void testThreshold() throws Throwable
{
Threshold guard = new Threshold("x",
Threshold guard = new Threshold("x",
state -> 10,
state -> 100,
(isWarn, what, v, t) -> format("%s: for %s, %s > %s",
@ -68,12 +71,19 @@ public class GuardrailsTest extends GuardrailTester
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");
assertFails(() -> guard.guard(101, "X", userClientState), "Aborting: for X, 101 > 100");
assertFails(() -> guard.guard(200, "Y", userClientState), "Aborting: for Y, 200 > 100");
assertValid(() -> guard.guard(5, "Z", userClientState));
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertWarns(() -> guard.guard(25, "A", false, userClientState), "Warning: for A, 25 > 10");
assertWarns(() -> guard.guard(100, "B", false, userClientState), "Warning: for B, 100 > 10");
assertFails(() -> guard.guard(101, "X", false, userClientState), "Aborting: for X, 101 > 100");
assertFails(() -> guard.guard(200, "Y", false, userClientState), "Aborting: for Y, 200 > 100");
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertValid(() -> guard.guard(5, "Z", true, userClientState));
assertWarns(() -> guard.guard(25, "A", true, userClientState), "Warning: for A, 25 > 10", "Warning: for <redacted>, 25 > 10");
assertWarns(() -> guard.guard(100, "B", true, userClientState), "Warning: for B, 100 > 10", "Warning: for <redacted>, 100 > 10");
assertFails(() -> guard.guard(101, "X", true, userClientState), "Aborting: for X, 101 > 100", "Aborting: for <redacted>, 101 > 100");
assertFails(() -> guard.guard(200, "Y", true, userClientState), "Aborting: for Y, 200 > 100", "Aborting: for <redacted>, 200 > 100");
assertValid(() -> guard.guard(5, "Z", true, userClientState));
}
@Test
@ -87,8 +97,11 @@ public class GuardrailsTest extends GuardrailTester
assertTrue(guard.enabled(userClientState));
assertValid(() -> guard.guard(5, "Z", userClientState));
assertWarns(() -> guard.guard(11, "A", userClientState), "Warning: for A, 11 > 10");
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertWarns(() -> guard.guard(11, "A", false, userClientState), "Warning: for A, 11 > 10");
assertValid(() -> guard.guard(5, "Z", true, userClientState));
assertWarns(() -> guard.guard(11, "A", true, userClientState), "Warning: for A, 11 > 10", "Warning: for <redacted>, 11 > 10");
}
@Test
@ -102,8 +115,11 @@ public class GuardrailsTest extends GuardrailTester
assertTrue(guard.enabled(userClientState));
assertValid(() -> guard.guard(5, "Z", userClientState));
assertFails(() -> guard.guard(11, "A", userClientState), "Aborting: for A, 11 > 10");
assertValid(() -> guard.guard(5, "Z", false, userClientState));
assertFails(() -> guard.guard(11, "A", false, userClientState), "Aborting: for A, 11 > 10");
assertValid(() -> guard.guard(5, "Z", true, userClientState));
assertFails(() -> guard.guard(11, "A", true, userClientState), "Aborting: for A, 11 > 10", "Aborting: for <redacted>, 11 > 10");
}
@Test
@ -116,23 +132,23 @@ public class GuardrailsTest extends GuardrailTester
isWarn ? "Warning" : "Failure", 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));
assertValid(() -> guard.guard(5, "x", false, null));
assertValid(() -> guard.guard(5, "x", false, userClientState));
assertValid(() -> guard.guard(5, "x", false, systemClientState));
assertValid(() -> guard.guard(5, "x", false, 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));
assertWarns(() -> guard.guard(100, "y", false, null), "Warning: for y, 100 > 10");
assertWarns(() -> guard.guard(100, "y", false, userClientState), "Warning: for y, 100 > 10");
assertValid(() -> guard.guard(100, "y", false, systemClientState));
assertValid(() -> guard.guard(100, "y", false, superClientState));
// value over fail threshold. An undefined user means that the check comes from a background process,
// so we warn instead of failing to prevent interrupting that process.
assertWarns(() -> guard.guard(101, "z", null), "Failure: for z, 101 > 100");
assertFails(() -> guard.guard(101, "z", userClientState), "Failure: for z, 101 > 100");
assertValid(() -> guard.guard(101, "z", systemClientState));
assertValid(() -> guard.guard(101, "z", superClientState));
// value over fail threshold. An undefined user means that the check comes from a background process, so we
// still emit failure messages and events, but we don't throw an exception to prevent interrupting that process.
assertFails(() -> guard.guard(101, "z", false, null), false, "Failure: for z, 101 > 100");
assertFails(() -> guard.guard(101, "z", false, userClientState), "Failure: for z, 101 > 100");
assertValid(() -> guard.guard(101, "z", false, systemClientState));
assertValid(() -> guard.guard(101, "z", false, superClientState));
}
@Test
@ -164,7 +180,7 @@ public class GuardrailsTest extends GuardrailTester
public void testValuesWarned() throws Throwable
{
// Using a sorted set below to ensure the order in the warning message checked below is not random
Values<Integer> warned = new Values<>("x",
Values<Integer> warned = new Values<>("x",
state -> insertionOrderedSet(4, 6, 20),
state -> Collections.emptySet(),
state -> Collections.emptySet(),
@ -184,7 +200,7 @@ public class GuardrailsTest extends GuardrailTester
public void testValuesIgnored() throws Throwable
{
// Using a sorted set below to ensure the order in the error message checked below are not random
Values<Integer> ignored = new Values<>("x",
Values<Integer> ignored = new Values<>("x",
state -> Collections.emptySet(),
state -> insertionOrderedSet(4, 6, 20),
state -> Collections.emptySet(),
@ -218,7 +234,7 @@ public class GuardrailsTest extends GuardrailTester
public void testValuesDisallowed() throws Throwable
{
// Using a sorted set below to ensure the order in the error message checked below are not random
Values<Integer> disallowed = new Values<>("x",
Values<Integer> disallowed = new Values<>("x",
state -> Collections.emptySet(),
state -> Collections.emptySet(),
state -> insertionOrderedSet(4, 6, 20),
@ -234,16 +250,16 @@ public class GuardrailsTest extends GuardrailTester
assertValid(() -> disallowed.guard(set(200), action, userClientState));
assertValid(() -> disallowed.guard(set(1, 2, 3), action, userClientState));
assertWarns(() -> disallowed.guard(set(4, 6), action, null),
assertFails(() -> disallowed.guard(set(4, 6), action, null), false,
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
assertWarns(() -> disallowed.guard(set(4, 5, 6, 7), action, null),
assertFails(() -> disallowed.guard(set(4, 5, 6, 7), action, null), false,
"Provided values [4, 6] are not allowed for integer (disallowed values are: [4, 6, 20])");
}
@Test
public void testValuesUsers() throws Throwable
{
Values<Integer> disallowed = new Values<>("x",
Values<Integer> disallowed = new Values<>("x",
state -> Collections.singleton(2),
state -> Collections.singleton(3),
state -> Collections.singleton(4),
@ -271,7 +287,7 @@ public class GuardrailsTest extends GuardrailTester
Assert.assertEquals(list(3, 3), triggeredOn);
message = "Provided values [4] are not allowed for integer (disallowed values are: [4])";
assertWarns(() -> disallowed.guard(set(4), action, null), message);
assertFails(() -> disallowed.guard(set(4), action, null), false, message);
assertFails(() -> disallowed.guard(set(4), action, userClientState), message);
assertValid(() -> disallowed.guard(set(4), action, systemClientState));
assertValid(() -> disallowed.guard(set(4), action, superClientState));

View File

@ -18,6 +18,8 @@
package org.apache.cassandra.db.guardrails;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
@ -125,18 +127,48 @@ public abstract class ThresholdTester extends GuardrailTester
.isLessThanOrEqualTo(failGetter.applyAsLong(guardrails()));
}
protected void assertThresholdWarns(String query, String... messages) throws Throwable
protected void assertThresholdWarns(String query, String message) throws Throwable
{
assertWarns(query, messages);
assertThresholdWarns(query, message, message);
}
protected void assertThresholdWarns(String query, String message, String redactedMessage) throws Throwable
{
assertThresholdWarns(query, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertThresholdWarns(String query, List<String> messages) throws Throwable
{
assertThresholdWarns(query, messages, messages);
}
protected void assertThresholdWarns(String query, List<String> messages, List<String> redactedMessages) throws Throwable
{
assertWarns(query, messages, redactedMessages);
Assertions.assertThat(currentValue())
.isGreaterThan(warnGetter.applyAsLong(guardrails()))
.isLessThanOrEqualTo(failGetter.applyAsLong(guardrails()));
}
protected void assertThresholdFails(String query, String... messages) throws Throwable
protected void assertThresholdFails(String query, String message) throws Throwable
{
assertFails(query, messages);
assertThresholdFails(query, message, message);
}
protected void assertThresholdFails(String query, String message, String redactedMessage) throws Throwable
{
assertThresholdFails(query, Collections.singletonList(message), Collections.singletonList(redactedMessage));
}
protected void assertThresholdFails(String query, List<String> messages) throws Throwable
{
assertThresholdFails(query, messages, messages);
}
protected void assertThresholdFails(String query, List<String> messages, List<String> redactedMessages) throws Throwable
{
assertFails(query, messages, redactedMessages);
Assertions.assertThat(currentValue())
.isGreaterThanOrEqualTo(warnGetter.applyAsLong(guardrails()))