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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]