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


Reply via email to