Add Timestamp Bound Guardrail (bound user supplied timestamps within a certain range)

Patch by Jordan West; Reviewed by Andrés de la Peña and Brandon Williams for CASSANDRA-18352
This commit is contained in:
Jordan West 2023-03-23 15:39:20 -07:00
parent 9ba27f398b
commit 2ff1ad4788
16 changed files with 529 additions and 15 deletions

View File

@ -1,4 +1,5 @@
5.0
* Add guardrail to bound timestamps (CASSANDRA-18352)
* Add keyspace_name column to system_views.clients (CASSANDRA-18525)
* Moved system properties and envs to CassandraRelevantProperties and CassandraRelevantEnv respectively (CASSANDRA-17797)
* Add sstablepartitions offline tool to find large partitions in sstables (CASSANDRA-8720)

View File

@ -1767,6 +1767,13 @@ drop_compact_storage_enabled: false
# Guardrail to allow/disallow user-provided timestamps. Defaults to true.
# user_timestamps_enabled: true
#
# Guardrail to bound user-provided timestamps within a given range. Default is infinite (denoted by null).
# Accepted values are durations of the form 12h, 24h, etc.
# maximum_timestamp_warn_threshold:
# maximum_timestamp_fail_threshold:
# minimum_timestamp_warn_threshold:
# minimum_timestamp_fail_threshold:
#
# Guardrail to allow/disallow GROUP BY functionality.
# group_by_enabled: true
#

View File

@ -904,6 +904,12 @@ public class Config
public volatile DurationSpec.LongNanosecondsBound repair_state_expires = new DurationSpec.LongNanosecondsBound("3d");
public volatile int repair_state_size = 100_000;
/** The configuration of timestamp bounds */
public volatile DurationSpec.LongMicrosecondsBound maximum_timestamp_warn_threshold = null;
public volatile DurationSpec.LongMicrosecondsBound maximum_timestamp_fail_threshold = null;
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_warn_threshold = null;
public volatile DurationSpec.LongMicrosecondsBound minimum_timestamp_fail_threshold = null;
/**
* The variants of paxos implementation and semantics supported by Cassandra.
*/

View File

