This is an automated email from the ASF dual-hosted git repository. jwest 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 2ff1ad4788 Add Timestamp Bound Guardrail (bound user supplied timestamps within a certain range) 2ff1ad4788 is described below commit 2ff1ad4788a1e29b99f81f75b2966b7951ba8250 Author: Jordan West <jord...@netflix.com> AuthorDate: Thu Mar 23 15:39:20 2023 -0700 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 --- CHANGES.txt | 1 + conf/cassandra.yaml | 7 ++ src/java/org/apache/cassandra/config/Config.java | 6 ++ .../org/apache/cassandra/config/DurationSpec.java | 58 ++++++++++++++ .../apache/cassandra/config/GuardrailsOptions.java | 88 +++++++++++++++++++++- .../cql3/statements/ModificationStatement.java | 11 +++ .../apache/cassandra/db/guardrails/Guardrails.java | 80 ++++++++++++++++++++ .../cassandra/db/guardrails/GuardrailsConfig.java | 47 ++++++++++++ .../cassandra/db/guardrails/GuardrailsMBean.java | 56 ++++++++++++++ .../config/DatabaseDescriptorRefTest.java | 1 + .../apache/cassandra/config/DurationSpecTest.java | 6 ++ .../db/guardrails/GuardrailCollectionSizeTest.java | 6 +- .../guardrails/GuardrailColumnValueSizeTest.java | 6 +- .../guardrails/GuardrailMaximumTimestampTest.java | 74 ++++++++++++++++++ .../guardrails/GuardrailMinimumTimestampTest.java | 74 ++++++++++++++++++ .../cassandra/db/guardrails/ThresholdTester.java | 23 +++--- 16 files changed, 529 insertions(+), 15 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 750a7cb760..c1735755c8 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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) diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 6b15e8c819..85aee95318 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -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 # diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index e07ab98631..0411a862a6 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -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. */ diff --git a/src/java/org/apache/cassandra/config/DurationSpec.java b/src/java/org/apache/cassandra/config/DurationSpec.java index 10d56c23eb..2522d86124 100644 --- a/src/java/org/apache/cassandra/config/DurationSpec.java +++ b/src/java/org/apache/cassandra/config/DurationSpec.java @@ -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 diff --git a/src/java/org/apache/cassandra/config/GuardrailsOptions.java b/src/java/org/apache/cassandra/config/GuardrailsOptions.java index 434c4deb4c..9734cb7cd2 100644 --- a/src/java/org/apache/cassandra/config/GuardrailsOptions.java +++ b/src/java/org/apache/cassandra/config/GuardrailsOptions.java @@ -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) diff --git a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java index 4af8b63f36..0cf677187c 100644 --- a/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java +++ b/src/java/org/apache/cassandra/cql3/statements/ModificationStatement.java @@ -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(), diff --git a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java index a4e0cd9d83..18f9cf345f 100644 --- a/src/java/org/apache/cassandra/db/guardrails/Guardrails.java +++ b/src/java/org/apache/cassandra/db/guardrails/Guardrails.java @@ -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()); + } } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java index 9d990f20be..12d24028e5 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsConfig.java @@ -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); } diff --git a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java index d738aa3dbf..5f66d71d8d 100644 --- a/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java +++ b/src/java/org/apache/cassandra/db/guardrails/GuardrailsMBean.java @@ -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); } diff --git a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java index 38b997ab14..b07bfa384f 100644 --- a/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java +++ b/test/unit/org/apache/cassandra/config/DatabaseDescriptorRefTest.java @@ -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", diff --git a/test/unit/org/apache/cassandra/config/DurationSpecTest.java b/test/unit/org/apache/cassandra/config/DurationSpecTest.java index 22846fc701..b0c73dedf2 100644 --- a/test/unit/org/apache/cassandra/config/DurationSpecTest.java +++ b/test/unit/org/apache/cassandra/config/DurationSpecTest.java @@ -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) diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailCollectionSizeTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailCollectionSizeTest.java index 1483e8101f..b15f85a125 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailCollectionSizeTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailCollectionSizeTest.java @@ -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 diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java index eab1cf749c..f0513daa9c 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailColumnValueSizeTest.java @@ -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 diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumTimestampTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumTimestampTest.java new file mode 100644 index 0000000000..a5c31ad1e7 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMaximumTimestampTest.java @@ -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"); + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumTimestampTest.java b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumTimestampTest.java new file mode 100644 index 0000000000..87169fc5e2 --- /dev/null +++ b/test/unit/org/apache/cassandra/db/guardrails/GuardrailMinimumTimestampTest.java @@ -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"); + } +} diff --git a/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java index fae305ab7d..7f79078733 100644 --- a/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java +++ b/test/unit/org/apache/cassandra/db/guardrails/ThresholdTester.java @@ -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); } } --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org For additional commands, e-mail: commits-h...@cassandra.apache.org