/*
* 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 gobblin.util.limiter.stressTest;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import gobblin.util.Decorator;
import gobblin.util.limiter.Limiter;
import gobblin.util.limiter.RestliServiceBasedLimiter;
import javax.annotation.Nullable;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* Used to compute statistics on a set of {@link Limiter}s used in a throttling service stress test.
*/
@Slf4j
@RequiredArgsConstructor
public class RateComputingLimiterContainer {
private final List<AtomicLong> subLimiterPermitCounts = Lists.newArrayList();
private final Queue<Long> unusedPermitsCounts = new LinkedList<>();
private Map<String, Long> lastReportTimes = Maps.newHashMap();
/**
* Decorate a {@link Limiter} to measure its permit rate for statistics computation.
*/
public Limiter decorateLimiter(Limiter limiter) {
AtomicLong localCount = new AtomicLong();
return new RateComputingLimiterDecorator(limiter, localCount);
}
/**
* A {@link Limiter} decorator that records all permits granted.
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class RateComputingLimiterDecorator implements Limiter, Decorator {
private final Limiter underlying;
private final AtomicLong localPermitCount;
@Override
public Object getDecoratedObject() {
return this.underlying;
}
@Override
public void start() {
this.underlying.start();
RateComputingLimiterContainer.this.subLimiterPermitCounts.add(this.localPermitCount);
}
@Override
public Closeable acquirePermits(long permits) throws InterruptedException {
Closeable closeable = this.underlying.acquirePermits(permits);
this.localPermitCount.addAndGet(permits);
return closeable;
}
@Override
public void stop() {
this.underlying.stop();
if (this.underlying instanceof RestliServiceBasedLimiter) {
RestliServiceBasedLimiter restliLimiter = (RestliServiceBasedLimiter) this.underlying;
RateComputingLimiterContainer.this.unusedPermitsCounts.add(restliLimiter.getUnusedPermits());
log.info("Unused permits: " + restliLimiter.getUnusedPermits());
}
RateComputingLimiterContainer.this.subLimiterPermitCounts.remove(this.localPermitCount);
}
}
/**
* Get a {@link DescriptiveStatistics} object with the rate of permit granting for all {@link Limiter}s decorated
* with this {@link RateComputingLimiterContainer}.
*/
public @Nullable DescriptiveStatistics getRateStatsSinceLastReport() {
return getNormalizedStatistics("seenQPS", Lists.transform(this.subLimiterPermitCounts, new Function<AtomicLong, Double>() {
@Override
public Double apply(AtomicLong atomicLong) {
return (double) atomicLong.getAndSet(0);
}
}));
}
public @Nullable DescriptiveStatistics getUnusedPermitsSinceLastReport() {
DescriptiveStatistics stats = getNormalizedStatistics("unusedPermits", this.unusedPermitsCounts);
this.unusedPermitsCounts.clear();
return stats;
}
private @Nullable DescriptiveStatistics getNormalizedStatistics(String key, Collection<? extends Number> values) {
long now = System.currentTimeMillis();
long deltaTime = 0;
if (this.lastReportTimes.containsKey(key)) {
deltaTime = now - this.lastReportTimes.get(key);
}
this.lastReportTimes.put(key, now);
if (deltaTime == 0) {
return null;
}
double[] normalizedValues = new double[values.size()];
int i = 0;
for (Number value : values) {
normalizedValues[i++] = 1000 * value.doubleValue() / deltaTime;
}
return new DescriptiveStatistics(normalizedValues);
}
}