@ -272,6 +272,64 @@ public abstract class DurationSpec
}
}
/**
* Represents a duration used for Cassandra configuration. The bound is [0, Long.MAX_VALUE) in microseconds.
* If the user sets a different unit - we still validate that converted to microseconds the quantity will not exceed
* that upper bound. (CASSANDRA-17571)
*/
public final static class LongMicrosecondsBound extends DurationSpec
{
/**
* Creates a {@code DurationSpec.LongMicrosecondsBound} of the specified amount.
* The bound is [0, Long.MAX_VALUE) in microseconds.
*
* @param value the duration
*/
public LongMicrosecondsBound(String value)
{
super(value, MICROSECONDS, Long.MAX_VALUE);
}
/**
* Creates a {@code DurationSpec.LongMicrosecondsBound} of the specified amount in the specified unit.
* The bound is [0, Long.MAX_VALUE) in milliseconds.
*
* @param quantity where quantity shouldn't be bigger than Long.MAX_VALUE - 1 in microseconds
* @param unit in which the provided quantity is
*/
public LongMicrosecondsBound(long quantity, TimeUnit unit)
{
super(quantity, unit, MICROSECONDS, Long.MAX_VALUE);
}
/**
* Creates a {@code DurationSpec.LongMicrosecondsBound} of the specified amount in microseconds.
* The bound is [0, Long.MAX_VALUE) in microseconds.
*
* @param microseconds where milliseconds shouldn't be bigger than Long.MAX_VALUE-1
*/
public LongMicrosecondsBound(long microseconds)
{
this(microseconds, MICROSECONDS);
}
/**
* @return this duration in number of milliseconds
*/
public long toMicroseconds()
{
return unit().toMicros(quantity());
}
/**
* @return this duration in number of seconds
*/
public long toSeconds()
{
return unit().toSeconds(quantity());
}
}
/**
* Represents a duration used for Cassandra configuration. The bound is [0, Long.MAX_VALUE) in milliseconds.
* If the user sets a different unit - we still validate that converted to milliseconds the quantity will not exceed

View File

@ -84,6 +84,8 @@ public class GuardrailsOptions implements GuardrailsConfig
validateDataDiskUsageMaxDiskSize(config.data_disk_usage_max_disk_size);
validateMinRFThreshold(config.minimum_replication_factor_warn_threshold, config.minimum_replication_factor_fail_threshold);
validateMaxRFThreshold(config.maximum_replication_factor_warn_threshold, config.maximum_replication_factor_fail_threshold);
validateTimestampThreshold(config.maximum_timestamp_warn_threshold, config.maximum_timestamp_fail_threshold, "maximum_timestamp");
validateTimestampThreshold(config.minimum_timestamp_warn_threshold, config.minimum_timestamp_fail_threshold, "minimum_timestamp");
}
@Override
@ -760,6 +762,64 @@ public class GuardrailsOptions implements GuardrailsConfig
x -> config.zero_ttl_on_twcs_enabled = x);
}
@Override
public DurationSpec.LongMicrosecondsBound getMaximumTimestampWarnThreshold()
{
return config.maximum_timestamp_warn_threshold;
}
@Override
public DurationSpec.LongMicrosecondsBound getMaximumTimestampFailThreshold()
{
return config.maximum_timestamp_fail_threshold;
}
@Override
public void setMaximumTimestampThreshold(@Nullable DurationSpec.LongMicrosecondsBound warn,
@Nullable DurationSpec.LongMicrosecondsBound fail)
{
validateTimestampThreshold(warn, fail, "maximum_timestamp");
updatePropertyWithLogging("maximum_timestamp_warn_threshold",
warn,
() -> config.maximum_timestamp_warn_threshold,
x -> config.maximum_timestamp_warn_threshold = x);
updatePropertyWithLogging("maximum_timestamp_fail_threshold",
fail,
() -> config.maximum_timestamp_fail_threshold,
x -> config.maximum_timestamp_fail_threshold = x);
}
@Override
public DurationSpec.LongMicrosecondsBound getMinimumTimestampWarnThreshold()
{
return config.minimum_timestamp_warn_threshold;
}
@Override
public DurationSpec.LongMicrosecondsBound getMinimumTimestampFailThreshold()
{
return config.minimum_timestamp_fail_threshold;
}
@Override
public void setMinimumTimestampThreshold(@Nullable DurationSpec.LongMicrosecondsBound warn,
@Nullable DurationSpec.LongMicrosecondsBound fail)
{
validateTimestampThreshold(warn, fail, "minimum_timestamp");
updatePropertyWithLogging("minimum_timestamp_warn_threshold",
warn,
() -> config.minimum_timestamp_warn_threshold,
x -> config.minimum_timestamp_warn_threshold = x);
updatePropertyWithLogging("minimum_timestamp_fail_threshold",
fail,
() -> config.minimum_timestamp_fail_threshold,
x -> config.minimum_timestamp_fail_threshold = x);
}
private static <T> void updatePropertyWithLogging(String propertyName, T newValue, Supplier<T> getter, Consumer<T> setter)
{
T oldValue = getter.get();
@ -771,6 +831,11 @@ public class GuardrailsOptions implements GuardrailsConfig
}
private static void validatePositiveNumeric(long value, long maxValue, String name)
{
validatePositiveNumeric(value, maxValue, name, false);
}
private static void validatePositiveNumeric(long value, long maxValue, String name, boolean allowZero)
{
if (value == -1)
return;
@ -779,12 +844,12 @@ public class GuardrailsOptions implements GuardrailsConfig
throw new IllegalArgumentException(format("Invalid value %d for %s: maximum allowed value is %d",
value, name, maxValue));
if (value == 0)
if (!allowZero && value == 0)
throw new IllegalArgumentException(format("Invalid value for %s: 0 is not allowed; " +
"if attempting to disable use -1", name));
// We allow -1 as a general "disabling" flag. But reject anything lower to avoid mistakes.
if (value <= 0)
if (value < 0)
throw new IllegalArgumentException(format("Invalid value %d for %s: negative values are not allowed, " +
"outside of -1 which disables the guardrail", value, name));
}
@ -808,6 +873,13 @@ public class GuardrailsOptions implements GuardrailsConfig
validateWarnLowerThanFail(warn, fail, name);
}
private static void validateMaxLongThreshold(long warn, long fail, String name, boolean allowZero)
{
validatePositiveNumeric(warn, Long.MAX_VALUE, name + "_warn_threshold", allowZero);
validatePositiveNumeric(fail, Long.MAX_VALUE, name + "_fail_threshold", allowZero);
validateWarnLowerThanFail(warn, fail, name);
}
private static void validateMinIntThreshold(int warn, int fail, String name)
{
validatePositiveNumeric(warn, Integer.MAX_VALUE, name + "_warn_threshold");
@ -835,6 +907,18 @@ public class GuardrailsOptions implements GuardrailsConfig
fail, DatabaseDescriptor.getDefaultKeyspaceRF()));
}
public static void validateTimestampThreshold(DurationSpec.LongMicrosecondsBound warn,
DurationSpec.LongMicrosecondsBound fail,
String name)
{
// this function is used for both upper and lower thresholds because lower threshold is relative
// despite using MinThreshold we still want the warn threshold to be less than or equal to
// the fail threshold.
validateMaxLongThreshold(warn == null ? -1 : warn.toMicroseconds(),
fail == null ? -1 : fail.toMicroseconds(),
name, true);
}
private static void validateWarnLowerThanFail(long warn, long fail, String name)
{
if (warn == -1 || fail == -1)

View File

@ -302,6 +302,16 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
}
}
public void validateTimestamp(QueryState queryState, QueryOptions options)
{
if (!isTimestampSet())
return;
long ts = attrs.getTimestamp(options.getTimestamp(queryState), options);
Guardrails.maximumAllowableTimestamp.guard(ts, table(), false, queryState.getClientState());
Guardrails.minimumAllowableTimestamp.guard(ts, table(), false, queryState.getClientState());
}
public RegularAndStaticColumns updatedColumns()
{
return updatedColumns;
@ -506,6 +516,7 @@ public abstract class ModificationStatement implements CQLStatement.SingleKeyspa
cl.validateForWrite();
validateDiskUsage(options, queryState.getClientState());
validateTimestamp(queryState, options);
List<? extends IMutation> mutations =
getMutations(queryState.getClientState(),

View File

@ -31,10 +31,12 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.cassandra.config.CassandraRelevantProperties;
import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.config.DatabaseDescriptor;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.config.GuardrailsOptions;
import org.apache.cassandra.db.ConsistencyLevel;
import org.apache.cassandra.db.compaction.TimeWindowCompactionStrategy;
import org.apache.cassandra.locator.InetAddressAndPort;
import org.apache.cassandra.service.ClientState;
import org.apache.cassandra.service.disk.usage.DiskUsageBroadcaster;
import org.apache.cassandra.utils.MBeanWrapper;
@ -421,6 +423,24 @@ public final class Guardrails implements GuardrailsMBean
format("The keyspace %s has a replication factor of %s, above the %s threshold of %s.",
what, value, isWarning ? "warning" : "failure", threshold));
public static final MaxThreshold maximumAllowableTimestamp =
new MaxThreshold("maximum_timestamp",
"Timestamps too far in the future can lead to data that can't be easily overwritten",
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampWarnThreshold()),
state -> maximumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMaximumTimestampFailThreshold()),
(isWarning, what, value, threshold) ->
format("The modification to table %s has a timestamp %s after the maximum allowable %s threshold %s",
what, value, isWarning ? "warning" : "failure", threshold));
public static final MinThreshold minimumAllowableTimestamp =
new MinThreshold("minimum_timestamp",
"Timestamps too far in the past can cause writes can be unexpectedly lost",
state -> minimumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMinimumTimestampWarnThreshold()),
state -> minimumTimestampAsRelativeMicros(CONFIG_PROVIDER.getOrCreate(state).getMinimumTimestampFailThreshold()),
(isWarning, what, value, threshold) ->
format("The modification to table %s has a timestamp %s before the minimum allowable %s threshold %s",
what, value, isWarning ? "warning" : "failure", threshold));
private Guardrails()
{
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
@ -1052,6 +1072,42 @@ public final class Guardrails implements GuardrailsMBean
DEFAULT_CONFIG.setZeroTTLOnTWCSWarned(value);
}
@Override
public String getMaximumTimestampWarnThreshold()
{
return durationToString(DEFAULT_CONFIG.getMaximumTimestampWarnThreshold());
}
@Override
public String getMaximumTimestampFailThreshold()
{
return durationToString(DEFAULT_CONFIG.getMaximumTimestampFailThreshold());
}
@Override
public void setMaximumTimestampThreshold(String warnSeconds, String failSeconds)
{
DEFAULT_CONFIG.setMaximumTimestampThreshold(durationFromString(warnSeconds), durationFromString(failSeconds));
}
@Override
public String getMinimumTimestampWarnThreshold()
{
return durationToString(DEFAULT_CONFIG.getMinimumTimestampWarnThreshold());
}
@Override
public String getMinimumTimestampFailThreshold()
{
return durationToString(DEFAULT_CONFIG.getMinimumTimestampFailThreshold());
}
@Override
public void setMinimumTimestampThreshold(String warnSeconds, String failSeconds)
{
DEFAULT_CONFIG.setMinimumTimestampThreshold(durationFromString(warnSeconds), durationFromString(failSeconds));
}
private static String toCSV(Set<String> values)
{
return values == null || values.isEmpty() ? "" : String.join(",", values);
@ -1100,4 +1156,28 @@ public final class Guardrails implements GuardrailsMBean
{
return StringUtils.isEmpty(size) ? null : new DataStorageSpec.LongBytesBound(size);
}
private static String durationToString(@Nullable DurationSpec duration)
{
return duration == null ? null : duration.toString();
}
private static DurationSpec.LongMicrosecondsBound durationFromString(@Nullable String duration)
{
return StringUtils.isEmpty(duration) ? null : new DurationSpec.LongMicrosecondsBound(duration);
}
private static long maximumTimestampAsRelativeMicros(@Nullable DurationSpec.LongMicrosecondsBound duration)
{
return duration == null
? Long.MAX_VALUE
: (ClientState.getLastTimestampMicros() + duration.toMicroseconds());
}
private static long minimumTimestampAsRelativeMicros(@Nullable DurationSpec.LongMicrosecondsBound duration)
{
return duration == null
? Long.MIN_VALUE
: (ClientState.getLastTimestampMicros() - duration.toMicroseconds());
}
}

View File

@ -23,6 +23,7 @@ import java.util.Set;
import javax.annotation.Nullable;
import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.db.ConsistencyLevel;
/**
@ -350,4 +351,50 @@ public interface GuardrailsConfig
* @param value {@code true} if 0 default TTL on TWCS tables is allowed, {@code false} otherwise.
*/
void setZeroTTLOnTWCSEnabled(boolean value);
/**
* @return A timestamp that if a user supplied timestamp is after will trigger a warning
*/
@Nullable
DurationSpec.LongMicrosecondsBound getMaximumTimestampWarnThreshold();
/**
* @return A timestamp that if a user supplied timestamp is after will cause a failure
*/
@Nullable
DurationSpec.LongMicrosecondsBound getMaximumTimestampFailThreshold();
/**
* Sets the warning upper bound for user supplied timestamps
*
* @param warn The highest acceptable difference between now and the written value timestamp before triggering a
* warning. {@code null} means disabled.
* @param fail The highest acceptable difference between now and the written value timestamp before triggering a
* failure. {@code null} means disabled.
*/
void setMaximumTimestampThreshold(@Nullable DurationSpec.LongMicrosecondsBound warn,
@Nullable DurationSpec.LongMicrosecondsBound fail);
/**
* @return A timestamp that if a user supplied timestamp is before will trigger a warning
*/
@Nullable
DurationSpec.LongMicrosecondsBound getMinimumTimestampWarnThreshold();
/**
* @return A timestamp that if a user supplied timestamp is after will trigger a warning
*/
@Nullable
DurationSpec.LongMicrosecondsBound getMinimumTimestampFailThreshold();
/**
* Sets the warning lower bound for user supplied timestamps
*
* @param warn The lowest acceptable difference between now and the written value timestamp before triggering a
* warning. {@code null} means disabled.
* @param fail The lowest acceptable difference between now and the written value timestamp before triggering a
* failure. {@code null} means disabled.
*/
void setMinimumTimestampThreshold(@Nullable DurationSpec.LongMicrosecondsBound warn,
@Nullable DurationSpec.LongMicrosecondsBound fail);
}

