You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cassandra.apache.org by ed...@apache.org on 2022/02/06 03:44:14 UTC

[cassandra] 01/13: Add new custom types and unit tests for configuration patch by Ekaterina Dimitrova; reviewed by Caleb Rackliffe, David Capwell, Michael Semb Wever and Benjamin Lerer for CASSANDRA-15234

This is an automated email from the ASF dual-hosted git repository.

edimitrova pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra.git

commit db9f7a67ec4b03413c10034956e2cf18739ca4b1
Author: Ekaterina Dimitrova <ek...@datastax.com>
AuthorDate: Tue Dec 14 23:00:56 2021 -0500

    Add new custom types and unit  tests for configuration
    patch by Ekaterina Dimitrova; reviewed by Caleb Rackliffe, David Capwell, Michael Semb Wever and Benjamin Lerer for CASSANDRA-15234
---
 CHANGES.txt                                        |   1 +
 NEWS.txt                                           |   6 +
 .../org/apache/cassandra/config/DataRateSpec.java  | 378 ++++++++++++++++++++
 .../apache/cassandra/config/DataStorageSpec.java   | 397 +++++++++++++++++++++
 .../org/apache/cassandra/config/DurationSpec.java  | 342 ++++++++++++++++++
 .../apache/cassandra/config/DataRateSpecTest.java  | 136 +++++++
 .../cassandra/config/DataStorageSpecTest.java      | 141 ++++++++
 .../apache/cassandra/config/DurationSpecTest.java  | 160 +++++++++
 8 files changed, 1561 insertions(+)

diff --git a/CHANGES.txt b/CHANGES.txt
index 4304aa7..66aae18 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,4 +1,5 @@
 4.1
+ * Standardize storage configuration parameters' names. Support unit suffixes. (CASSANDRA-15234)
  * Remove support for Windows (CASSANDRA-16956)
  * Runtime-configurable YAML option to prohibit USE statements (CASSANDRA-17318)
  * When streaming sees a ClosedChannelException this triggers the disk failure policy (CASSANDRA-17116)
diff --git a/NEWS.txt b/NEWS.txt
index ff166df..966a729 100644
--- a/NEWS.txt
+++ b/NEWS.txt
@@ -81,6 +81,12 @@ New features
 
 Upgrading
 ---------
+    - There is a new cassandra.yaml version 2. Units suffixes should be provided for all rates(B/s|MiB/s|KiB/s|MiB/s),
+      memory (KiB|MiB|GiB|B) and duration(d|h|s|ms|us|µs|ns|m)
+      parameters. (CASSANDRA-15234)
+      Backward compatibility with the old cassandra.yaml file will be in place until at least the next major version.
+    - Many cassandra.yaml parameters' names have been changed. Full list can be found on ...... (ADD LINK LATER WHEN PAGE
+      IS CREATED) (CASSANDRA-15234)
     - Before you upgrade, if you are using `cassandra.auth_bcrypt_gensalt_log2_rounds` property,
       confirm it is set to value lower than 31 otherwise Cassandra will fail to start. See CASSANDRA-9384
       for further details. You also need to regenerate passwords for users for who the password
