package com.lambdaworks.redis.metrics;
import com.lambdaworks.redis.metrics.CommandMetrics.CommandLatency;
import com.lambdaworks.redis.protocol.CommandType;
import com.lambdaworks.redis.protocol.ProtocolKeyword;
import io.netty.channel.local.LocalAddress;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
import org.HdrHistogram.Histogram;
import org.LatencyUtils.LatencyStats;
import org.LatencyUtils.PauseDetector;
import org.LatencyUtils.SimplePauseDetector;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import static com.lambdaworks.redis.internal.LettuceClassUtils.isPresent;
/**
* Default implementation of a {@link CommandLatencyCollector} for command latencies.
*
* @author Mark Paluch
*/
public class DefaultCommandLatencyCollector implements CommandLatencyCollector {
private static final AtomicReference<PauseDetectorWrapper> PAUSE_DETECTOR = new AtomicReference<>();
private static final boolean LATENCY_UTILS_AVAILABLE = isPresent("org.LatencyUtils.PauseDetector");
private static final boolean HDR_UTILS_AVAILABLE = isPresent("org.HdrHistogram.Histogram");
private static final long MIN_LATENCY = 1000;
private static final long MAX_LATENCY = TimeUnit.MINUTES.toNanos(5);
private final CommandLatencyCollectorOptions options;
private Map<CommandLatencyId, Latencies> latencyMetrics = new ConcurrentHashMap<>(CommandType.values().length);
public DefaultCommandLatencyCollector(CommandLatencyCollectorOptions options) {
this.options = options;
}
/**
* Record the command latency per {@code connectionPoint} and {@code commandType}.
*
* @param local the local address
* @param remote the remote address
* @param commandType the command type
* @param firstResponseLatency latency value in {@link TimeUnit#NANOSECONDS} from send to the first response
* @param completionLatency latency value in {@link TimeUnit#NANOSECONDS} from send to the command completion
*/
public void recordCommandLatency(SocketAddress local, SocketAddress remote, ProtocolKeyword commandType,
long firstResponseLatency, long completionLatency) {
if (!isEnabled()) {
return;
}
CommandLatencyId id = createId(local, remote, commandType);
Latencies latencies = latencyMetrics.get(id);
if (latencies == null) {
PauseDetectorWrapper wrapper = PAUSE_DETECTOR.get();
if (wrapper == null) {
wrapper = new PauseDetectorWrapper();
if (PAUSE_DETECTOR.compareAndSet(null, wrapper)) {
wrapper.initialize();
}
}
latencies = new Latencies(PAUSE_DETECTOR.get().pauseDetector);
latencyMetrics.put(id, latencies);
}
latencies.firstResponse.recordLatency(rangify(firstResponseLatency));
latencies.completion.recordLatency(rangify(completionLatency));
}
private CommandLatencyId createId(SocketAddress local, SocketAddress remote, ProtocolKeyword commandType) {
return CommandLatencyId.create(options.localDistinction() ? local : LocalAddress.ANY, remote, commandType);
}
private long rangify(long latency) {
return Math.max(MIN_LATENCY, Math.min(MAX_LATENCY, latency));
}
@Override
public boolean isEnabled() {
return latencyMetrics != null && options.isEnabled();
}
@Override
public void shutdown() {
if (latencyMetrics != null) {
latencyMetrics.clear();
latencyMetrics = null;
}
}
@Override
public Map<CommandLatencyId, CommandMetrics> retrieveMetrics() {
Map<CommandLatencyId, Latencies> copy = new HashMap<>();
copy.putAll(latencyMetrics);
if (options.resetLatenciesAfterEvent()) {
latencyMetrics.clear();
}
Map<CommandLatencyId, CommandMetrics> latencies = getMetrics(copy);
return latencies;
}
private Map<CommandLatencyId, CommandMetrics> getMetrics(Map<CommandLatencyId, Latencies> latencyMetrics) {
Map<CommandLatencyId, CommandMetrics> latencies = new TreeMap<>();
for (Map.Entry<CommandLatencyId, Latencies> entry : latencyMetrics.entrySet()) {
Histogram firstResponse = entry.getValue().firstResponse.getIntervalHistogram();
Histogram completion = entry.getValue().completion.getIntervalHistogram();
if (firstResponse.getTotalCount() == 0 && completion.getTotalCount() == 0) {
continue;
}
CommandLatency firstResponseLatency = getMetric(firstResponse);
CommandLatency completionLatency = getMetric(completion);
CommandMetrics metrics = new CommandMetrics(firstResponse.getTotalCount(), options.targetUnit(),
firstResponseLatency, completionLatency);
latencies.put(entry.getKey(), metrics);
}
return latencies;
}
private CommandLatency getMetric(Histogram histogram) {
Map<Double, Long> percentiles = getPercentiles(histogram);
TimeUnit timeUnit = options.targetUnit();
CommandLatency metric = new CommandLatency(timeUnit.convert(histogram.getMinValue(), TimeUnit.NANOSECONDS),
timeUnit.convert(histogram.getMaxValue(), TimeUnit.NANOSECONDS), percentiles);
return metric;
}
private Map<Double, Long> getPercentiles(Histogram histogram) {
Map<Double, Long> percentiles = new TreeMap<Double, Long>();
for (double targetPercentile : options.targetPercentiles()) {
percentiles.put(targetPercentile,
options.targetUnit().convert(histogram.getValueAtPercentile(targetPercentile), TimeUnit.NANOSECONDS));
}
return percentiles;
}
/**
* Returns {@literal true} if HdrUtils and LatencyUtils are available on the class path.
*
* @return
*/
public static boolean isAvailable() {
return LATENCY_UTILS_AVAILABLE && HDR_UTILS_AVAILABLE;
}
/**
* Returns a disabled no-op {@link CommandLatencyCollector}.
*
* @return
*/
public static CommandLatencyCollector disabled() {
return new CommandLatencyCollector() {
@Override
public void recordCommandLatency(SocketAddress local, SocketAddress remote, ProtocolKeyword commandType,
long firstResponseLatency, long completionLatency) {
}
@Override
public void shutdown() {
}
@Override
public Map<CommandLatencyId, CommandMetrics> retrieveMetrics() {
return Collections.emptyMap();
}
@Override
public boolean isEnabled() {
return false;
}
};
}
private static class Latencies {
public final LatencyStats firstResponse;
public final LatencyStats completion;
public Latencies(PauseDetector pauseDetector) {
firstResponse = LatencyStats.Builder.create().pauseDetector(pauseDetector).build();
completion = LatencyStats.Builder.create().pauseDetector(pauseDetector).build();
}
}
private static class PauseDetectorWrapper {
public static final AtomicLong counter = new AtomicLong();
PauseDetector pauseDetector;
public void initialize() {
if (counter.getAndIncrement() > 0) {
InternalLogger instance = InternalLoggerFactory.getInstance(getClass());
instance.info("Initialized PauseDetectorWrapper more than once.");
}
pauseDetector = new SimplePauseDetector(TimeUnit.MILLISECONDS.toNanos(10), TimeUnit.MILLISECONDS.toNanos(10), 3);
Runtime.getRuntime().addShutdownHook(new Thread("ShutdownHook for SimplePauseDetector") {
@Override
public void run() {
pauseDetector.shutdown();
}
});
}
}
}