blob: 1ec2d1e77420d0bd31b56ab6578d97875fc71a84 [file] [log] [blame]
/*
* 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.math.DoubleMath;
import com.google.common.primitives.Ints;
import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.BYTES_PER_SECOND;
/**
* 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 abstract class DataRateSpec
{
/**
* The Regexp used to parse the rate provided as String in cassandra.yaml.
*/
private static final Pattern UNITS_PATTERN = Pattern.compile("^(\\d+)(MiB/s|KiB/s|B/s)$");
private final long quantity;
private final DataRateUnit unit;
private DataRateSpec(String value)
{
//parse the string field value
Matcher matcher = UNITS_PATTERN.matcher(value);
if (!matcher.find())
throw new IllegalArgumentException("Invalid data 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));
}
private DataRateSpec(String value, DataRateUnit minUnit, long max)
{
this(value);
validateQuantity(value, quantity(), unit(), minUnit, max);
}
private DataRateSpec(long quantity, DataRateUnit unit, DataRateUnit minUnit, long max)
{
this.quantity = quantity;
this.unit = unit;
validateQuantity(quantity, unit, minUnit, max);
}
private static void validateQuantity(String value, double quantity, DataRateUnit unit, DataRateUnit minUnit, long max)
{
// negatives are not allowed by the regex pattern
if (minUnit.convert(quantity, unit) >= max)
throw new IllegalArgumentException("Invalid data rate: " + value + ". It shouldn't be more than " +
(max - 1) + " in " + minUnit.name().toLowerCase());
}
private static void validateQuantity(double quantity, DataRateUnit unit, DataRateUnit minUnit, long max)
{
if (quantity < 0)
throw new IllegalArgumentException("Invalid data rate: value must be non-negative");
if (minUnit.convert(quantity, unit) >= max)
throw new IllegalArgumentException(String.format("Invalid data rate: %s %s. It shouldn't be more than %d in %s",
quantity, unit.name().toLowerCase(),
max - 1, minUnit.name().toLowerCase()));
}
// get vs no-get prefix is not consistent in the code base, but for classes involved with config parsing, it is
// imporant to be explicit about get/set as this changes how parsing is done; this class is a data-type, so is
// not nested, having get/set can confuse parsing thinking this is a nested type
/**
* @return the data rate unit assigned.
*/
public DataRateUnit unit()
{
return unit;
}
/**
* @return the data rate quantity.
*/
private double quantity()
{
return quantity;
}
/**
* @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 (DoubleMath.isMathematicalInteger(quantity) ? (long) quantity : quantity) + unit.symbol;
}
/**
* Represents a data rate used for Cassandra configuration. The bound is [0, Long.MAX_VALUE) in bytes per second.
* If the user sets a different unit, we still validate that converted to bytes per second the quantity will not exceed
* that upper bound. (CASSANDRA-17571)
*/
public final static class LongBytesPerSecondBound extends DataRateSpec
{
/**
* Creates a {@code DataRateSpec.LongBytesPerSecondBound} of the specified amount.
*
* @param value the data rate
*/
public LongBytesPerSecondBound(String value)
{
super(value, BYTES_PER_SECOND, Long.MAX_VALUE);
}
/**
* Creates a {@code DataRateSpec.LongBytesPerSecondBound} of the specified amount in the specified unit.
*
* @param quantity where quantity shouldn't be bigger than Long.MAX_VALUE - 1 in bytes per second
* @param unit in which the provided quantity is
*/
public LongBytesPerSecondBound(long quantity, DataRateUnit unit)
{
super(quantity, unit, BYTES_PER_SECOND, Long.MAX_VALUE);
}
/**
* Creates a {@code DataRateSpec.LongBytesPerSecondBound} of the specified amount in bytes per second.
*
* @param bytesPerSecond where bytesPerSecond shouldn't be bigger than Long.MAX_VALUE
*/
public LongBytesPerSecondBound(long bytesPerSecond)
{
this(bytesPerSecond, BYTES_PER_SECOND);
}
// this one should be used only for backward compatibility for stream_throughput_outbound and inter_dc_stream_throughput_outbound
// which were in megabits per second in 4.0. Do not start using it for any new properties
@Deprecated
public static LongBytesPerSecondBound megabitsPerSecondInBytesPerSecond(long megabitsPerSecond)
{
final long BYTES_PER_MEGABIT = 125_000;
long bytesPerSecond = megabitsPerSecond * BYTES_PER_MEGABIT;
if (megabitsPerSecond >= Integer.MAX_VALUE)
throw new IllegalArgumentException("Invalid data rate: " + megabitsPerSecond + " megabits per second; " +
"stream_throughput_outbound and inter_dc_stream_throughput_outbound" +
" should be between 0 and " + (Integer.MAX_VALUE - 1) + " in megabits per second");
return new LongBytesPerSecondBound(bytesPerSecond, BYTES_PER_SECOND);
}
}
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 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 IllegalArgumentException(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();
}
}
}