View File

@ -651,4 +651,60 @@ public interface GuardrailsMBean
* @param value {@code true} if 0 default TTL on TWCS tables is allowed, {@code false} otherwise.
*/
void setZeroTTLOnTWCSEnabled(boolean value);
/**
* @return The highest acceptable difference between now and the written value timestamp before triggering a warning.
* Expressed as a string formatted as in, for example, {@code 10s} {@code 20m}, {@code 30h} or {@code 40d}.
* A {@code null} value means disabled.
*/
@Nullable
String getMaximumTimestampWarnThreshold();
/**
* @return The highest acceptable difference between now and the written value timestamp before triggering a failure.
* Expressed as a string formatted as in, for example, {@code 10s} {@code 20m}, {@code 30h} or {@code 40d}.
* A {@code null} value means disabled.
*/
@Nullable
String getMaximumTimestampFailThreshold();
/**
* Sets the warning upper bound for user supplied timestamps.
*
* @param warnDuration The highest acceptable difference between now and the written value timestamp before
* triggering a warning. Expressed as a string formatted as in, for example, {@code 10s},
* {@code 20m}, {@code 30h} or {@code 40d}. A {@code null} value means disabled.
* @param failDuration The highest acceptable difference between now and the written value timestamp before
* triggering a failure. Expressed as a string formatted as in, for example, {@code 10s},
* {@code 20m}, {@code 30h} or {@code 40d}. A {@code null} value means disabled.
*/
void setMaximumTimestampThreshold(@Nullable String warnDuration, @Nullable String failDuration);
/**
* @return The lowest acceptable difference between now and the written value timestamp before triggering a warning.
* Expressed as a string formatted as in, for example, {@code 10s} {@code 20m}, {@code 30h} or {@code 40d}.
* A {@code null} value means disabled.
*/
@Nullable
String getMinimumTimestampWarnThreshold();
/**
* @return The lowest acceptable difference between now and the written value timestamp before triggering a failure.
* Expressed as a string formatted as in, for example, {@code 10s} {@code 20m}, {@code 30h} or {@code 40d}.
* A {@code null} value means disabled.
*/
@Nullable
String getMinimumTimestampFailThreshold();
/**
* Sets the warning lower bound for user supplied timestamps.
*
* @param warnDuration The lowest acceptable difference between now and the written value timestamp before
* triggering a warning. Expressed as a string formatted as in, for example, {@code 10s},
* {@code 20m}, {@code 30h} or {@code 40d}. A {@code null} value means disabled.
* @param failDuration The lowest acceptable difference between now and the written value timestamp before
* triggering a failure. Expressed as a string formatted as in, for example, {@code 10s},
* {@code 20m}, {@code 30h} or {@code 40d}. A {@code null} value means disabled.
*/
void setMinimumTimestampThreshold(@Nullable String warnDuration, @Nullable String failDuration);
}

