/**
* Licensed 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.aurora.common.stats;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.EvictingQueue;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.primitives.Longs;
import com.google.common.util.concurrent.AbstractScheduledService;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.inject.Inject;
import com.google.inject.name.Named;
import org.apache.aurora.common.quantity.Amount;
import org.apache.aurora.common.quantity.Time;
import org.apache.aurora.common.util.BuildInfo;
import org.apache.aurora.common.util.Clock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A simple in-memory repository for exported variables.
*
* @author John Sirois
*/
public class TimeSeriesRepositoryImpl
extends AbstractScheduledService implements TimeSeriesRepository {
private static final Logger LOG = LoggerFactory.getLogger(TimeSeriesRepositoryImpl.class);
/**
* {@literal @Named} binding key for the sampling period.
*/
public static final String SAMPLE_PERIOD =
"com.twitter.common.stats.TimeSeriesRepositoryImpl.SAMPLE_PERIOD";
/**
* {@literal @Named} binding key for the maximum number of retained samples.
*/
public static final String SAMPLE_RETENTION_PERIOD =
"com.twitter.common.stats.TimeSeriesRepositoryImpl.SAMPLE_RETENTION_PERIOD";
private final SlidingStats scrapeDuration = new SlidingStats("variable_scrape", "micros");
// We store TimeSeriesImpl, which allows us to add samples.
private final LoadingCache<String, TimeSeriesImpl> timeSeries;
private final EvictingQueue<Number> timestamps;
private final StatRegistry statRegistry;
private final Amount<Long, Time> samplePeriod;
private final int retainedSampleLimit;
private final BuildInfo buildInfo;
@Inject
public TimeSeriesRepositoryImpl(
StatRegistry statRegistry,
@Named(SAMPLE_PERIOD) Amount<Long, Time> samplePeriod,
@Named(SAMPLE_RETENTION_PERIOD) final Amount<Long, Time> retentionPeriod,
BuildInfo buildInfo) {
this.statRegistry = checkNotNull(statRegistry);
this.samplePeriod = checkNotNull(samplePeriod);
this.buildInfo = checkNotNull(buildInfo);
Preconditions.checkArgument(samplePeriod.getValue() > 0, "Sample period must be positive.");
checkNotNull(retentionPeriod);
Preconditions.checkArgument(retentionPeriod.getValue() > 0,
"Sample retention period must be positive.");
retainedSampleLimit = (int) (retentionPeriod.as(Time.SECONDS) / samplePeriod.as(Time.SECONDS));
Preconditions.checkArgument(retainedSampleLimit > 0,
"Sample retention period must be greater than sample period.");
timeSeries = CacheBuilder.newBuilder().build(
new CacheLoader<String, TimeSeriesImpl>() {
@Override public TimeSeriesImpl load(final String name) {
TimeSeriesImpl timeSeries = new TimeSeriesImpl(name);
// Backfill so we have data for pre-accumulated timestamps.
int numTimestamps = timestamps.size();
if (numTimestamps != 0) {
for (int i = 1; i < numTimestamps; i++) {
timeSeries.addSample(0L);
}
}
return timeSeries;
}
});
timestamps = EvictingQueue.create(retainedSampleLimit);
}
private final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(
1 /* One thread. */,
new ThreadFactoryBuilder().setNameFormat("VariableSampler-%d").setDaemon(true).build());
@Override
protected void startUp() throws Exception {
JvmStats.export();
for (String name : buildInfo.getProperties().keySet()) {
final String stringValue = buildInfo.getProperties().get(name);
LOG.info("Build Info key: " + name + " has value " + stringValue);
if (stringValue == null) {
continue;
}
final Long longValue = Longs.tryParse(stringValue);
if (longValue != null) {
Stats.exportStatic(new StatImpl<Long>(Stats.normalizeName("build." + name)) {
@Override
public Long read() {
return longValue;
}
});
} else {
Stats.exportString(new StatImpl<String>(Stats.normalizeName("build." + name)) {
@Override
public String read() {
return stringValue;
}
});
}
}
}
@Override
protected Scheduler scheduler() {
return Scheduler.newFixedRateSchedule(
samplePeriod.getValue(),
samplePeriod.getValue(),
samplePeriod.getUnit().getTimeUnit());
}
@Override
protected ScheduledExecutorService executor() {
return executor;
}
@Override
protected void runOneIteration() throws Exception {
runSampler(Clock.SYSTEM_CLOCK);
}
@Override
protected void shutDown() throws Exception {
executor.shutdown();
LOG.info("Variable sampler shut down");
}
@VisibleForTesting
synchronized void runSampler(Clock clock) {
timestamps.add(clock.nowMillis());
long startNanos = clock.nowNanos();
for (RecordingStat<? extends Number> var : statRegistry.getStats()) {
timeSeries.getUnchecked(var.getName()).addSample(var.sample());
}
scrapeDuration.accumulate(
Amount.of(clock.nowNanos() - startNanos, Time.NANOSECONDS).as(Time.MICROSECONDS));
}
@Override
public synchronized Set<String> getAvailableSeries() {
return ImmutableSet.copyOf(timeSeries.asMap().keySet());
}
@Override
public synchronized TimeSeries get(String name) {
if (!timeSeries.asMap().containsKey(name)) return null;
return timeSeries.getUnchecked(name);
}
@Override
public synchronized Iterable<Number> getTimestamps() {
return Iterables.unmodifiableIterable(timestamps);
}
private class TimeSeriesImpl implements TimeSeries {
private final String name;
private final EvictingQueue<Number> samples;
TimeSeriesImpl(String name) {
this.name = name;
samples = EvictingQueue.create(retainedSampleLimit);
}
@Override public String getName() {
return name;
}
void addSample(Number value) {
samples.add(value);
}
@Override public Iterable<Number> getSamples() {
return Iterables.unmodifiableIterable(samples);
}
}
}