com.rabbitmq.perf.VariableValueIndicator Maven / Gradle / Ivy
Show all versions of perf-test Show documentation
// Copyright (c) 2019 Pivotal Software, Inc. All rights reserved.
//
// This software, the RabbitMQ Java client library, is triple-licensed under the
// Mozilla Public License 1.1 ("MPL"), the GNU General Public License version 2
// ("GPL") and the Apache License version 2 ("ASL"). For the MPL, please see
// LICENSE-MPL-RabbitMQ. For the GPL, please see LICENSE-GPL2. For the ASL,
// please see LICENSE-APACHE2.
//
// This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND,
// either express or implied. See the LICENSE file for specific language governing
// rights and limitations of this software.
//
// If you have any questions regarding licensing, please contact us at
// [email protected].
package com.rabbitmq.perf;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import static java.util.Arrays.asList;
/**
* {@link ValueIndicator} that periodically changes the variable value based on variable value definitions.
*
* Definitions use the [VALUE]:[DURATION]
syntax, where VALUE
* is an integer >= 0 and DURATION
is a positive integer. Duration
* is in seconds.
*
* So with value definitions such as 200:60
, 1000:20
, and
* 500:15
, the value will be 200 for 60 seconds, then
* 1000 for 20 seconds, then 500 for 15 seconds, then back to 200 for 60 seconds,
* and so on.
*
* The {@link VariableValueIndicator} periodically updates the value according to value definitions.
* It uses a provided {@link ScheduledExecutorService} for this. The period is the
* greatest common divisor of the durations.
*
* @since 2.8.0
*/
class VariableValueIndicator implements ValueIndicator {
public static final int NANO_TO_SECOND = 1_000_000_000;
private static final Logger LOGGER = LoggerFactory.getLogger(VariableValueIndicator.class);
private final ScheduledExecutorService scheduledExecutorService;
private final List> intervals;
private final AtomicReference value = new AtomicReference<>();
private final List definitions;
public VariableValueIndicator(List values, ScheduledExecutorService scheduledExecutorService,
Function conversion) {
if (values == null || values.isEmpty()) {
throw new IllegalArgumentException();
}
for (String value : values) {
validate(value);
}
this.scheduledExecutorService = scheduledExecutorService;
this.definitions = new ArrayList<>(values);
this.intervals = intervals(values, conversion);
this.value.set(intervals.get(0).value);
}
static void updateValueIfNecessary(List> intervals, long startTime, long now, int cycleDuration, AtomicReference value) {
long elapsed = ((now - startTime) / NANO_TO_SECOND) % cycleDuration;
for (Interval interval : intervals) {
if (interval.isIn(elapsed)) {
if (!value.get().equals(interval.value)) {
value.set(interval.value);
}
break;
}
}
}
@SuppressWarnings("unchecked")
static void validate(String definition) throws IllegalArgumentException {
final AtomicBoolean valid = new AtomicBoolean(true);
if (definition == null || definition.isEmpty() || !definition.contains(":")) {
valid.set(false);
}
String[] valueAndDuration = null;
if (valid.get()) {
valueAndDuration = definition.split(":");
if (valueAndDuration.length != 2) {
valid.set(false);
}
}
if (valid.get()) {
asList(new Object[][]{
{valueAndDuration[0], (Predicate) value -> value >= 0},
{valueAndDuration[1], (Predicate) value -> value > 0}
}).forEach(parameters -> {
String input = parameters[0].toString();
Predicate validation = (Predicate) parameters[1];
try {
int value = Integer.valueOf(input);
if (!validation.test(value)) {
valid.set(false);
}
} catch (NumberFormatException e) {
valid.set(false);
}
});
}
if (!valid.get()) {
throw new IllegalArgumentException(
"Invalid variable value definition: " + definition + ". " +
"Should be [VALUE]:[DURATION] with VALUE integer >= 0 and DURATION integer > 0"
);
}
}
private static int gcd(int a, int b) {
if (a == 0) {
return b;
}
return gcd(b % a, a);
}
static int gcd(int[] arr, int n) {
int result = arr[0];
for (int i = 1; i < n; i++) {
result = gcd(arr[i], result);
}
return result;
}
static List> intervals(List values, Function conversion) {
final List> intervals = new ArrayList<>(values.size());
int start = 0;
for (String value : values) {
validate(value);
String[] valueAndDuration = value.split(":");
int duration = Integer.valueOf(valueAndDuration[1]);
int end = start + duration;
Interval interval = new Interval<>(start, end, conversion.apply(valueAndDuration[0]));
intervals.add(interval);
start = end;
}
return intervals;
}
@Override
public T getValue() {
return value.get();
}
@Override
public void start() {
int[] durations = new int[this.definitions.size()];
for (int i = 0; i < this.definitions.size(); i++) {
String[] valueAndDuration = definitions.get(i).split(":");
durations[i] = Integer.valueOf(valueAndDuration[1]);
}
int cycleDuration = intervals.get(intervals.size() - 1).end;
int gcdSchedulingPeriod = 1;
try {
gcdSchedulingPeriod = gcd(durations, durations.length);
} catch (Exception e) {
LOGGER.warn("Error while calculating GCD: " + e.getMessage() + ". Falling back to 1-second scheduling.");
}
long startTime = System.nanoTime();
scheduledExecutorService.scheduleAtFixedRate(() -> {
updateValueIfNecessary(intervals, startTime, System.nanoTime(), cycleDuration, this.value);
}, 0, gcdSchedulingPeriod, TimeUnit.SECONDS);
}
@Override
public boolean isVariable() {
return true;
}
@Override
public List values() {
return this.intervals.stream().map(interval -> interval.value).collect(Collectors.toList());
}
static class Interval {
final int start;
final int end;
final T value;
Interval(int start, int end, T value) {
this.start = start;
this.end = end;
this.value = value;
}
boolean isIn(long value) {
return value >= start && value < end;
}
@Override
public String toString() {
return "Interval{" +
"start=" + start +
", end=" + end +
", value=" + value +
'}';
}
}
}