View File

@ -120,6 +120,7 @@ public class DatabaseDescriptorRefTest
"org.apache.cassandra.config.DurationSpec$IntMinutesBound",
"org.apache.cassandra.config.DurationSpec$IntSecondsBound",
"org.apache.cassandra.config.DurationSpec$LongMillisecondsBound",
"org.apache.cassandra.config.DurationSpec$LongMicrosecondsBound",
"org.apache.cassandra.config.DurationSpec$LongNanosecondsBound",
"org.apache.cassandra.config.DurationSpec$LongSecondsBound",
"org.apache.cassandra.config.EncryptionOptions",

View File

@ -213,6 +213,7 @@ public class DurationSpecTest
assertEquals(new DurationSpec.IntSecondsBound("10s"), DurationSpec.IntSecondsBound.inSecondsString("10"));
assertEquals(new DurationSpec.IntSecondsBound("10s"), DurationSpec.IntSecondsBound.inSecondsString("10s"));
assertEquals(10L, new DurationSpec.LongMicrosecondsBound("10us").toMicroseconds());
assertEquals(10L, new DurationSpec.LongMillisecondsBound("10ms").toMilliseconds());
assertEquals(10L, new DurationSpec.LongSecondsBound("10s").toSeconds());
}
@ -284,6 +285,11 @@ public class DurationSpecTest
assertThatThrownBy(() -> new DurationSpec.IntMinutesBound("-10s")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid duration: -10s");
assertThatThrownBy(() -> new DurationSpec.LongMicrosecondsBound("10ns")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid duration: 10ns Accepted units");
assertThatThrownBy(() -> new DurationSpec.LongMicrosecondsBound(10, NANOSECONDS)).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid duration: 10 NANOSECONDS Accepted units");
assertThatThrownBy(() -> new DurationSpec.LongMillisecondsBound("10ns")).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Invalid duration: 10ns Accepted units");
assertThatThrownBy(() -> new DurationSpec.LongMillisecondsBound(10, NANOSECONDS)).isInstanceOf(IllegalArgumentException.class)

View File

@ -28,12 +28,14 @@ import com.google.common.collect.ImmutableSet;
import org.junit.After;
import org.junit.Test;
import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.db.marshal.BytesType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
import org.apache.cassandra.db.marshal.SetType;
import static java.nio.ByteBuffer.allocate;
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.BYTES;
/**
* Tests the guardrail for the size of collections, {@link Guardrails#collectionSize}.
@ -53,7 +55,9 @@ public class GuardrailCollectionSizeTest extends ThresholdTester
Guardrails.collectionSize,
Guardrails::setCollectionSizeThreshold,
Guardrails::getCollectionSizeWarnThreshold,
Guardrails::getCollectionSizeFailThreshold);
Guardrails::getCollectionSizeFailThreshold,
bytes -> new DataStorageSpec.LongBytesBound(bytes, BYTES).toString(),
size -> new DataStorageSpec.LongBytesBound(size).toBytes());
}
@After

View File

@ -28,6 +28,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Test;
import org.apache.cassandra.config.DataStorageSpec;
import org.apache.cassandra.db.marshal.BytesType;
import org.apache.cassandra.db.marshal.ListType;
import org.apache.cassandra.db.marshal.MapType;
@ -35,6 +36,7 @@ import org.apache.cassandra.db.marshal.SetType;
import static java.lang.String.format;
import static java.nio.ByteBuffer.allocate;
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.BYTES;
/**
* Tests the guardrail for the size of column values, {@link Guardrails#columnValueSize}.
@ -51,7 +53,9 @@ public class GuardrailColumnValueSizeTest extends ThresholdTester
Guardrails.columnValueSize,
Guardrails::setColumnValueSizeThreshold,
Guardrails::getColumnValueSizeWarnThreshold,
Guardrails::getColumnValueSizeFailThreshold);
Guardrails::getColumnValueSizeFailThreshold,
bytes -> new DataStorageSpec.LongBytesBound(bytes, BYTES).toString(),
size -> new DataStorageSpec.LongBytesBound(size).toBytes());
}
@Test

View File

@ -0,0 +1,74 @@
/*
* 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 org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.service.ClientState;
public class GuardrailMaximumTimestampTest extends ThresholdTester
{
public GuardrailMaximumTimestampTest()
{
super(TimeUnit.DAYS.toSeconds(1) + "s",
TimeUnit.DAYS.toSeconds(2) + "s",
Guardrails.maximumAllowableTimestamp,
Guardrails::setMaximumTimestampThreshold,
Guardrails::getMaximumTimestampWarnThreshold,
Guardrails::getMaximumTimestampFailThreshold,
micros -> new DurationSpec.LongMicrosecondsBound(micros, TimeUnit.MICROSECONDS).toString(),
micros -> new DurationSpec.LongMicrosecondsBound(micros).toMicroseconds());
}
@Before
public void setupTest()
{
createTable("CREATE TABLE IF NOT EXISTS %s (k int primary key, v int)");
}
@Test
public void testDisabledAllowsAnyTimestamp() throws Throwable
{
guardrails().setMaximumTimestampThreshold(null, null);
assertValid("INSERT INTO %s (k, v) VALUES (2, 2) USING TIMESTAMP " + (Long.MAX_VALUE - 1));
}
@Test
public void testEnabledFail() throws Throwable
{
assertFails("INSERT INTO %s (k, v) VALUES (2, 2) USING TIMESTAMP " + (Long.MAX_VALUE - 1), "maximum_timestamp violated");
}
@Test
public void testEnabledInRange() throws Throwable
{
assertValid("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP " + ClientState.getTimestamp());
}
@Test
public void testEnabledWarn() throws Throwable
{
assertWarns("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP " + (ClientState.getTimestamp() + (TimeUnit.DAYS.toMicros(1) + 40000)),
"maximum_timestamp violated");
}
}

View File

@ -0,0 +1,74 @@
/*
* 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 org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.config.DurationSpec;
import org.apache.cassandra.service.ClientState;
public class GuardrailMinimumTimestampTest extends ThresholdTester
{
public GuardrailMinimumTimestampTest()
{
super(TimeUnit.DAYS.toSeconds(1) + "s",
TimeUnit.DAYS.toSeconds(2) + "s",
Guardrails.minimumAllowableTimestamp,
Guardrails::setMinimumTimestampThreshold,
Guardrails::getMinimumTimestampWarnThreshold,
Guardrails::getMinimumTimestampFailThreshold,
micros -> new DurationSpec.LongMicrosecondsBound(micros, TimeUnit.MICROSECONDS).toString(),
micros -> new DurationSpec.LongMicrosecondsBound(micros).toMicroseconds());
}
@Before
public void setupTest()
{
createTable("CREATE TABLE IF NOT EXISTS %s (k int primary key, v int)");
}
@Test
public void testDisabled() throws Throwable
{
guardrails().setMinimumTimestampThreshold(null, null);
assertValid("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP 12345");
}
@Test
public void testEnabledFailure() throws Throwable
{
assertFails("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP 12345", "minimum_timestamp violated");
}
@Test
public void testEnabledInRange() throws Throwable
{
assertValid("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP " + ClientState.getTimestamp());
}
@Test
public void testEnabledWarn() throws Throwable
{
assertWarns("INSERT INTO %s (k, v) VALUES (1, 1) USING TIMESTAMP " + (ClientState.getTimestamp() - (TimeUnit.DAYS.toMicros(1) + 40000)),
"minimum_timestamp violated");
}
}

View File

@ -28,11 +28,9 @@ import java.util.function.ToLongFunction;
import org.junit.Before;
import org.junit.Test;
import org.apache.cassandra.config.DataStorageSpec;
import org.assertj.core.api.Assertions;
import static java.lang.String.format;
import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.BYTES;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
@ -89,14 +87,18 @@ public abstract class ThresholdTester extends GuardrailTester
Threshold threshold,
TriConsumer<Guardrails, String, String> setter,
Function<Guardrails, String> warnGetter,
Function<Guardrails, String> failGetter)
Function<Guardrails, String> failGetter,
Function<Long, String> stringFormatter,
ToLongFunction<String> stringParser)
{
super(threshold);
this.warnThreshold = new DataStorageSpec.LongBytesBound(warnThreshold).toBytes();
this.failThreshold = new DataStorageSpec.LongBytesBound(failThreshold).toBytes();
this.setter = (g, w, a) -> setter.accept(g, w == null ? null : new DataStorageSpec.LongBytesBound(w, BYTES).toString(), a == null ? null : new DataStorageSpec.LongBytesBound(a, BYTES).toString());
this.warnGetter = g -> new DataStorageSpec.LongBytesBound(warnGetter.apply(g)).toBytes();
this.failGetter = g -> new DataStorageSpec.LongBytesBound(failGetter.apply(g)).toBytes();
this.warnThreshold = stringParser.applyAsLong(warnThreshold);
this.failThreshold = stringParser.applyAsLong(failThreshold);
this.setter = (g, w, f) -> setter.accept(g,
w == null ? null : stringFormatter.apply(w),
f == null ? null : stringFormatter.apply(f));
this.warnGetter = g -> stringParser.applyAsLong(warnGetter.apply(g));
this.failGetter = g -> stringParser.applyAsLong(failGetter.apply(g));
maxValue = Long.MAX_VALUE - 1;
disabledValue = null;
}
@ -247,10 +249,9 @@ public abstract class ThresholdTester extends GuardrailTester
value, name, disabledValue);
if (expectedMessage == null && value < 0)
expectedMessage = format("Invalid data storage: value must be non-negative");
expectedMessage = "value must be non-negative";
assertEquals(format("Exception message '%s' does not contain '%s'", e.getMessage(), expectedMessage),
expectedMessage, e.getMessage());
Assertions.assertThat(e).hasMessageContaining(expectedMessage);
}
}