diff --git a/src/java/org/apache/cassandra/config/DataRateSpec.java b/src/java/org/apache/cassandra/config/DataRateSpec.java
new file mode 100644
index 0000000..3512513
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/DataRateSpec.java
@@ -0,0 +1,378 @@
+/*
+ * 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.config;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.google.common.primitives.Ints;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+/**
+ * Represents a data rate type used for cassandra configuration. It supports the opportunity for the users to be able to
+ * add units to the confiuration parameter value. (CASSANDRA-15234)
+ */
+public final class DataRateSpec
+{
+    /**
+     * The Regexp used to parse the rate provided as String in cassandra.yaml.
+     */
+    private static final Pattern BIT_RATE_UNITS_PATTERN = Pattern.compile("^(\\d+)(MiB/s|KiB/s|B/s)$");
+
+    private final double quantity;
+
+    private final DataRateUnit unit;
+
+    public DataRateSpec(String value)
+    {
+        //parse the string field value
+        Matcher matcher = BIT_RATE_UNITS_PATTERN.matcher(value);
+
+        if (!matcher.find())
+            throw new ConfigurationException("Invalid bit rate: " + value + " Accepted units: MiB/s, KiB/s, B/s where " +
+                                             "case matters and " + "only non-negative values are valid");
+
+        quantity = Long.parseLong(matcher.group(1));
+        unit = DataRateUnit.fromSymbol(matcher.group(2));
+    }
+
+    DataRateSpec(double quantity, DataRateUnit unit)
+    {
+        if (quantity < 0)
+            throw new ConfigurationException("Invalid bit rare: value must be non-negative");
+
+        if (quantity > Long.MAX_VALUE)
+            throw new NumberFormatException("Invalid bit rate: value must be between 0 and Long.MAX_VALUE = 9223372036854775807");
+
+        this.quantity = quantity;
+        this.unit = unit;
+    }
+
+    /**
+     * Creates a {@code DataRateSpec} of the specified amount of bits per second.
+     *
+     * @param bytesPerSecond the amount of bytes per second
+     * @return a {@code DataRateSpec}
+     */
+    public static DataRateSpec inBytesPerSecond(long bytesPerSecond)
+    {
+        return new DataRateSpec(bytesPerSecond, DataRateUnit.BYTES_PER_SECOND);
+    }
+
+    /**
+     * Creates a {@code DataRateSpec} of the specified amount of kibibytes per second.
+     *
+     * @param kibibytesPerSecond the amount of kibibytes per second
+     * @return a {@code DataRateSpec}
+     */
+    public static DataRateSpec inKibibytesPerSecond(long kibibytesPerSecond)
+    {
+        return new DataRateSpec(kibibytesPerSecond, DataRateUnit.KIBIBYTES_PER_SECOND);
+    }
+
+    /**
+     * Creates a {@code DataRateSpec} of the specified amount of mebibytes per second.
+     *
+     * @param mebibytesPerSecond the amount of mebibytes per second
+     * @return a {@code DataRateSpec}
+     */
+    public static DataRateSpec inMebibytesPerSecond(long mebibytesPerSecond)
+    {
+        return new DataRateSpec(mebibytesPerSecond, DataRateUnit.MEBIBYTES_PER_SECOND);
+    }
+
+    /**
+     * Creates a {@code DataRateSpec} of the specified amount of mebibytes per second.
+     *
+     * @param megabitsPerSecond the amount of megabits per second
+     * @return a {@code DataRateSpec}
+     */
+    public static DataRateSpec megabitsPerSecondInMebibytesPerSecond(long megabitsPerSecond)
+    {
+        final double MEBIBYTES_PER_MEGABIT = 0.119209289550781;
+        double mebibytesPerSecond = (double)megabitsPerSecond * MEBIBYTES_PER_MEGABIT;
+
+        return new DataRateSpec(mebibytesPerSecond, DataRateUnit.MEBIBYTES_PER_SECOND);
+    }
+
+    /**
+     * @return the data rate unit assigned.
+     */
+    public DataRateUnit getUnit()
+    {
+        return unit;
+    }
+
+    /**
+     * @return the data rate in bytes per second
+     */
+    public double toBytesPerSecond()
+    {
+        return unit.toBytesPerSecond(quantity);
+    }
+
+    /**
+     * Returns the data rate in bytes per second as an {@code int}
+     *
+     * @return the data rate in bytes per second or {@code Integer.MAX_VALUE} if the rate is too large.
+     */
+    public int toBytesPerSecondAsInt()
+    {
+        return Ints.saturatedCast(Math.round(toBytesPerSecond()));
+    }
+
+    /**
+     * @return the data rate in kibibytes per second
+     */
+    public double toKibibytesPerSecond()
+    {
+        return unit.toKibibytesPerSecond(quantity);
+    }
+
+    /**
+     * Returns the data rate in kibibytes per second as an {@code int}
+     *
+     * @return the data rate in kibibytes per second or {@code Integer.MAX_VALUE} if the number of kibibytes is too large.
+     */
+    public int toKibibytesPerSecondAsInt()
+    {
+        return Ints.saturatedCast(Math.round(toKibibytesPerSecond()));
+    }
+
+    /**
+     * @return the data rate in mebibytes per second
+     */
+    public double toMebibytesPerSecond()
+    {
+        return unit.toMebibytesPerSecond(quantity);
+    }
+
+    /**
+     * Returns the data rate in mebibytes per second as an {@code int}
+     *
+     * @return the data rate in mebibytes per second or {@code Integer.MAX_VALUE} if the number of mebibytes is too large.
+     */
+    public int toMebibytesPerSecondAsInt()
+    {
+        return Ints.saturatedCast(Math.round(toMebibytesPerSecond()));
+    }
+
+    /**
+     * This method is required in order to support backward compatibility with the old unit used for a few Data Rate
+     * parameters before CASSANDRA-15234
+     *
+     * @return the data rate in megabits per second.
+     */
+    public double toMegabitsPerSecond()
+    {
+        return unit.toMegabitsPerSecond(quantity);
+    }
+
+    /**
+     * Returns the data rate in megabits per second as an {@code int}. This method is required in order to support
+     * backward compatibility with the old unit used for a few Data Rate parameters before CASSANDRA-15234
+     *
+     * @return the data rate in mebibytes per second or {@code Integer.MAX_VALUE} if the number of mebibytes is too large.
+     */
+    public int toMegabitsPerSecondAsInt()
+    {
+        return Ints.saturatedCast(Math.round(toMegabitsPerSecond()));
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(unit.toKibibytesPerSecond(quantity));
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (this == obj)
+            return true;
+
+        if (!(obj instanceof DataRateSpec))
+            return false;
+
+        DataRateSpec other = (DataRateSpec) obj;
+        if (unit == other.unit)
+            return quantity == other.quantity;
+
+        // Due to overflows we can only guarantee that the 2 data rates are equal if we get the same results
+        // doing the conversion in both directions.
+        return unit.convert(other.quantity, other.unit) == quantity && other.unit.convert(quantity, unit) == other.quantity;
+    }
+
+    @Override
+    public String toString()
+    {
+        return Math.round(quantity) + unit.symbol;
+    }
+
+    public enum DataRateUnit
+    {
+        BYTES_PER_SECOND("B/s")
+        {
+            public double toBytesPerSecond(double d)
+            {
+                return d;
+            }
+
+            public double toKibibytesPerSecond(double d)
+            {
+                return d / 1024.0;
+            }
+
+            public double toMebibytesPerSecond(double d)
+            {
+                return d / (1024.0 * 1024.0);
+            }
+
+            public double toMegabitsPerSecond(double d) { return (d / 125000.0); }
+
+            public double convert(double source, DataRateUnit sourceUnit)
+            {
+                return sourceUnit.toBytesPerSecond(source);
+            }
+        },
+        KIBIBYTES_PER_SECOND("KiB/s")
+        {
+            public double toBytesPerSecond(double d)
+            {
+                return x(d, 1024.0, (MAX / 1024.0));
+            }
+
+            public double toKibibytesPerSecond(double d)
+            {
+                return d;
+            }
+
+            public double toMebibytesPerSecond(double d)
+            {
+                return d / 1024.0;
+            }
+
+            public double toMegabitsPerSecond(double d)
+            {
+                return d / 122.0;
+            }
+
+            public double convert(double source, DataRateUnit sourceUnit)
+            {
+                return sourceUnit.toKibibytesPerSecond(source);
+            }
+        },
+        MEBIBYTES_PER_SECOND("MiB/s")
+        {
+            public double toBytesPerSecond(double d)
+            {
+                return x(d, (1024.0 * 1024.0), (MAX / (1024.0 * 1024.0)));
+            }
+
+            public double toKibibytesPerSecond(double d)
+            {
+                return x(d, 1024.0, (MAX / 1024.0));
+            }
+
+            public double toMebibytesPerSecond(double d)
+            {
+                return d;
+            }
+
+            public double toMegabitsPerSecond(double d)
+            {
+                if (d > MAX / (MEGABITS_PER_MEBIBYTE))
+                    return MAX;
+                return Math.round(d * MEGABITS_PER_MEBIBYTE);
+            }
+
+            public double convert(double source, DataRateUnit sourceUnit)
+            {
+                return sourceUnit.toMebibytesPerSecond(source);
+            }
+        };
+
+        static final double MAX = Long.MAX_VALUE;
+        static final double MEGABITS_PER_MEBIBYTE = 8.388608;
+
+        /**
+         * Scale d by m, checking for overflow. This has a short name to make above code more readable.
+         */
+        static double x(double d, double m, double over)
+        {
+            assert (over > 0.0) && (over < (MAX - 1)) && (over == (MAX / m));
+
+            if (d > over)
+                return MAX;
+            return d * m;
+        }
+
+        /**
+         * @param symbol the unit symbol
+         * @return the rate unit corresponding to the given symbol
+         */
+        public static DataRateUnit fromSymbol(String symbol)
+        {
+            for (DataRateUnit value : values())
+            {
+                if (value.symbol.equalsIgnoreCase(symbol))
+                    return value;
+            }
+            throw new ConfigurationException(String.format("Unsupported data rate unit: %s. Supported units are: %s",
+                                                             symbol, Arrays.stream(values())
+                                                                           .map(u -> u.symbol)
+                                                                           .collect(Collectors.joining(", "))));
+        }
+
+        /**
+         * The unit symbol
+         */
+        private final String symbol;
+
+        DataRateUnit(String symbol)
+        {
+            this.symbol = symbol;
+        }
+
+        public double toBytesPerSecond(double d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public double toKibibytesPerSecond(double d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public double toMebibytesPerSecond(double d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public double toMegabitsPerSecond(double d) { throw new AbstractMethodError(); }
+
+        public double convert(double source, DataRateUnit sourceUnit)
+        {
+            throw new AbstractMethodError();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/config/DataStorageSpec.java b/src/java/org/apache/cassandra/config/DataStorageSpec.java
new file mode 100644
index 0000000..72ed2ba
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/DataStorageSpec.java
@@ -0,0 +1,397 @@
+/*
+ * 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.config;
+
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.google.common.primitives.Ints;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+/**
+ * Represents an amount of data storage. Wrapper class for Cassandra configuration parameters, providing to the
+ * users the opportunity to be able to provide config with a unit of their choice in cassandra.yaml as per the available
+ * options. (CASSANDRA-15234)
+ */
+public final class DataStorageSpec
+{
+    /**
+     * The Regexp used to parse the storage provided as String.
+     */
+    private static final Pattern STORAGE_UNITS_PATTERN = Pattern.compile("^(\\d+)(GiB|MiB|KiB|B)$");
+
+    private final long quantity;
+
+    private final DataStorageUnit unit;
+
+    public DataStorageSpec(String value)
+    {
+        if (value == null || value.equals("null"))
+        {
+            quantity = 0;
+            unit = DataStorageUnit.MEBIBYTES; // the unit doesn't really matter as 0 is 0 in all units
+            return;
+        }
+
+        //parse the string field value
+        Matcher matcher = STORAGE_UNITS_PATTERN.matcher(value);
+
+        if (!matcher.find())
+        {
+            throw new ConfigurationException("Invalid data storage: " + value + " Accepted units: B, KiB, MiB, GiB" +
+                                             " where case matters and only non-negative values are accepted");
+        }
+
+        quantity = Long.parseLong(matcher.group(1));
+        unit = DataStorageUnit.fromSymbol(matcher.group(2));
+    }
+
+    DataStorageSpec(long quantity, DataStorageUnit unit)
+    {
+        if (quantity < 0)
+            throw new ConfigurationException("Invalid data storage: value must be positive, but was " + quantity);
+
+        this.quantity = quantity;
+        this.unit = unit;
+    }
+
+    /**
+     * Creates a {@code DataStorageSpec} of the specified amount of bytes.
+     *
+     * @param bytes the amount of bytes
+     * @return a {@code DataStorageSpec}
+     */
+    public static DataStorageSpec inBytes(long bytes)
+    {
+        return new DataStorageSpec(bytes, DataStorageUnit.BYTES);
+    }
+
+    /**
+     * Creates a {@code DataStorageSpec} of the specified amount of kibibytes.
+     *
+     * @param kibibytes the amount of kibibytes
+     * @return a {@code DataStorageSpec}
+     */
+    public static DataStorageSpec inKibibytes(long kibibytes)
+    {
+        return new DataStorageSpec(kibibytes, DataStorageUnit.KIBIBYTES);
+    }
+
+    /**
+     * Creates a {@code DataStorageSpec} of the specified amount of mebibytes.
+     *
+     * @param mebibytes the amount of mebibytes
+     * @return a {@code DataStorageSpec}
+     */
+    public static DataStorageSpec inMebibytes(long mebibytes)
+    {
+        return new DataStorageSpec(mebibytes, DataStorageUnit.MEBIBYTES);
+    }
+
+    /**
+     * @return the data storage unit.
+     */
+    public DataStorageUnit getUnit()
+    {
+        return unit;
+    }
+
+    /**
+     * @return the amount of data storage in bytes
+     */
+    public long toBytes()
+    {
+        return unit.toBytes(quantity);
+    }
+
+    /**
+     * Returns the amount of data storage in bytes as an {@code int}
+     *
+     * @return the amount of data storage in bytes or {@code Integer.MAX_VALUE} if the number of bytes is too large.
+     */
+    public int toBytesAsInt()
+    {
+        return Ints.saturatedCast(toBytes());
+    }
+
+    /**
+     * @return the amount of data storage in kibibytes
+     */
+    public long toKibibytes()
+    {
+        return unit.toKibibytes(quantity);
+    }
+
+    /**
+     * Returns the amount of data storage in kibibytes as an {@code int}
+     *
+     * @return the amount of data storage in kibibytes or {@code Integer.MAX_VALUE} if the number of kibibytes is too large.
+     */
+    public int toKibibytesAsInt()
+    {
+        return Ints.saturatedCast(toKibibytes());
+    }
+
+    /**
+     * @return the amount of data storage in mebibytes
+     */
+    public long toMebibytes()
+    {
+        return unit.toMebibytes(quantity);
+    }
+
+    /**
+     * Returns the amount of data storage in mebibytes as an {@code int}
+     *
+     * @return the amount of data storage in mebibytes or {@code Integer.MAX_VALUE} if the number of mebibytes is too large.
+     */
+    public int toMebibytesAsInt()
+    {
+        return Ints.saturatedCast(toMebibytes());
+    }
+
+    /**
+     * @return the amount of data storage in gibibytes
+     */
+    public long toGibibytes()
+    {
+        return unit.toGibibytes(quantity);
+    }
+
+    /**
+     * Returns the amount of data storage in gibibytes as an {@code int}
+     *
+     * @return the amount of data storage in gibibytes or {@code Integer.MAX_VALUE} if the number of gibibytes is too large.
+     */
+    public int toGibibytesAsInt()
+    {
+        return Ints.saturatedCast(toGibibytes());
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return Objects.hash(unit.toKibibytes(quantity));
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (this == obj)
+            return true;
+
+        if (!(obj instanceof DataStorageSpec))
+            return false;
+
+        DataStorageSpec other = (DataStorageSpec) obj;
+        if (unit == other.unit)
+            return quantity == other.quantity;
+
+        // Due to overflows we can only guarantee that the 2 storages are equal if we get the same results
+        // doing the convertion in both directions.
+        return unit.convert(other.quantity, other.unit) == quantity && other.unit.convert(quantity, unit) == other.quantity;
+    }
+
+    @Override
+    public String toString()
+    {
+        return quantity + unit.symbol;
+    }
+
+    public enum DataStorageUnit
+    {
+        BYTES("B")
+        {
+            public long toBytes(long d)
+            {
+                return d;
+            }
+
+            public long toKibibytes(long d)
+            {
+                return (d / 1024L);
+            }
+
+            public long toMebibytes(long d)
+            {
+                return (d / (1024L * 1024));
+            }
+
+            public long toGibibytes(long d)
+            {
+                return (d / (1024L * 1024 * 1024));
+            }
+
+            public long convert(long source, DataStorageUnit sourceUnit)
+            {
+                return sourceUnit.toBytes(source);
+            }
+        },
+        KIBIBYTES("KiB")
+        {
+            public long toBytes(long d)
+            {
+                return x(d, 1024L, (MAX / 1024L));
+            }
+
+            public long toKibibytes(long d)
+            {
+                return d;
+            }
+
+            public long toMebibytes(long d)
+            {
+                return (d / 1024L);
+            }
+
+            public long toGibibytes(long d)
+            {
+                return (d / (1024L * 1024));
+            }
+
+            public long convert(long source, DataStorageUnit sourceUnit)
+            {
+                return sourceUnit.toKibibytes(source);
+            }
+        },
+        MEBIBYTES("MiB")
+        {
+            public long toBytes(long d)
+            {
+                return x(d, (1024L * 1024), MAX / (1024L * 1024));
+            }
+
+            public long toKibibytes(long d)
+            {
+                return x(d, 1024L, (MAX / 1024L));
+            }
+
+            public long toMebibytes(long d)
+            {
+                return d;
+            }
+
+            public long toGibibytes(long d)
+            {
+                return (d / 1024L);
+            }
+
+            public long convert(long source, DataStorageUnit sourceUnit)
+            {
+                return sourceUnit.toMebibytes(source);
+            }
+        },
+        GIBIBYTES("GiB")
+        {
+            public long toBytes(long d)
+            {
+                return x(d, (1024L * 1024 * 1024), MAX / (1024L * 1024 * 1024));
+            }
+
+            public long toKibibytes(long d)
+            {
+                return x(d, (1024L * 1024), MAX / (1024L * 1024));
+            }
+
+            public long toMebibytes(long d)
+            {
+                return x(d, 1024L, (MAX / 1024L));
+            }
+
+            public long toGibibytes(long d)
+            {
+                return d;
+            }
+
+            public long convert(long source, DataStorageUnit sourceUnit)
+            {
+                return sourceUnit.toGibibytes(source);
+            }
+        };
+
+        /**
+         * Scale d by m, checking for overflow. This has a short name to make above code more readable.
+         */
+        static long x(long d, long m, long over)
+        {
+            assert (over > 0) && (over < (MAX-1L)) && (over == (MAX / m));
+
+            if (d > over)
+                return Long.MAX_VALUE;
+            return Math.multiplyExact(d, m);
+        }
+
+        /**
+         * @param symbol the unit symbol
+         * @return the memory unit corresponding to the given symbol
+         */
+        public static DataStorageUnit fromSymbol(String symbol)
+        {
+            for (DataStorageUnit value : values())
+            {
+                if (value.symbol.equalsIgnoreCase(symbol))
+                    return value;
+            }
+            throw new ConfigurationException(String.format("Unsupported data storage unit: %s. Supported units are: %s",
+                                                           symbol, Arrays.stream(values())
+                                                                         .map(u -> u.symbol)
+                                                                         .collect(Collectors.joining(", "))));
+        }
+
+        static final long MAX = Long.MAX_VALUE;
+
+        /**
+         * The unit symbol
+         */
+        private final String symbol;
+
+        DataStorageUnit(String symbol)
+        {
+            this.symbol = symbol;
+        }
+
+        public long toBytes(long d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public long toKibibytes(long d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public long toMebibytes(long d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public long toGibibytes(long d)
+        {
+            throw new AbstractMethodError();
+        }
+
+        public long convert(long source, DataStorageUnit sourceUnit)
+        {
+            throw new AbstractMethodError();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/java/org/apache/cassandra/config/DurationSpec.java b/src/java/org/apache/cassandra/config/DurationSpec.java
new file mode 100644
index 0000000..796fde6
--- /dev/null
+++ b/src/java/org/apache/cassandra/config/DurationSpec.java
@@ -0,0 +1,342 @@
+/*
+ * 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.config;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.google.common.primitives.Ints;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+
+/**
+ * Represents a positive time duration. Wrapper class for Cassandra duration configuration parameters, providing to the
+ * users the opportunity to be able to provide config with a unit of their choice in cassandra.yaml as per the available
+ * options. (CASSANDRA-15234)
+ */
+public final class DurationSpec
+{
+    /**
+     * The Regexp used to parse the duration provided as String.
+     */
+    private static final Pattern TIME_UNITS_PATTERN = Pattern.compile(("^(\\d+)(d|h|s|ms|us|µs|ns|m)"));
+    private static final Pattern VALUES_PATTERN = Pattern.compile(("\\d+"));
+
+    public final long quantity;
+
+    private final TimeUnit unit;
+
+    public DurationSpec(String value)
+    {
+        if (value == null || value.equals("null") || value.toLowerCase(Locale.ROOT).equals("nan"))
+        {
+            quantity = 0;
+            unit = TimeUnit.MILLISECONDS;
+            return;
+        }
+
+        //parse the string field value
+        Matcher matcher = TIME_UNITS_PATTERN.matcher(value);
+
+        if(matcher.find())
+        {
+            quantity = Long.parseLong(matcher.group(1));
+            unit = fromSymbol(matcher.group(2));
+        }
+        else
+        {
+            throw new ConfigurationException("Invalid duration: " + value + " Accepted units: d, h, m, s, ms, us, µs," +
+                                             " ns where case matters and " + "only non-negative values");
+        }
+    }
+
+    DurationSpec(long quantity, TimeUnit unit)
+    {
+        if (quantity < 0)
+            throw new ConfigurationException("Invalid duration: value must be positive");
+
+        this.quantity = quantity;
+        this.unit = unit;
+    }
+
+    private DurationSpec(double quantity, TimeUnit unit)
+    {
+        this(Math.round(quantity), unit);
+    }
+
+    /**
+     * Creates a {@code DurationSpec} of the specified amount of milliseconds.
+     *
+     * @param milliseconds the amount of milliseconds
+     * @return a duration
+     */
+    public static DurationSpec inMilliseconds(long milliseconds)
+    {
+        return new DurationSpec(milliseconds, TimeUnit.MILLISECONDS);
+    }
+
+    public static DurationSpec inDoubleMilliseconds(double milliseconds)
+    {
+        return new DurationSpec(milliseconds, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * Creates a {@code DurationSpec} of the specified amount of seconds.
+     *
+     * @param seconds the amount of seconds
+     * @return a duration
+     */
+    public static DurationSpec inSeconds(long seconds)
+    {
+        return new DurationSpec(seconds, TimeUnit.SECONDS);
+    }
+
+    /**
+     * Creates a {@code DurationSpec} of the specified amount of minutes.
+     *
+     * @param minutes the amount of minutes
+     * @return a duration
+     */
+    public static DurationSpec inMinutes(long minutes)
+    {
+        return new DurationSpec(minutes, TimeUnit.MINUTES);
+    }
+
+    /**
+     * Creates a {@code DurationSpec} of the specified amount of hours.
+     *
+     * @param hours the amount of hours
+     * @return a duration
+     */
+    public static DurationSpec inHours(long hours)
+    {
+        return new DurationSpec(hours, TimeUnit.HOURS);
+    }
+
+    /**
+     * Creates a {@code DurationSpec} of the specified amount of seconds. Custom method for special cases.
+     *
+     * @param value which can be in the old form only presenting the quantity or the post CASSANDRA-15234 form - a
+     * value consisting of quantity and unit. This method is necessary for three parameters which didn't change their
+     * names but only their value format. (key_cache_save_period, row_cache_save_period, counter_cache_save_period)
+     * @return a duration
+     */
+    public static DurationSpec inSecondsString(String value)
+    {
+        //parse the string field value
+        Matcher matcher = VALUES_PATTERN.matcher(value);
+
+        long seconds;
+        //if the provided string value is just a number, then we create a Duration Spec value in seconds
+        if (matcher.matches())
+        {
+            seconds = Long.parseLong(value);
+            return new DurationSpec(seconds, TimeUnit.SECONDS);
+        }
+
+        //otherwise we just use the standard constructors
+        return new DurationSpec(value);
+    }
+
+    /**
+     * @param symbol the time unit symbol
+     * @return the time unit associated to the specified symbol
+     */
+    static TimeUnit fromSymbol(String symbol)
+    {
+        switch (symbol.toLowerCase())
+        {
+            case "d": return TimeUnit.DAYS;
+            case "h": return TimeUnit.HOURS;
+            case "m": return TimeUnit.MINUTES;
+            case "s": return TimeUnit.SECONDS;
+            case "ms": return TimeUnit.MILLISECONDS;
+            case "us":
+            case "µs": return TimeUnit.MICROSECONDS;
+            case "ns": return TimeUnit.NANOSECONDS;
+        }
+        throw new ConfigurationException(String.format("Unsupported time unit: %s. Supported units are: %s",
+                                                       symbol, Arrays.stream(TimeUnit.values())
+                                                                     .map(DurationSpec::getSymbol)
+                                                                     .collect(Collectors.joining(", "))));
+    }
+
+    /**
+     * @param targetUnit the time unit
+     * @return this duration in the specified time unit
+     */
+    public long to(TimeUnit targetUnit)
+    {
+        return targetUnit.convert(quantity, unit);
+    }
+
+    /**
+     * @return this duration in number of hours
+     */
+    public long toHours()
+    {
+        return unit.toHours(quantity);
+    }
+
+    /**
+     * Returns this duration in number of minutes as an {@code int}
+     *
+     * @return this duration in number of minutes or {@code Integer.MAX_VALUE} if the number of minutes is too large.
+     */
+    public int toHoursAsInt()
+    {
+        return Ints.saturatedCast(toHours());
+    }
+
+    /**
+     * @return this duration in number of minutes
+     */
+    public long toMinutes()
+    {
+        return unit.toMinutes(quantity);
+    }
+
+    /**
+     * Returns this duration in number of minutes as an {@code int}
+     *
+     * @return this duration in number of minutes or {@code Integer.MAX_VALUE} if the number of minutes is too large.
+     */
+    public int toMinutesAsInt()
+    {
+        return Ints.saturatedCast(toMinutes());
+    }
+
+    /**
+     * @return this duration in number of seconds
+     */
+    public long toSeconds()
+    {
+        return unit.toSeconds(quantity);
+    }
+
+    /**
+     * Returns this duration in number of seconds as an {@code int}
+     *
+     * @return this duration in number of seconds or {@code Integer.MAX_VALUE} if the number of seconds is too large.
+     */
+    public int toSecondsAsInt()
+    {
+        return Ints.saturatedCast(toSeconds());
+    }
+
+    /**
+     * @return this duration in number of nanoseconds
+     */
+    public long toNanoseconds()
+    {
+        return unit.toNanos(quantity);
+    }
+
+    /**
+     * Returns this duration in number of nanoseconds as an {@code int}
+     *
+     * @return this duration in number of nanoseconds or {@code Integer.MAX_VALUE} if the number of nanoseconds is too large.
+     */
+    public int toNanosecondsAsInt()
+    {
+        return Ints.saturatedCast(toNanoseconds());
+    }
+
+    /**
+     * @return this duration in number of milliseconds
+     */
+    public long toMilliseconds()
+    {
+        return unit.toMillis(quantity);
+    }
+
+    /**
+     * @return the duration value in milliseconds
+     */
+    public static long toMilliseconds(DurationSpec quantity)
+    {
+        return quantity.toMilliseconds();
+    }
+
+    /**
+     * Returns this duration in number of milliseconds as an {@code int}
+     *
+     * @return this duration in number of milliseconds or {@code Integer.MAX_VALUE} if the number of milliseconds is too large.
+     */
+    public int toMillisecondsAsInt()
+    {
+        return Ints.saturatedCast(toMilliseconds());
+    }
+
+    @Override
+    public int hashCode()
+    {
+        // Milliseconds seems to be a reasonable tradeoff
+        return Objects.hash(unit.toMillis(quantity));
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+        if (this == obj)
+            return true;
+
+        if (!(obj instanceof DurationSpec))
+            return false;
+
+        DurationSpec other = (DurationSpec) obj;
+        if (unit == other.unit)
+            return quantity == other.quantity;
+
+        // Due to overflows we can only guarantee that the 2 durations are equal if we get the same results
+        // doing the conversion in both directions.
+        return unit.convert(other.quantity, other.unit) == quantity && other.unit.convert(quantity, unit) == other.quantity;
+    }
+
+    @Override
+    public String toString()
+    {
+        return quantity + getSymbol(unit);
+    }
+
+    /**
+     * Returns the symbol associated to the specified unit
+     *
+     * @param unit the time unit
+     * @return the time unit symbol
+     */
+    static String getSymbol(TimeUnit unit)
+    {
+        switch (unit)
+        {
+            case DAYS: return "d";
+            case HOURS: return "h";
+            case MINUTES: return "m";
+            case SECONDS: return "s";
+            case MILLISECONDS: return "ms";
+            case MICROSECONDS: return "us";
+            case NANOSECONDS: return "ns";
+        }
+        throw new AssertionError();
+    }
+}
diff --git a/test/unit/org/apache/cassandra/config/DataRateSpecTest.java b/test/unit/org/apache/cassandra/config/DataRateSpecTest.java
new file mode 100644
index 0000000..73625fb
--- /dev/null
+++ b/test/unit/org/apache/cassandra/config/DataRateSpecTest.java
@@ -0,0 +1,136 @@
+/*
+ * 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.config;
+
+import org.junit.Test;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.quicktheories.core.Gen;
+import org.quicktheories.generators.SourceDSL;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.*;
+import static org.quicktheories.QuickTheory.qt;
+
+public class DataRateSpecTest
+{
+    @Test
+    public void testConversions()
+    {
+        assertEquals(10, new DataRateSpec("10B/s").toBytesPerSecond(), 0);
+        assertEquals(10240, new DataRateSpec("10KiB/s").toBytesPerSecond(), 0);
+        assertEquals(0, new DataRateSpec("10KiB/s").toMebibytesPerSecond(), 0.1);
+        assertEquals(10240, new DataRateSpec("10MiB/s").toKibibytesPerSecond(), 0);
+        assertEquals(10485760, new DataRateSpec("10MiB/s").toBytesPerSecond(), 0);
+        assertEquals(10485760, new DataRateSpec("10MiB/s").toBytesPerSecond(), 0);
+        assertEquals(new DataRateSpec("24MiB/s").toString(), DataRateSpec.megabitsPerSecondInMebibytesPerSecond(200L).toString());
+    }
+
+    @Test
+    public void testOverflowingDuringConversion()
+    {
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807B/s").toBytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807B/s").toBytesPerSecondAsInt(), 0);
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807KiB/s").toBytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807KiB/s").toBytesPerSecondAsInt(), 0);
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toBytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toBytesPerSecondAsInt(), 0);
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toBytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toBytesPerSecondAsInt(), 0);
+
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toMegabitsPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toMegabitsPerSecondAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807KiB/s").toKibibytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807KiB/s").toKibibytesPerSecondAsInt());
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toKibibytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toKibibytesPerSecondAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toMebibytesPerSecond(), 0);
+        assertEquals(Integer.MAX_VALUE, new DataRateSpec("9223372036854775807MiB/s").toMebibytesPerSecondAsInt());
+    }
+
+    @Test
+    public void testFromSymbol()
+    {
+        assertEquals(DataRateSpec.DataRateUnit.fromSymbol("B/s"), DataRateSpec.DataRateUnit.BYTES_PER_SECOND);
+        assertEquals(DataRateSpec.DataRateUnit.fromSymbol("KiB/s"), DataRateSpec.DataRateUnit.KIBIBYTES_PER_SECOND);
+        assertEquals(DataRateSpec.DataRateUnit.fromSymbol("MiB/s"), DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND);
+        assertThatThrownBy(() -> DataRateSpec.DataRateUnit.fromSymbol("n"))
+        .isInstanceOf(ConfigurationException.class)
+        .hasMessageContaining("Unsupported data rate unit: n");
+    }
+
+    @Test
+    public void testInvalidInputs()
+    {
+        assertThatThrownBy(() -> new DataRateSpec("10")).isInstanceOf(ConfigurationException.class)
+                                                        .hasMessageContaining("Invalid bit rate: 10");
+        assertThatThrownBy(() -> new DataRateSpec("-10b/s")).isInstanceOf(ConfigurationException.class)
+                                                            .hasMessageContaining("Invalid bit rate: -10b/s");
+        assertThatThrownBy(() -> new DataRateSpec("10xb/s")).isInstanceOf(ConfigurationException.class)
+                                                            .hasMessageContaining("Invalid bit rate: 10xb/s");
+        assertThatThrownBy(() -> new DataRateSpec("9223372036854775809B/s")
+                                 .toBytesPerSecond()).isInstanceOf(NumberFormatException.class)
+                                                     .hasMessageContaining("For input string: \"9223372036854775809\"");
+    }
+
+    @Test
+    public void testEquals()
+    {
+        assertEquals(new DataRateSpec("10B/s"), new DataRateSpec("10B/s"));
+        assertEquals(new DataRateSpec("10KiB/s"), new DataRateSpec("10240B/s"));
+        assertEquals(new DataRateSpec("10240B/s"), new DataRateSpec("10KiB/s"));
+        assertEquals(DataRateSpec.inMebibytesPerSecond(Long.MAX_VALUE), DataRateSpec.inMebibytesPerSecond(Long.MAX_VALUE));
+        assertNotEquals(DataRateSpec.inMebibytesPerSecond(Long.MAX_VALUE), DataRateSpec.inBytesPerSecond(Long.MAX_VALUE));
+        assertNotEquals(new DataRateSpec("0KiB/s"), new DataRateSpec("10MiB/s"));
+    }
+
+    @Test
+    public void thereAndBack()
+    {
+        Gen<DataRateSpec.DataRateUnit> unitGen = SourceDSL.arbitrary().enumValues(DataRateSpec.DataRateUnit.class);
+        Gen<Long> valueGen = SourceDSL.longs().between(0, Long.MAX_VALUE);
+        qt().forAll(valueGen, unitGen).check((value, unit) -> {
+            DataRateSpec there = new DataRateSpec(value, unit);
+            DataRateSpec back = new DataRateSpec(there.toString());
+            DataRateSpec BACK = new DataRateSpec(there.toString());
+            return there.equals(back) && there.equals(BACK);
+        });
+    }
+
+    @Test
+    public void eq()
+    {
+        qt().forAll(gen(), gen()).check((a, b) -> a.equals(b) == b.equals(a));
+    }
+
+    @Test
+    public void eqAndHash()
+    {
+        qt().forAll(gen(), gen()).check((a, b) -> !a.equals(b) || a.hashCode() == b.hashCode());
+    }
+
+    private static Gen<DataRateSpec> gen()
+    {
+        Gen<DataRateSpec.DataRateUnit> unitGen = SourceDSL.arbitrary().enumValues(DataRateSpec.DataRateUnit.class);
+        Gen<Long> valueGen = SourceDSL.longs().between(0, Long.MAX_VALUE);
+        Gen<DataRateSpec> gen = rs -> new DataRateSpec(valueGen.generate(rs), unitGen.generate(rs));
+        return gen.describedAs(DataRateSpec::toString);
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/config/DataStorageSpecTest.java b/test/unit/org/apache/cassandra/config/DataStorageSpecTest.java
new file mode 100644
index 0000000..66c6444
--- /dev/null
+++ b/test/unit/org/apache/cassandra/config/DataStorageSpecTest.java
@@ -0,0 +1,141 @@
+/*
+ * 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.config;
+
+import java.util.Locale;
+
+import org.junit.Test;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.quicktheories.core.Gen;
+import org.quicktheories.generators.SourceDSL;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.*;
+import static org.quicktheories.QuickTheory.qt;
+
+public class DataStorageSpecTest
+{
+    @Test
+    public void testConversions()
+    {
+        assertEquals(10, new DataStorageSpec("10B").toBytes());
+        assertEquals(10240, new DataStorageSpec("10KiB").toBytes());
+        assertEquals(0, new DataStorageSpec("10KiB").toMebibytes());
+        assertEquals(10240, new DataStorageSpec("10MiB").toKibibytes());
+        assertEquals(10485760, new DataStorageSpec("10MiB").toBytes());
+    }
+
+    @Test
+    public void testOverflowingDuringConversion()
+    {
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807B").toBytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807B").toBytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807KiB").toBytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807KiB").toBytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toBytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toBytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toBytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toBytesAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807KiB").toKibibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807KiB").toKibibytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toKibibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toKibibytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toKibibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toKibibytesAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toMebibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807MiB").toMebibytesAsInt());
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toMebibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toMebibytesAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toGibibytes());
+        assertEquals(Integer.MAX_VALUE, new DataStorageSpec("9223372036854775807GiB").toGibibytesAsInt());
+    }
+
+    @Test
+    public void testFromSymbol()
+    {
+        assertEquals(DataStorageSpec.DataStorageUnit.fromSymbol("B"), DataStorageSpec.DataStorageUnit.BYTES);
+        assertEquals(DataStorageSpec.DataStorageUnit.fromSymbol("KiB"), DataStorageSpec.DataStorageUnit.KIBIBYTES);
+        assertEquals(DataStorageSpec.DataStorageUnit.fromSymbol("MiB"), DataStorageSpec.DataStorageUnit.MEBIBYTES);
+        assertEquals(DataStorageSpec.DataStorageUnit.fromSymbol("GiB"), DataStorageSpec.DataStorageUnit.GIBIBYTES);
+        assertThatThrownBy(() -> DataStorageSpec.DataStorageUnit.fromSymbol("n"))
+        .isInstanceOf(ConfigurationException.class)
+        .hasMessageContaining("Unsupported data storage unit: n");
+    }
+
+    @Test
+    public void testInvalidInputs()
+    {
+        assertThatThrownBy(() -> new DataStorageSpec("10")).isInstanceOf(ConfigurationException.class)
+                                                           .hasMessageContaining("Invalid data storage: 10");
+        assertThatThrownBy(() -> new DataStorageSpec("-10bps")).isInstanceOf(ConfigurationException.class)
+                                                               .hasMessageContaining("Invalid data storage: -10bps");
+        assertThatThrownBy(() -> new DataStorageSpec("-10b")).isInstanceOf(ConfigurationException.class)
+                                                             .hasMessageContaining("Invalid data storage: -10b");
+        assertThatThrownBy(() -> new DataStorageSpec("10HG")).isInstanceOf(ConfigurationException.class)
+                                                             .hasMessageContaining("Invalid data storage: 10HG");
+        assertThatThrownBy(() -> new DataStorageSpec("9223372036854775809B")
+                                 .toBytes()).isInstanceOf(NumberFormatException.class)
+                                            .hasMessageContaining("For input string: \"9223372036854775809\"");
+    }
+
+    @Test
+    public void testEquals()
+    {
+        assertEquals(new DataStorageSpec("10B"), new DataStorageSpec("10B"));
+        assertEquals(new DataStorageSpec("10KiB"), new DataStorageSpec("10240B"));
+        assertEquals(new DataStorageSpec("10240B"), new DataStorageSpec("10KiB"));
+        assertEquals(DataStorageSpec.inMebibytes(Long.MAX_VALUE), DataStorageSpec.inMebibytes(Long.MAX_VALUE));
+        assertNotEquals(DataStorageSpec.inMebibytes(Long.MAX_VALUE), DataStorageSpec.inKibibytes(Long.MAX_VALUE));
+        assertNotEquals(DataStorageSpec.inMebibytes(Long.MAX_VALUE), DataStorageSpec.inBytes(Long.MAX_VALUE));
+        assertNotEquals(new DataStorageSpec("0MiB"), new DataStorageSpec("10KiB"));
+    }
+
+    @Test
+    public void thereAndBack()
+    {
+        qt().forAll(gen()).check(there -> {
+            DataStorageSpec back = new DataStorageSpec(there.toString());
+            DataStorageSpec BACK = new DataStorageSpec(there.toString().toUpperCase(Locale.ROOT).replace("I", "i"));
+            return there.equals(back) && there.equals(BACK);
+        });
+    }
+
+    @Test
+    public void eq()
+    {
+        qt().forAll(gen(), gen()).check((a, b) -> a.equals(b) == b.equals(a));
+    }
+
+    @Test
+    public void eqAndHash()
+    {
+        qt().forAll(gen(), gen()).check((a, b) -> !a.equals(b) || a.hashCode() == b.hashCode());
+    }
+
+    private static Gen<DataStorageSpec> gen()
+    {
+        Gen<DataStorageSpec.DataStorageUnit> unitGen = SourceDSL.arbitrary().enumValues(DataStorageSpec.DataStorageUnit.class);
+        Gen<Long> valueGen = SourceDSL.longs().between(0, Long.MAX_VALUE);
+        Gen<DataStorageSpec> gen = rs -> new DataStorageSpec(valueGen.generate(rs), unitGen.generate(rs));
+        return gen.describedAs(DataStorageSpec::toString);
+    }
+}
\ No newline at end of file
diff --git a/test/unit/org/apache/cassandra/config/DurationSpecTest.java b/test/unit/org/apache/cassandra/config/DurationSpecTest.java
new file mode 100644
index 0000000..84733d8
--- /dev/null
+++ b/test/unit/org/apache/cassandra/config/DurationSpecTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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.config;
+
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.apache.cassandra.exceptions.ConfigurationException;
+import org.quicktheories.core.Gen;
+import org.quicktheories.generators.SourceDSL;
+
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.Assert.*;
+import static org.quicktheories.QuickTheory.qt;
+
+public class DurationSpecTest
+{
+    @Test
+    public void testConversions()
+    {
+        assertEquals(10L, new DurationSpec("10s").toSeconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toSecondsAsInt());
+        assertEquals(10000, new DurationSpec("10s").toMilliseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toMillisecondsAsInt());
+        assertEquals(0, new DurationSpec("10s").toMinutes());
+        assertEquals(10, new DurationSpec("10m").toMinutes());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toMinutesAsInt());
+        assertEquals(600000, new DurationSpec("10m").toMilliseconds());
+        assertEquals(600, new DurationSpec("10m").toSeconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toSecondsAsInt());
+        assertEquals(DurationSpec.inDoubleMilliseconds(0.7), new DurationSpec("1ms"));
+        assertEquals(DurationSpec.inDoubleMilliseconds(0.33), new DurationSpec("0ms"));
+        assertEquals(DurationSpec.inDoubleMilliseconds(0.333), new DurationSpec("0ms"));
+    }
+
+    @Test
+    public void testFromSymbol()
+    {
+        assertEquals(DurationSpec.fromSymbol("ms"), TimeUnit.MILLISECONDS);
+        assertEquals(DurationSpec.fromSymbol("d"), TimeUnit.DAYS);
+        assertEquals(DurationSpec.fromSymbol("h"), TimeUnit.HOURS);
+        assertEquals(DurationSpec.fromSymbol("m"), TimeUnit.MINUTES);
+        assertEquals(DurationSpec.fromSymbol("s"), TimeUnit.SECONDS);
+        assertEquals(DurationSpec.fromSymbol("us"), TimeUnit.MICROSECONDS);
+        assertEquals(DurationSpec.fromSymbol("µs"), TimeUnit.MICROSECONDS);
+        assertEquals(DurationSpec.fromSymbol("ns"), TimeUnit.NANOSECONDS);
+        assertThatThrownBy(() -> DurationSpec.fromSymbol("n")).isInstanceOf(ConfigurationException.class)
+                                                              .hasMessageContaining("Unsupported time unit: n");
+    }
+
+    @Test
+    public void testGetSymbol()
+    {
+        assertEquals(DurationSpec.getSymbol(TimeUnit.MILLISECONDS), "ms");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.DAYS), "d");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.HOURS), "h");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.MINUTES), "m");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.SECONDS), "s");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.MICROSECONDS), "us");
+        assertEquals(DurationSpec.getSymbol(TimeUnit.NANOSECONDS), "ns");
+    }
+
+    @Test
+    public void testInvalidInputs()
+    {
+        assertThatThrownBy(() -> new DurationSpec("10")).isInstanceOf(ConfigurationException.class)
+                                                        .hasMessageContaining("Invalid duration: 10");
+        assertThatThrownBy(() -> new DurationSpec("-10s")).isInstanceOf(ConfigurationException.class)
+                                                          .hasMessageContaining("Invalid duration: -10s");
+        assertThatThrownBy(() -> new DurationSpec("10xd")).isInstanceOf(ConfigurationException.class)
+                                                          .hasMessageContaining("Invalid duration: 10xd");
+        assertThatThrownBy(() -> new DurationSpec("0.333555555ms")).isInstanceOf(ConfigurationException.class)
+                                                                   .hasMessageContaining("Invalid duration: 0.333555555ms");
+    }
+
+    @Test
+    public void testEquals()
+    {
+        assertEquals(new DurationSpec("10s"), new DurationSpec("10s"));
+        assertEquals(new DurationSpec("10s"), new DurationSpec("10000ms"));
+        assertEquals(new DurationSpec("10000ms"), new DurationSpec("10s"));
+        assertEquals(DurationSpec.inMinutes(Long.MAX_VALUE), DurationSpec.inMinutes(Long.MAX_VALUE));
+        assertEquals(new DurationSpec("4h"), new DurationSpec("14400s"));
+        assertEquals(DurationSpec.inSecondsString("14400"), new DurationSpec("14400s"));
+        assertEquals(DurationSpec.inSecondsString("4h"), new DurationSpec("14400s"));
+        assertEquals(DurationSpec.inSecondsString("14400s"), new DurationSpec("14400s"));
+        assertEquals(DurationSpec.inHours(Long.MAX_VALUE),DurationSpec.inHours(Long.MAX_VALUE));
+        assertNotEquals(DurationSpec.inMinutes(Long.MAX_VALUE), DurationSpec.inMilliseconds(Long.MAX_VALUE));
+        assertNotEquals(new DurationSpec("0m"), new DurationSpec("10ms"));
+    }
+
+    @Test
+    public void thereAndBack()
+    {
+        Gen<TimeUnit> unitGen = SourceDSL.arbitrary().enumValues(TimeUnit.class);
+        Gen<Long> valueGen = SourceDSL.longs().between(0, Long.MAX_VALUE);
+        qt().forAll(valueGen, unitGen).check((value, unit) -> {
+            DurationSpec there = new DurationSpec(value, unit);
+            DurationSpec back = new DurationSpec(there.toString());
+            return there.equals(back);
+        });
+    }
+
+    @Test
+    public void testOverflowingDuringConversion()
+    {
+        // we are heavily dependent on the Java TimeUnit for our configuration of type duration. We want to be sure
+        // that any regression in handlining overflow will be caught quickly on our end
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807ns").toNanoseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807ns").toNanosecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807ms").toNanoseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807ms").toNanosecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807s").toNanoseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toNanosecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807m").toNanoseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807m").toNanosecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807h").toNanoseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807h").toNanosecondsAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807ms").toMilliseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807ms").toMillisecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807s").toMilliseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toMillisecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807m").toMilliseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807m").toMillisecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807h").toMilliseconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807h").toMillisecondsAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807s").toSeconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807s").toSecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807m").toSeconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807m").toSecondsAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807h").toSeconds());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807h").toSecondsAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807m").toMinutes());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807m").toMinutesAsInt());
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807h").toMinutes());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807h").toMinutesAsInt());
+
+        assertEquals(Long.MAX_VALUE, new DurationSpec("9223372036854775807h").toHours());
+        assertEquals(Integer.MAX_VALUE, new DurationSpec("9223372036854775807h").toHoursAsInt());
+    }
+}
\ No newline at end of file

---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@cassandra.apache.org
For additional commands, e-mail: commits-help@cassandra.apache.org