http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/RecordingStatImpl.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/RecordingStatImpl.java b/commons/src/main/java/com/twitter/common/stats/RecordingStatImpl.java new file mode 100644 index 0000000..aa635e4 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/RecordingStatImpl.java @@ -0,0 +1,49 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.google.common.base.Preconditions; + +/** + * A convenience class to wrap a {@link RecordingStat}. + * + * @author William Farner + */ +class RecordingStatImpl<T extends Number> implements RecordingStat<T> { + private final Stat<T> recorded; + private final String name; + + public RecordingStatImpl(Stat<T> recorded) { + this.recorded = Preconditions.checkNotNull(recorded); + this.name = Stats.validateName(recorded.getName()); + } + + @Override + public String getName() { + return name; + } + + @Override + public T sample() { + return read(); + } + + @Override + public T read() { + return recorded.read(); + } +}
http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/RequestStats.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/RequestStats.java b/commons/src/main/java/com/twitter/common/stats/RequestStats.java new file mode 100644 index 0000000..c2454d3 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/RequestStats.java @@ -0,0 +1,145 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.twitter.common.stats.StatsProvider.RequestTimer; + +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicLong; + +/** + * A class to represent the statistics associated with a client connection to an external service. + * Tracks request latency/rate, and error rate. + * + * @author William Farner + */ +public class RequestStats implements RequestTimer { + + private static final float DEFAULT_SAMPLE_PERCENT = 10; + private static final double[] DEFAULT_PERCENTILES = {10, 50, 90, 99, 99.9, 99.99}; + + private final SlidingStats requests; + private final Percentile<Long> percentile; + + private final AtomicLong errors; + private final AtomicLong reconnects; + private final AtomicLong timeouts; + + /** + * Creates a new request statistics object, using the default percentiles and sampling rate. + * + * @param name The unique name for this request type. + */ + public RequestStats(String name) { + this(name, new Percentile<Long>(name, DEFAULT_SAMPLE_PERCENT, DEFAULT_PERCENTILES)); + } + + /** + * Creates a new request statistics object using a custom percentile tracker. + * + * @param name The unique name for this request type. + * @param percentile The percentile tracker, or {@code null} to disable percentile tracking. + */ + public RequestStats(String name, @Nullable Percentile<Long> percentile) { + requests = new SlidingStats(name + "_requests", "micros"); + this.percentile = percentile; + errors = Stats.exportLong(name + "_errors"); + reconnects = Stats.exportLong(name + "_reconnects"); + timeouts = Stats.exportLong(name + "_timeouts"); + Rate<AtomicLong> requestsPerSec = + Rate.of(name + "_requests_per_sec", requests.getEventCounter()).build(); + Stats.export(Ratio.of(name + "_error_rate", + Rate.of(name + "_errors_per_sec", errors).build(), requestsPerSec)); + Rate<AtomicLong> timeoutsPerSec = Rate.of(name + "_timeouts_per_sec", timeouts).build(); + Stats.export(timeoutsPerSec); + Stats.export(Ratio.of(name + "_timeout_rate", timeoutsPerSec, requestsPerSec)); + } + + public SlidingStats getSlidingStats() { + return requests; + } + + public AtomicLong getErrorCounter() { + return errors; + } + + public AtomicLong getReconnectCounter() { + return reconnects; + } + + public AtomicLong getTimeoutCounter() { + return timeouts; + } + + public Percentile<Long> getPercentile() { + return percentile; + } + + /** + * Accumulates a request and its latency. + * + * @param latencyMicros The elapsed time required to complete the request. + */ + public void requestComplete(long latencyMicros) { + requests.accumulate(latencyMicros); + if (percentile != null) percentile.record(latencyMicros); + } + + /** + * Accumulates the error counter and the request counter. + */ + public void incErrors() { + requestComplete(0); + errors.incrementAndGet(); + } + + /** + * Accumulates the error counter, the request counter and the request latency. + * + * @param latencyMicros The elapsed time before the error occurred. + */ + public void incErrors(long latencyMicros) { + requestComplete(latencyMicros); + errors.incrementAndGet(); + } + + /** + * Accumulates the reconnect counter. + */ + public void incReconnects() { + reconnects.incrementAndGet(); + } + + /** + * Accumulates the timtout counter. + */ + public void incTimeouts() { + timeouts.incrementAndGet(); + } + + public long getErrorCount() { + return errors.get(); + } + + public long getReconnectCount() { + return reconnects.get(); + } + + public long getTimeoutCount() { + return timeouts.get(); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/ReservoirSampler.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/ReservoirSampler.java b/commons/src/main/java/com/twitter/common/stats/ReservoirSampler.java new file mode 100644 index 0000000..061181a --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/ReservoirSampler.java @@ -0,0 +1,84 @@ +package com.twitter.common.stats; + +import java.util.Vector; + +import com.google.common.base.Preconditions; + +import com.twitter.common.util.Random; + +/** + * An in memory implementation of Reservoir Sampling for sampling from + * a population. + * <p>Several optimizations can be done. + * Especially, one can avoid rolling the dice as many times as the + * size of the population with an involved trick. + * See "Random Sampling with a Reservoir", Vitter, 1985</p> + * <p>TODO (delip): Fix this when the problem arises</p> + * + * @param <T> Type of the sample + * @author Delip Rao + */ +public class ReservoirSampler<T> { + private final Vector<T> reservoir = new Vector<T>(); + private final int numSamples; + + private final Random random; + private int numItemsSeen = 0; + + /** + * Create a new sampler with a certain reservoir size using + * a supplied random number generator. + * + * @param numSamples Maximum number of samples to + * retain in the reservoir. Must be non-negative. + * @param random Instance of the random number generator + * to use for sampling + */ + public ReservoirSampler(int numSamples, Random random) { + Preconditions.checkArgument(numSamples > 0, + "numSamples should be positive"); + Preconditions.checkNotNull(random); + this.numSamples = numSamples; + this.random = random; + } + + /** + * Create a new sampler with a certain reservoir size using + * the default random number generator. + * + * @param numSamples Maximum number of samples to + * retain in the reservoir. Must be non-negative. + */ + public ReservoirSampler(int numSamples) { + this(numSamples, Random.Util.newDefaultRandom()); + } + + /** + * Sample an item and store in the reservoir if needed. + * + * @param item The item to sample - may not be null. + */ + public void sample(T item) { + Preconditions.checkNotNull(item); + if (reservoir.size() < numSamples) { + // reservoir not yet full, just append + reservoir.add(item); + } else { + // find a sample to replace + int rIndex = random.nextInt(numItemsSeen + 1); + if (rIndex < numSamples) { + reservoir.set(rIndex, item); + } + } + numItemsSeen++; + } + + /** + * Get samples collected in the reservoir. + * + * @return A sequence of the samples. No guarantee is provided on the order of the samples. + */ + public Iterable<T> getSamples() { + return reservoir; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/SampledStat.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/SampledStat.java b/commons/src/main/java/com/twitter/common/stats/SampledStat.java new file mode 100644 index 0000000..74e503e --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/SampledStat.java @@ -0,0 +1,46 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * A convenience class to perform the basic tasks needed for a {@link RecordingStat} except the + * actual value calculation. + * + * @author William Farner + */ +public abstract class SampledStat<T extends Number> extends StatImpl<T> implements RecordingStat<T> { + + private volatile T prevValue; + + public SampledStat(String name, T defaultValue) { + super(name); + this.prevValue = defaultValue; /* Don't forbid null. */ + } + + public abstract T doSample(); + + @Override + public final T sample() { + prevValue = doSample(); + return prevValue; + } + + @Override + public T read() { + return prevValue; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/Significance.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/Significance.java b/commons/src/main/java/com/twitter/common/stats/Significance.java new file mode 100644 index 0000000..8356e50 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/Significance.java @@ -0,0 +1,62 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * Calculate significance scores between an observed amount and an expected amount. + * + * @author Gilad Mishne + */ +public class Significance { + + /** + * @param observed The observed amount. + * @param expected The expected amount. + * @return [(observed - expected) ** 2 / expected] * sign(observed - expected) + */ + public static double chiSqrScore(double observed, double expected) { + double score = Math.pow((observed - expected), 2) / expected; + if (observed < expected) { + score *= -1; + } + return score; + } + + /** + * @param observed The observed amount. + * @param expected The expected amount. + * @return -2 * expected * log(observed / expected) * sign(observed - expected) + */ + public static double logLikelihood(double observed, double expected) { + if (observed == 0) { + return -expected; + } + if (expected == 0) { + return observed; + } + double score = -2 * observed * Math.log(observed / expected); + if (observed < expected) { + score *= -1; + } + return score; + } + + private Significance() { + // prevent instantiation + } + +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/SlidingStats.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/SlidingStats.java b/commons/src/main/java/com/twitter/common/stats/SlidingStats.java new file mode 100644 index 0000000..b676a57 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/SlidingStats.java @@ -0,0 +1,96 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.twitter.common.base.MorePreconditions; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Tracks event statistics over a sliding window of time. An event is something that has a + * frequency and associated total. + * + * @author William Farner + */ +public class SlidingStats { + + private static final int DEFAULT_WINDOW_SIZE = 1; + + private final AtomicLong total; + private final AtomicLong events; + private final Stat<Double> perEventLatency; + + /** + * Creates a new sliding statistic with the given name + * + * @param name Name for this stat collection. + * @param totalUnitDisplay String to display for the total counter unit. + */ + public SlidingStats(String name, String totalUnitDisplay) { + this(name, totalUnitDisplay, DEFAULT_WINDOW_SIZE); + } + + /** + * Creates a new sliding statistic with the given name + * + * @param name Name for this stat collection. + * @param totalUnitDisplay String to display for the total counter unit. + * @param windowSize The window size for the per second Rate and Ratio stats. + */ + public SlidingStats(String name, String totalUnitDisplay, int windowSize) { + MorePreconditions.checkNotBlank(name); + + String totalDisplay = name + "_" + totalUnitDisplay + "_total"; + String eventDisplay = name + "_events"; + total = Stats.exportLong(totalDisplay); + events = Stats.exportLong(eventDisplay); + perEventLatency = Stats.export(Ratio.of(name + "_" + totalUnitDisplay + "_per_event", + Rate.of(totalDisplay + "_per_sec", total).withWindowSize(windowSize).build(), + Rate.of(eventDisplay + "_per_sec", events).withWindowSize(windowSize).build())); + } + + public AtomicLong getTotalCounter() { + return total; + } + + public AtomicLong getEventCounter() { + return events; + } + + public Stat<Double> getPerEventLatency() { + return perEventLatency; + } + + /** + * Accumulates counter by an offset. This is is useful for tracking things like + * latency of operations. + * + * TODO(William Farner): Implement a wrapper to SlidingStats that expects to accumulate time, and can + * convert between time units. + * + * @param value The value to accumulate. + */ + public void accumulate(long value) { + total.addAndGet(value); + events.incrementAndGet(); + } + + @Override + public String toString() { + return total + " " + events; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/Stat.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/Stat.java b/commons/src/main/java/com/twitter/common/stats/Stat.java new file mode 100644 index 0000000..586b3bb --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/Stat.java @@ -0,0 +1,40 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * A stat that may only be read, no method calls will cause any internal state changes. + * + * @author William Farner + */ +public interface Stat<T> { + + /** + * Gets the name of this stat. For sake of convention, variable names should be alphanumeric, and + * use underscores. + * + * @return The variable name. + */ + String getName(); + + /** + * Retrieves the most recent value of the stat. + * + * @return The most recent value. + */ + T read(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/StatImpl.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/StatImpl.java b/commons/src/main/java/com/twitter/common/stats/StatImpl.java new file mode 100644 index 0000000..78713db --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/StatImpl.java @@ -0,0 +1,38 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.twitter.common.base.MorePreconditions; + +/** + * A convenience class to not require stat implementers to implement {@link #getName()}. + * + * @author William Farner + */ +public abstract class StatImpl<T> implements Stat<T> { + + private final String name; + + public StatImpl(String name) { + this.name = MorePreconditions.checkNotBlank(name); + } + + @Override + public String getName() { + return name; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/StatRegistry.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/StatRegistry.java b/commons/src/main/java/com/twitter/common/stats/StatRegistry.java new file mode 100644 index 0000000..9994aa3 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/StatRegistry.java @@ -0,0 +1,32 @@ +// ================================================================================================= +// Copyright 2012 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * A registry of stats. + * + * @author William Farner + */ +public interface StatRegistry { + + /** + * Gets all stats in the registry. + * + * @return All registered stats. + */ + Iterable<RecordingStat<? extends Number>> getStats(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/Statistics.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/Statistics.java b/commons/src/main/java/com/twitter/common/stats/Statistics.java new file mode 100644 index 0000000..2d051e8 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/Statistics.java @@ -0,0 +1,96 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * A simple class to keep running statistics that require O(1) storage. + * + * @author William Farner + */ +public class Statistics implements StatisticsInterface { + private long populationSize; + private long sum; + private double accumulatedVariance; + private double runningMean; + + private long minValue; + private long maxValue; + + public Statistics() { + clear(); + } + + public void accumulate(long value) { + populationSize++; + sum += value; + double delta = value - runningMean; + runningMean += delta / populationSize; + accumulatedVariance += delta * (value - runningMean); + + // Update max/min. + minValue = value < minValue ? value : minValue; + maxValue = value > maxValue ? value : maxValue; + } + + public void clear() { + populationSize = 0; + sum = 0; + accumulatedVariance = 0; + runningMean = 0; + minValue = Long.MAX_VALUE; + maxValue = Long.MIN_VALUE; + } + + public double variance() { + return accumulatedVariance / populationSize; + } + + public double standardDeviation() { + return Math.sqrt(variance()); + } + + public double mean() { + return runningMean; + } + + public long min() { + return minValue; + } + + public long max() { + return maxValue; + } + + public long range() { + return maxValue - minValue; + } + + public long sum() { + return sum; + } + + public long populationSize() { + return populationSize; + } + + @Override + public String toString() { + return String.format("Mean: %f, Min: %d, Max: %d, Range: %d, Stddev: %f, Variance: %f, " + + "Population: %d, Sum: %d", mean(), min(), max(), range(), standardDeviation(), + variance(), populationSize(), sum()); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/StatisticsInterface.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/StatisticsInterface.java b/commons/src/main/java/com/twitter/common/stats/StatisticsInterface.java new file mode 100644 index 0000000..9be533b --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/StatisticsInterface.java @@ -0,0 +1,73 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +/** + * Interface representing statistics of a set of (long) elements. + */ +public interface StatisticsInterface { + /** + * Add a value in the Statistics object. + * @param value value that you want to accumulate. + */ + void accumulate(long value); + + /** + * Clear the Statistics instance (equivalent to recreate a new one). + */ + void clear(); + + /** + * Return the variance of the inserted elements. + */ + double variance(); + + /** + * Return the standard deviation of the inserted elements. + */ + double standardDeviation(); + + /** + * Return the mean of the inserted elements. + */ + double mean(); + + /** + * Return the min of the inserted elements. + */ + long min(); + + /** + * Return the max of the inserted elements. + */ + long max(); + + /** + * Return the range of the inserted elements. + */ + long range(); + + /** + * Return the sum of the inserted elements. + */ + long sum(); + + /** + * Return the number of the inserted elements. + */ + long populationSize(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/Stats.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/Stats.java b/commons/src/main/java/com/twitter/common/stats/Stats.java new file mode 100644 index 0000000..93bc82d --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/Stats.java @@ -0,0 +1,411 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; +import java.util.regex.Pattern; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Supplier; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.MapMaker; +import com.google.common.util.concurrent.AtomicDouble; + +import com.twitter.common.base.MorePreconditions; + +/** + * Manages {@link Stat}s that should be exported for monitoring. + * + * Statistic names may only contain {@code [A-Za-z0-9_]}, + * all other chars will be logged as a warning and replaced with underscore on export. + * + * @author John Sirois + */ +public class Stats { + + private static final Logger LOG = Logger.getLogger(Stats.class.getName()); + private static final Pattern NOT_NAME_CHAR = Pattern.compile("[^A-Za-z0-9_]"); + + private static final ConcurrentMap<String, Stat<?>> VAR_MAP = new MapMaker().makeMap(); + + // Store stats in the order they were registered, so that derived variables are + // sampled after their inputs. + private static final Collection<RecordingStat<? extends Number>> ORDERED_NUMERIC_STATS = + new ConcurrentLinkedQueue<RecordingStat<? extends Number>>(); + + private static final Cache<String, RecordingStat<? extends Number>> NUMERIC_STATS = + CacheBuilder.newBuilder().build(); + + public static String normalizeName(String name) { + return NOT_NAME_CHAR.matcher(name).replaceAll("_"); + } + + static String validateName(String name) { + String normalized = normalizeName(name); + if (!name.equals(normalized)) { + LOG.warning("Invalid stat name " + name + " exported as " + normalized); + } + return normalized; + } + + /** + * A {@link StatsProvider} that exports gauge-style stats to the global {@link Stat}s repository + * for time series tracking. + */ + public static final StatsProvider STATS_PROVIDER = new StatsProvider() { + private final StatsProvider untracked = new StatsProvider() { + @Override public AtomicLong makeCounter(String name) { + final AtomicLong longVar = new AtomicLong(); + Stats.exportStatic(new StatImpl<Long>(name) { + @Override public Long read() { + return longVar.get(); + } + }); + return longVar; + } + + @Override public <T extends Number> Stat<T> makeGauge(String name, final Supplier<T> gauge) { + return Stats.exportStatic(new StatImpl<T>(name) { + @Override public T read() { + return gauge.get(); + } + }); + } + + @Override public StatsProvider untracked() { + return this; + } + + @Override public RequestTimer makeRequestTimer(String name) { + // TODO(William Farner): Add support for this once a caller shows interest in using it. + throw new UnsupportedOperationException(); + } + }; + + @Override public <T extends Number> Stat<T> makeGauge(String name, final Supplier<T> gauge) { + return Stats.export(new StatImpl<T>(name) { + @Override public T read() { + return gauge.get(); + } + }); + } + + @Override public AtomicLong makeCounter(String name) { + return Stats.exportLong(name); + } + + @Override public StatsProvider untracked() { + return untracked; + } + + @Override public RequestTimer makeRequestTimer(String name) { + return new RequestStats(name); + } + }; + + /** + * A {@link StatRegistry} that provides stats registered with the global {@link Stat}s repository. + */ + public static final StatRegistry STAT_REGISTRY = new StatRegistry() { + @Override public Iterable<RecordingStat<? extends Number>> getStats() { + return Stats.getNumericVariables(); + } + }; + + private static class ExportStat implements Callable<RecordingStat<? extends Number>> { + private final AtomicBoolean called = new AtomicBoolean(false); + + private final RecordingStat<? extends Number> stat; + private final String name; + + private <T extends Number> ExportStat(String name, Stat<T> stat) { + this.name = name; + this.stat = (stat instanceof RecordingStat) + ? (RecordingStat<? extends Number>) stat + : new RecordingStatImpl<T>(stat); + } + + @Override + public RecordingStat<? extends Number> call() { + try { + exportStaticInternal(name, stat); + ORDERED_NUMERIC_STATS.add(stat); + return stat; + } finally { + called.set(true); + } + } + } + + /** + * Exports a stat for tracking. + * if the stat provided implements the internal {@link RecordingStat} interface, it will be + * registered for time series collection and returned. If a {@link RecordingStat} with the same + * name as the provided stat has already been exported, the previously-exported stat will be + * returned and no additional registration will be performed. + * + * @param var The variable to export. + * @param <T> The value exported by the variable. + * @return A reference to the stat that was stored. The stat returned may not be equal to the + * stat provided. If a variable was already returned with the same + */ + public static <T extends Number> Stat<T> export(Stat<T> var) { + String validatedName = validateName(MorePreconditions.checkNotBlank(var.getName())); + ExportStat exportStat = new ExportStat(validatedName, var); + try { + @SuppressWarnings("unchecked") + Stat<T> exported = (Stat<T>) NUMERIC_STATS.get(validatedName, exportStat); + return exported; + } catch (ExecutionException e) { + throw new IllegalStateException( + "Unexpected error exporting stat " + validatedName, e.getCause()); + } finally { + if (!exportStat.called.get()) { + LOG.warning("Re-using already registered variable for key " + validatedName); + } + } + } + + /** + * Exports a string stat. + * String-based statistics will not be registered for time series collection. + * + * @param var Stat to export. + * @return A reference back to {@code var}, or the variable that was already registered under the + * same name as {@code var}. + */ + public static Stat<String> exportString(Stat<String> var) { + return exportStatic(var); + } + + /** + * Adds a collection of stats for export. + * + * @param vars The variables to add. + */ + public static void exportAll(Iterable<Stat<? extends Number>> vars) { + for (Stat<? extends Number> var : vars) { + export(var); + } + } + + /** + * Exports an {@link AtomicInteger}, which will be included in time series tracking. + * + * @param name The name to export the stat with. + * @param intVar The variable to export. + * @return A reference to the {@link AtomicInteger} provided. + */ + public static AtomicInteger export(final String name, final AtomicInteger intVar) { + export(new SampledStat<Integer>(name, 0) { + @Override public Integer doSample() { return intVar.get(); } + }); + + return intVar; + } + + /** + * Creates and exports an {@link AtomicInteger}. + * + * @param name The name to export the stat with. + * @return A reference to the {@link AtomicInteger} created. + */ + public static AtomicInteger exportInt(String name) { + return exportInt(name, 0); + } + + /** + * Creates and exports an {@link AtomicInteger} with initial value. + * + * @param name The name to export the stat with. + * @param initialValue The initial stat value. + * @return A reference to the {@link AtomicInteger} created. + */ + public static AtomicInteger exportInt(String name, int initialValue) { + return export(name, new AtomicInteger(initialValue)); + } + + /** + * Exports an {@link AtomicLong}, which will be included in time series tracking. + * + * @param name The name to export the stat with. + * @param longVar The variable to export. + * @return A reference to the {@link AtomicLong} provided. + */ + public static AtomicLong export(String name, final AtomicLong longVar) { + export(new StatImpl<Long>(name) { + @Override public Long read() { return longVar.get(); } + }); + + return longVar; + } + + /** + * Creates and exports an {@link AtomicLong}. + * + * @param name The name to export the stat with. + * @return A reference to the {@link AtomicLong} created. + */ + public static AtomicLong exportLong(String name) { + return exportLong(name, 0L); + } + + /** + * Creates and exports an {@link AtomicLong} with initial value. + * + * @param name The name to export the stat with. + * @param initialValue The initial stat value. + * @return A reference to the {@link AtomicLong} created. + */ + public static AtomicLong exportLong(String name, long initialValue) { + return export(name, new AtomicLong(initialValue)); + } + + /** + * Exports an {@link AtomicDouble}, which will be included in time series tracking. + * + * @param name The name to export the stat with. + * @param doubleVar The variable to export. + * @return A reference to the {@link AtomicDouble} provided. + */ + public static AtomicDouble export(String name, final AtomicDouble doubleVar) { + export(new StatImpl<Double>(name) { + @Override public Double read() { return doubleVar.doubleValue(); } + }); + + return doubleVar; + } + + /** + * Creates and exports an {@link AtomicDouble}. + * + * @param name The name to export the stat with. + * @return A reference to the {@link AtomicDouble} created. + */ + public static AtomicDouble exportDouble(String name) { + return exportDouble(name, 0.0); + } + + /** + * Creates and exports an {@link AtomicDouble} with initial value. + * + * @param name The name to export the stat with. + * @param initialValue The initial stat value. + * @return A reference to the {@link AtomicDouble} created. + */ + public static AtomicDouble exportDouble(String name, double initialValue) { + return export(name, new AtomicDouble(initialValue)); + } + + /** + * Exports a metric that tracks the size of a collection. + * + * @param name Name of the stat to export. + * @param collection Collection whose size should be tracked. + */ + public static void exportSize(String name, final Collection<?> collection) { + export(new StatImpl<Integer>(name) { + @Override public Integer read() { + return collection.size(); + } + }); + } + + /** + * Exports a metric that tracks the size of a map. + * + * @param name Name of the stat to export. + * @param map Map whose size should be tracked. + */ + public static void exportSize(String name, final Map<?, ?> map) { + export(new StatImpl<Integer>(name) { + @Override public Integer read() { + return map.size(); + } + }); + } + + /** + * Exports a metric that tracks the size of a cache. + * + * @param name Name of the stat to export. + * @param cache Cache whose size should be tracked. + */ + public static void exportSize(String name, final Cache<?, ?> cache) { + export(new StatImpl<Long>(name) { + @Override public Long read() { + return cache.size(); + } + }); + } + + /** + * Exports a 'static' statistic, which will not be registered for time series tracking. + * + * @param var Variable to statically export. + * @return A reference back to the provided {@link Stat}. + */ + public static <T> Stat<T> exportStatic(Stat<T> var) { + String validatedName = validateName(MorePreconditions.checkNotBlank(var.getName())); + exportStaticInternal(validatedName, var); + return var; + } + + private static void exportStaticInternal(String name, Stat<?> stat) { + if (VAR_MAP.put(name, stat) != null) { + LOG.warning("Warning - exported variable collision on " + name); + } + } + + /** + * Fetches all registered stat. + * + * @return An iterable of all registered stats. + */ + public static Iterable<Stat<?>> getVariables() { + return ImmutableList.copyOf(VAR_MAP.values()); + } + + static Iterable<RecordingStat<? extends Number>> getNumericVariables() { + return ImmutableList.copyOf(ORDERED_NUMERIC_STATS); + } + + @VisibleForTesting + public static void flush() { + VAR_MAP.clear(); + ORDERED_NUMERIC_STATS.clear(); + NUMERIC_STATS.invalidateAll(); + } + + public static <T> Stat<T> getVariable(String name) { + MorePreconditions.checkNotBlank(name); + @SuppressWarnings("unchecked") + Stat<T> stat = (Stat<T>) VAR_MAP.get(name); + return stat; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/StatsProvider.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/StatsProvider.java b/commons/src/main/java/com/twitter/common/stats/StatsProvider.java new file mode 100644 index 0000000..347d0d5 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/StatsProvider.java @@ -0,0 +1,91 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.google.common.base.Supplier; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A minimal interface to a Stats repository. + * + * @author John Sirois + */ +public interface StatsProvider { + + /** + * Creates and exports a counter for tracking. + * + * @param name The name to export the stat with. + * @return A reference to the counter that will be tracked for incrementing. + */ + AtomicLong makeCounter(String name); + + /** + * Exports a read-only value for tracking. + * + * @param name The name of the variable to export. + * @param gauge The supplier of the instantaneous values to export. + * @param <T> The type of number exported by the variable. + * @return A reference to the stat that was stored. + */ + <T extends Number> Stat<T> makeGauge(String name, Supplier<T> gauge); + + /** + * Gets a stats provider that does not track stats in an internal time series repository. + * The stored variables will only be available as instantaneous values. + * + * @return A stats provider that creates untracked stats. + */ + StatsProvider untracked(); + + /** + * A stat for tracking service requests. + */ + interface RequestTimer { + + /** + * Accumulates a request and its latency. + * + * @param latencyMicros The elapsed time required to complete the request. + */ + void requestComplete(long latencyMicros); + + /** + * Accumulates the error counter and the request counter. + */ + void incErrors(); + + /** + * Accumulates the reconnect counter. + */ + void incReconnects(); + + /** + * Accumulates the timeout counter. + */ + void incTimeouts(); + } + + /** + * Creates and exports a sets of stats that allows for typical rROC request tracking. + * + * @param name The name to export the stat with. + * @return A reference to the request timer that can be used to track RPCs. + */ + RequestTimer makeRequestTimer(String name); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/TimeSeries.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/TimeSeries.java b/commons/src/main/java/com/twitter/common/stats/TimeSeries.java new file mode 100644 index 0000000..57573fa --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/TimeSeries.java @@ -0,0 +1,41 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import java.util.Calendar; + +/** + * A time series of values. + * + * @author William Farner + */ +public interface TimeSeries { + + /** + * A name describing this time series. + * + * @return The name of this time series data. + */ + public String getName(); + + /** + * A series of numbers representing regular samples of a variable. + * + * @return The time series of sample values. + */ + public Iterable<Number> getSamples(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepository.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepository.java b/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepository.java new file mode 100644 index 0000000..d439219 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepository.java @@ -0,0 +1,60 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import java.util.Set; + +import com.twitter.common.application.ShutdownRegistry; + +/** + * A repository for time series data. + * + * @author William Farner + */ +public interface TimeSeriesRepository { + + /** + * Starts the time series sampler. + * + * @param shutdownRegistry An action registry that the repository can use to register a shutdown + * for the sampler. + */ + public void start(ShutdownRegistry shutdownRegistry); + + /** + * Fetches the names of all available time series. + * + * @return Available time series, which can then be obtained by calling {@link #get(String)}. + */ + public Set<String> getAvailableSeries(); + + /** + * Fetches a time series by name. + * + * @param name The name of the time series to fetch. + * @return The time series registered with the given name, or {@code null} if no such time series + * has been registered. + */ + public TimeSeries get(String name); + + /** + * Gets an ordered iterable of the timestamps that all timeseries were sampled at. + * + * @return All current timestamps. + */ + public Iterable<Number> getTimestamps(); +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepositoryImpl.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepositoryImpl.java b/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepositoryImpl.java new file mode 100644 index 0000000..bfd0fdb --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/TimeSeriesRepositoryImpl.java @@ -0,0 +1,200 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +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.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.inject.Inject; +import com.google.inject.name.Named; + +import com.twitter.common.application.ShutdownRegistry; +import com.twitter.common.base.Command; +import com.twitter.common.collections.BoundedQueue; +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Time; +import com.twitter.common.util.Clock; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * A simple in-memory repository for exported variables. + * + * @author John Sirois + */ +public class TimeSeriesRepositoryImpl implements TimeSeriesRepository { + + private static final Logger LOG = Logger.getLogger(TimeSeriesRepositoryImpl.class.getName()); + + /** + * {@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 BoundedQueue<Number> timestamps; + + private final StatRegistry statRegistry; + private final Amount<Long, Time> samplePeriod; + private final int retainedSampleLimit; + + @Inject + public TimeSeriesRepositoryImpl( + StatRegistry statRegistry, + @Named(SAMPLE_PERIOD) Amount<Long, Time> samplePeriod, + @Named(SAMPLE_RETENTION_PERIOD) final Amount<Long, Time> retentionPeriod) { + this.statRegistry = checkNotNull(statRegistry); + this.samplePeriod = checkNotNull(samplePeriod); + 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 = new BoundedQueue<Number>(retainedSampleLimit); + } + + /** + * Starts the variable sampler, which will fetch variables {@link Stats} on the given period. + * + */ + @Override + public void start(ShutdownRegistry shutdownRegistry) { + checkNotNull(shutdownRegistry); + checkNotNull(samplePeriod); + Preconditions.checkArgument(samplePeriod.getValue() > 0); + + final ScheduledExecutorService executor = new ScheduledThreadPoolExecutor(1 /* One thread. */, + new ThreadFactoryBuilder().setNameFormat("VariableSampler-%d").setDaemon(true).build()); + + final AtomicBoolean shouldSample = new AtomicBoolean(true); + final Runnable sampler = new Runnable() { + @Override public void run() { + if (shouldSample.get()) { + try { + runSampler(Clock.SYSTEM_CLOCK); + } catch (Exception e) { + LOG.log(Level.SEVERE, "ignoring runSampler failure", e); + } + } + } + }; + + executor.scheduleAtFixedRate(sampler, samplePeriod.getValue(), samplePeriod.getValue(), + samplePeriod.getUnit().getTimeUnit()); + shutdownRegistry.addAction(new Command() { + @Override + public void execute() throws RuntimeException { + shouldSample.set(false); + 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 BoundedQueue<Number> samples; + + TimeSeriesImpl(String name) { + this.name = name; + samples = new BoundedQueue<Number>(retainedSampleLimit); + } + + @Override public String getName() { + return name; + } + + void addSample(Number value) { + samples.add(value); + } + + @Override public Iterable<Number> getSamples() { + return Iterables.unmodifiableIterable(samples); + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/Windowed.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/Windowed.java b/commons/src/main/java/com/twitter/common/stats/Windowed.java new file mode 100644 index 0000000..5531e99 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/Windowed.java @@ -0,0 +1,139 @@ +// ================================================================================================= +// Copyright 2013 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import java.lang.reflect.Array; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; + +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Time; +import com.twitter.common.util.Clock; + +/** + * Windowed is an abstraction that let you span a class across a sliding window. + * It creates a ring buffer of T and reuse the buffer after clearing it or use a new one (via + * the {@code clearer} function). + * + * <pre> + * tenured instances + * ++++++++++++++++++++++++++++++++++ + * [----A-----][-----B----][-----C----][-----D----] + * ++++++++++ + * current instance + * </pre> + * + * The schema above represents the valid instances over time + * (A,B,C) are the tenured ones + * D is the current instance + */ +public abstract class Windowed<T> { + private Class<T> clazz; + protected final T[] buffers; + private final long sliceDuration; + private final Clock clock; + private long index = -1L; + private Function<T, T> clearer; + + /** + * @param clazz the type of the underlying element T + * @param window the length of the window + * @param slices the number of slices (the window will be divided into {@code slices} slices) + * @param sliceProvider the supplier of element + * @param clearer the function that clear (or re-create) an element + * @param clock the clock used for to select the appropriate histogram + */ + public Windowed(Class<T> clazz, Amount<Long, Time> window, int slices, + Supplier<T> sliceProvider, Function<T, T> clearer, Clock clock) { + Preconditions.checkNotNull(window); + // Ensure that we have at least 1ms per slice + Preconditions.checkArgument(window.as(Time.MILLISECONDS) > (slices + 1)); + Preconditions.checkArgument(window.as(Time.MILLISECONDS) > (slices + 1)); + Preconditions.checkArgument(0 < slices); + Preconditions.checkNotNull(sliceProvider); + Preconditions.checkNotNull(clock); + + this.clazz = clazz; + this.sliceDuration = window.as(Time.MILLISECONDS) / slices; + @SuppressWarnings("unchecked") // safe because we have the clazz proof of type H + T[] bufs = (T[]) Array.newInstance(clazz, slices + 1); + for (int i = 0; i < bufs.length; i++) { + bufs[i] = sliceProvider.get(); + } + this.buffers = bufs; + this.clearer = clearer; + this.clock = clock; + } + + /** + * Return the index of the latest Histogram. + * You have to modulo it with buffer.length before accessing the array with this number. + */ + protected int getCurrentIndex() { + long now = clock.nowMillis(); + return (int) (now / sliceDuration); + } + + /** + * Check for expired elements and return the current one. + */ + protected T getCurrent() { + sync(getCurrentIndex()); + return buffers[(int) (index % buffers.length)]; + } + + /** + * Check for expired elements and return all the tenured (old) ones. + */ + protected T[] getTenured() { + long currentIndex = getCurrentIndex(); + sync(currentIndex); + @SuppressWarnings("unchecked") // safe because we have the clazz proof of type T + T[] tmp = (T[]) Array.newInstance(clazz, buffers.length - 1); + for (int i = 0; i < tmp.length; i++) { + int idx = (int) ((currentIndex + 1 + i) % buffers.length); + tmp[i] = buffers[idx]; + } + return tmp; + } + + /** + * Clear all the elements. + */ + public void clear() { + for (int i = 0; i <= buffers.length; i++) { + buffers[i] = clearer.apply(buffers[i]); + } + } + + /** + * Synchronize elements with a point in time. + * i.e. Check for expired ones and clear them, and update the index variable. + */ + protected void sync(long currentIndex) { + if (index < currentIndex) { + long from = Math.max(index + 1, currentIndex - buffers.length + 1); + for (long i = from; i <= currentIndex; i++) { + int idx = (int) (i % buffers.length); + buffers[idx] = clearer.apply(buffers[idx]); + } + index = currentIndex; + } + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/WindowedApproxHistogram.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/WindowedApproxHistogram.java b/commons/src/main/java/com/twitter/common/stats/WindowedApproxHistogram.java new file mode 100644 index 0000000..f010b32 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/WindowedApproxHistogram.java @@ -0,0 +1,156 @@ +// ================================================================================================= +// Copyright 2013 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Supplier; + +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Data; +import com.twitter.common.quantity.Time; +import com.twitter.common.util.Clock; + +/** + * WindowedApproxHistogram is an implementation of WindowedHistogram with an + * ApproximateHistogram as the underlying storing histogram. + */ +public class WindowedApproxHistogram extends WindowedHistogram<ApproximateHistogram> { + @VisibleForTesting public static final int DEFAULT_SLICES = 3; + @VisibleForTesting public static final Amount<Long, Time> DEFAULT_WINDOW = + Amount.of(1L, Time.MINUTES); + @VisibleForTesting public static final Amount<Long, Data> DEFAULT_MAX_MEMORY = Amount.of( + (DEFAULT_SLICES + 1) * ApproximateHistogram.DEFAULT_MAX_MEMORY.as(Data.BYTES), Data.BYTES); + + /** + * Create a {@code WindowedApproxHistogram } with a window duration of {@code window} and + * decomposed in {@code slices} Histograms. Those Histograms will individually take less than + * {@code maxMemory / (slices + 1)}. The clock will be used to find the correct index in the + * ring buffer. + * + * @param window duration of the window + * @param slices number of slices in the window + * @param maxMemory maximum memory used by the whole histogram + */ + public WindowedApproxHistogram(Amount<Long, Time> window, final int slices, + final Amount<Long, Data> maxMemory, Clock clock) { + super(ApproximateHistogram.class, window, slices, + new Supplier<ApproximateHistogram>() { + private Amount<Long, Data> perHistogramMemory = Amount.of( + maxMemory.as(Data.BYTES) / (slices + 1), Data.BYTES); + @Override + public ApproximateHistogram get() { + return new ApproximateHistogram(perHistogramMemory); + } + }, + new Function<ApproximateHistogram[], Histogram>() { + @Override + public Histogram apply(ApproximateHistogram[] histograms) { + return ApproximateHistogram.merge(histograms); + } + }, clock); + } + + /** + * Create a {@code WindowedApproxHistogram } with a window duration of {@code window} and + * decomposed in {@code slices} Histograms. Those Histograms will individually have a + * precision of {@code precision / (slices + 1)}. The ticker will be used to measure elapsed + * time in the WindowedHistogram. + * + * @param window duration of the window + * @param slices number of slices in the window + * @param precision precision of the whole histogram + */ + public WindowedApproxHistogram(Amount<Long, Time> window, final int slices, + final Precision precision, Clock clock) { + super(ApproximateHistogram.class, window, slices, + new Supplier<ApproximateHistogram>() { + private Precision perHistogramPrecision = new Precision( + precision.getEpsilon(), precision.getN() / (slices + 1)); + @Override + public ApproximateHistogram get() { + return new ApproximateHistogram(perHistogramPrecision); + } + }, + new Function<ApproximateHistogram[], Histogram>() { + @Override + public Histogram apply(ApproximateHistogram[] histograms) { + return ApproximateHistogram.merge(histograms); + } + }, clock); + } + + /** + * Equivalent to calling + * {@link #WindowedApproxHistogram(Amount, int, Amount, Clock)} + * with the System clock. + */ + public WindowedApproxHistogram(Amount<Long, Time> window, int slices, + Amount<Long, Data> maxMemory) { + this(window, slices, maxMemory, Clock.SYSTEM_CLOCK); + } + + /** + * Equivalent to calling + * {@link #WindowedApproxHistogram(Amount, int, Amount)} + * with default window and slices. + */ + public WindowedApproxHistogram(Amount<Long, Data> maxMemory) { + this(DEFAULT_WINDOW, DEFAULT_SLICES, maxMemory); + } + + /** + * Equivalent to calling + * {@link #WindowedApproxHistogram(Amount, int, Precision, Clock)} + * with the System clock. + */ + public WindowedApproxHistogram(Amount<Long, Time> window, int slices, Precision precision) { + this(window, slices, precision, Clock.SYSTEM_CLOCK); + } + + /** + * Equivalent to calling + * {@link #WindowedApproxHistogram(Amount, int, Precision)} + * with default window and slices. + */ + public WindowedApproxHistogram(Precision precision) { + this(DEFAULT_WINDOW, DEFAULT_SLICES, precision); + } + + /** + * Equivalent to calling + * {@link #WindowedApproxHistogram(Amount, int, Amount, Clock)} + * with the default maxMemory parameter and System clock. + */ + public WindowedApproxHistogram(Amount<Long, Time> window, int slices) { + this(window, slices, DEFAULT_MAX_MEMORY, Clock.SYSTEM_CLOCK); + } + + /** + * WindowedApproxHistogram constructor with default values. + */ + public WindowedApproxHistogram() { + this(DEFAULT_WINDOW, DEFAULT_SLICES, DEFAULT_MAX_MEMORY, Clock.SYSTEM_CLOCK); + } + + /** + * WindowedApproxHistogram constructor with custom Clock (for testing purposes only). + */ + @VisibleForTesting public WindowedApproxHistogram(Clock clock) { + this(DEFAULT_WINDOW, DEFAULT_SLICES, DEFAULT_MAX_MEMORY, clock); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/WindowedHistogram.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/WindowedHistogram.java b/commons/src/main/java/com/twitter/common/stats/WindowedHistogram.java new file mode 100644 index 0000000..bdc7347 --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/WindowedHistogram.java @@ -0,0 +1,113 @@ +// ================================================================================================= +// Copyright 2013 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats; + +import com.google.common.base.Function; +import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; + +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Time; +import com.twitter.common.util.Clock; + +/** + * Histogram windowed over time. + * <p> + * This histogram is composed of a series of ({@code slices} + 1) histograms representing a window + * of {@code range} duration. We only update the latest one, and we query the oldest ones (i.e. all + * histograms except the head). + * </p> + * <pre> + * range + * <-------------> + * [AAA][BBB][CCC][DDD] here slices = 3 + * ---------------------> + * t1 t2 + * + * For t in [t1,t2) we: + * insert elements in DDD + * query quantile over [AAA][BBB][CCC] + * </pre> + * <p> + * When {@code t} is in {@code [t1, t2)} we insert value into the latest histogram (DDD here), + * when we query the histogram, we 'merge' all other histograms (all except the latest) and query + * it. when {@code t > t2} the oldest histogram become the newest (like in a ring buffer) and + * so on ... + * </p> + * <p> + * Note: We use MergedHistogram class to represent a merged histogram without actually + * merging the underlying histograms. + * </p> + */ +public class WindowedHistogram<H extends Histogram> extends Windowed<H> implements Histogram { + + private long mergedHistIndex = -1L; + private Function<H[], Histogram> merger; + private Histogram mergedHistogram = null; + + /** + * Create a WindowedHistogram of {@code slices + 1} elements over a time {@code window}. + * This code is independent from the implementation of Histogram, you just need to provide + * a {@code Supplier<H>} to create the histograms and a {@code Function<H[], Histogram>} to + * merge them. + * + * @param clazz the type of the underlying Histogram H + * @param window the length of the window + * @param slices the number of slices (the window will be divided into {@code slices} slices) + * @param sliceProvider the supplier of histogram + * @param merger the function that merge an array of histogram H[] into a single Histogram + * @param clock the clock used for to select the appropriate histogram + */ + public WindowedHistogram(Class<H> clazz, Amount<Long, Time> window, int slices, + Supplier<H> sliceProvider, Function<H[], Histogram> merger, Clock clock) { + super(clazz, window, slices, sliceProvider, new Function<H, H>() { + @Override + public H apply(H h) { h.clear(); return h; } + }, clock); + Preconditions.checkNotNull(merger); + + this.merger = merger; + } + + @Override + public synchronized void add(long x) { + getCurrent().add(x); + } + + @Override + public synchronized void clear() { + for (Histogram h: buffers) { + h.clear(); + } + } + + @Override + public synchronized long getQuantile(double quantile) { + long currentIndex = getCurrentIndex(); + if (mergedHistIndex < currentIndex) { + H[] tmp = getTenured(); + mergedHistogram = merger.apply(tmp); + mergedHistIndex = currentIndex; + } + return mergedHistogram.getQuantile(quantile); + } + + @Override + public synchronized long[] getQuantiles(double[] quantiles) { + return Histograms.extractQuantiles(this, quantiles); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/WindowedStatistics.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/WindowedStatistics.java b/commons/src/main/java/com/twitter/common/stats/WindowedStatistics.java new file mode 100644 index 0000000..96480ea --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/WindowedStatistics.java @@ -0,0 +1,160 @@ +package com.twitter.common.stats; + +import com.google.common.base.Supplier; +import com.google.common.base.Function; + +import com.twitter.common.quantity.Amount; +import com.twitter.common.quantity.Time; +import com.twitter.common.util.Clock; + +/** + * Keep track of statistics over a set of value in a sliding window. + * WARNING: The computation of the statistics needs to be explicitly requested with + * {@code refresh()} before reading any statistics. + * + * @see Windowed class for more details about how the window is parametrized. + */ +public class WindowedStatistics extends Windowed<Statistics> implements StatisticsInterface { + private int lastIndex = -1; + private double variance = 0.0; + private double mean = 0.0; + private long sum = 0L; + private long min = Long.MAX_VALUE; + private long max = Long.MIN_VALUE; + private long populationSize = 0L; + + public WindowedStatistics(Amount<Long, Time> window, int slices, Clock clock) { + super(Statistics.class, window, slices, + new Supplier<Statistics>() { + @Override public Statistics get() { return new Statistics(); } + }, + new Function<Statistics, Statistics>() { + @Override public Statistics apply(Statistics s) { s.clear(); return s; } + }, + clock); + } + + /** + * Construct a Statistics sliced over time in {@code slices + 1} windows. + * The {@code window} parameter represents the total window, that will be sliced into + * {@code slices + 1} parts. + * + * Ex: WindowedStatistics(Amount.of(1L, Time.MINUTES), 3) will be sliced like this: + * <pre> + * 20s 20s 20s 20s + * [----A-----][-----B----][-----C----][-----D----] + * </pre> + * The current window is 'D' (the one you insert elements into) and the tenured windows + * are 'A', 'B', 'C' (the ones you read elements from). + */ + public WindowedStatistics(Amount<Long, Time> window, int slices) { + this(window, slices, Clock.SYSTEM_CLOCK); + } + + /** + * Equivalent to calling {@link #WindowedStatistics(Amount, int)} with a 1 minute window + * and 3 slices. + */ + public WindowedStatistics() { + this(Amount.of(1L, Time.MINUTES), 3, Clock.SYSTEM_CLOCK); + } + + public void accumulate(long value) { + getCurrent().accumulate(value); + } + + /** + * Compute all the statistics in one pass. + */ + public void refresh() { + int currentIndex = getCurrentIndex(); + if (lastIndex != currentIndex) { + lastIndex = currentIndex; + double x = 0.0; + variance = 0.0; + mean = 0.0; + sum = 0L; + populationSize = 0L; + min = Long.MAX_VALUE; + max = Long.MIN_VALUE; + for (Statistics s : getTenured()) { + if (s.populationSize() == 0) { + continue; + } + x += s.populationSize() * (s.variance() + s.mean() * s.mean()); + sum += s.sum(); + populationSize += s.populationSize(); + min = Math.min(min, s.min()); + max = Math.max(max, s.max()); + } + if (populationSize != 0) { + mean = ((double) sum) / populationSize; + variance = x / populationSize - mean * mean; + } + } + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the variance of the aggregated windows + */ + public double variance() { + return variance; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the standard deviation of the aggregated windows + */ + public double standardDeviation() { + return Math.sqrt(variance()); + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the mean of the aggregated windows + */ + public double mean() { + return mean; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the sum of the aggregated windows + */ + public long sum() { + return sum; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the min of the aggregated windows + */ + public long min() { + return min; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the max of the aggregated windows + */ + public long max() { + return max; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the range of the aggregated windows + */ + public long range() { + return max - min; + } + + /** + * WARNING: You need to call refresh() to recompute the variance + * @return the population size of the aggregated windows + */ + public long populationSize() { + return populationSize; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/stats/testing/RealHistogram.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/stats/testing/RealHistogram.java b/commons/src/main/java/com/twitter/common/stats/testing/RealHistogram.java new file mode 100644 index 0000000..ff1e84a --- /dev/null +++ b/commons/src/main/java/com/twitter/common/stats/testing/RealHistogram.java @@ -0,0 +1,45 @@ +// ================================================================================================= +// Copyright 2013 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.stats.testing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.twitter.common.stats.Histogram; +import com.twitter.common.stats.Histograms; + +public class RealHistogram implements Histogram { + private final List<Long> buffer = new ArrayList<Long>(); + + @Override public void add(long x) { + buffer.add(x); + } + + @Override public void clear() { + buffer.clear(); + } + + @Override public long getQuantile(double quantile) { + Collections.sort(buffer); + return buffer.get((int) (quantile * buffer.size())); + } + + @Override public long[] getQuantiles(double[] quantiles) { + return Histograms.extractQuantiles(this, quantiles); + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/86a547b9/commons/src/main/java/com/twitter/common/testing/TearDownRegistry.java ---------------------------------------------------------------------- diff --git a/commons/src/main/java/com/twitter/common/testing/TearDownRegistry.java b/commons/src/main/java/com/twitter/common/testing/TearDownRegistry.java new file mode 100644 index 0000000..6061dda --- /dev/null +++ b/commons/src/main/java/com/twitter/common/testing/TearDownRegistry.java @@ -0,0 +1,54 @@ +// ================================================================================================= +// Copyright 2011 Twitter, Inc. +// ------------------------------------------------------------------------------------------------- +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this work except in compliance with the License. +// You may obtain a copy of the License in the LICENSE file, or 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 com.twitter.common.testing; + +import com.google.common.base.Preconditions; +import com.google.common.testing.TearDown; +import com.google.common.testing.TearDownAccepter; + +import com.twitter.common.application.ShutdownRegistry; +import com.twitter.common.base.ExceptionalCommand; + +/** + * An action registry suitable for use as a shutdownRegistry in tests that extend + * {@link com.google.common.testing.junit4.TearDownTestCase}. + * + * @author John Sirois + */ +public class TearDownRegistry implements ShutdownRegistry { + private final TearDownAccepter tearDownAccepter; + + /** + * Creates a new tear down registry that delegates execution of shutdown actions to a + * {@code tearDownAccepter}. + * + * @param tearDownAccepter A tear down accepter that will be used to register shutdown actions + * with. + */ + public TearDownRegistry(TearDownAccepter tearDownAccepter) { + this.tearDownAccepter = Preconditions.checkNotNull(tearDownAccepter); + } + + @Override + public <E extends Exception, T extends ExceptionalCommand<E>> void addAction(final T action) { + tearDownAccepter.addTearDown(new TearDown() { + @Override public void tearDown() throws Exception { + action.execute(); + } + }); + } +}
