001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *     http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing, software
013     * distributed under the License is distributed on an "AS IS" BASIS,
014     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015     * See the License for the specific language governing permissions and
016     * limitations under the License.
017     */
018    
019    package org.apache.hadoop.metrics2.lib;
020    
021    import static org.apache.hadoop.metrics2.lib.Interns.info;
022    
023    import java.io.IOException;
024    import java.util.Map;
025    import java.util.concurrent.Executors;
026    import java.util.concurrent.ScheduledExecutorService;
027    import java.util.concurrent.TimeUnit;
028    
029    import org.apache.commons.lang.StringUtils;
030    import org.apache.hadoop.classification.InterfaceAudience;
031    import org.apache.hadoop.classification.InterfaceStability;
032    import org.apache.hadoop.metrics2.MetricsInfo;
033    import org.apache.hadoop.metrics2.MetricsRecordBuilder;
034    import org.apache.hadoop.metrics2.util.Quantile;
035    import org.apache.hadoop.metrics2.util.SampleQuantiles;
036    
037    import com.google.common.annotations.VisibleForTesting;
038    
039    /**
040     * Watches a stream of long values, maintaining online estimates of specific
041     * quantiles with provably low error bounds. This is particularly useful for
042     * accurate high-percentile (e.g. 95th, 99th) latency metrics.
043     */
044    @InterfaceAudience.Public
045    @InterfaceStability.Evolving
046    public class MutableQuantiles extends MutableMetric {
047    
048      @VisibleForTesting
049      public static final Quantile[] quantiles = { new Quantile(0.50, 0.050),
050          new Quantile(0.75, 0.025), new Quantile(0.90, 0.010),
051          new Quantile(0.95, 0.005), new Quantile(0.99, 0.001) };
052    
053      private final MetricsInfo numInfo;
054      private final MetricsInfo[] quantileInfos;
055      private final int interval;
056    
057      private SampleQuantiles estimator;
058      private long previousCount = 0;
059    
060      @VisibleForTesting
061      protected Map<Quantile, Long> previousSnapshot = null;
062    
063      private final ScheduledExecutorService scheduler = Executors
064          .newScheduledThreadPool(1);
065    
066      /**
067       * Instantiates a new {@link MutableQuantiles} for a metric that rolls itself
068       * over on the specified time interval.
069       * 
070       * @param name
071       *          of the metric
072       * @param description
073       *          long-form textual description of the metric
074       * @param sampleName
075       *          type of items in the stream (e.g., "Ops")
076       * @param valueName
077       *          type of the values
078       * @param interval
079       *          rollover interval (in seconds) of the estimator
080       */
081      public MutableQuantiles(String name, String description, String sampleName,
082          String valueName, int interval) {
083        String ucName = StringUtils.capitalize(name);
084        String usName = StringUtils.capitalize(sampleName);
085        String uvName = StringUtils.capitalize(valueName);
086        String desc = StringUtils.uncapitalize(description);
087        String lsName = StringUtils.uncapitalize(sampleName);
088        String lvName = StringUtils.uncapitalize(valueName);
089    
090        numInfo = info(ucName + "Num" + usName, String.format(
091            "Number of %s for %s with %ds interval", lsName, desc, interval));
092        // Construct the MetricsInfos for the quantiles, converting to percentiles
093        quantileInfos = new MetricsInfo[quantiles.length];
094        String nameTemplate = ucName + "%dthPercentile" + uvName;
095        String descTemplate = "%d percentile " + lvName + " with " + interval
096            + " second interval for " + desc;
097        for (int i = 0; i < quantiles.length; i++) {
098          int percentile = (int) (100 * quantiles[i].quantile);
099          quantileInfos[i] = info(String.format(nameTemplate, percentile),
100              String.format(descTemplate, percentile));
101        }
102    
103        estimator = new SampleQuantiles(quantiles);
104    
105        this.interval = interval;
106        scheduler.scheduleAtFixedRate(new RolloverSample(this), interval, interval,
107            TimeUnit.SECONDS);
108      }
109    
110      @Override
111      public synchronized void snapshot(MetricsRecordBuilder builder, boolean all) {
112        if (all || changed()) {
113          builder.addGauge(numInfo, previousCount);
114          for (int i = 0; i < quantiles.length; i++) {
115            long newValue = 0;
116            // If snapshot is null, we failed to update since the window was empty
117            if (previousSnapshot != null) {
118              newValue = previousSnapshot.get(quantiles[i]);
119            }
120            builder.addGauge(quantileInfos[i], newValue);
121          }
122          if (changed()) {
123            clearChanged();
124          }
125        }
126      }
127    
128      public synchronized void add(long value) {
129        estimator.insert(value);
130      }
131    
132      public int getInterval() {
133        return interval;
134      }
135    
136      /**
137       * Runnable used to periodically roll over the internal
138       * {@link SampleQuantiles} every interval.
139       */
140      private static class RolloverSample implements Runnable {
141    
142        MutableQuantiles parent;
143    
144        public RolloverSample(MutableQuantiles parent) {
145          this.parent = parent;
146        }
147    
148        @Override
149        public void run() {
150          synchronized (parent) {
151            try {
152              parent.previousCount = parent.estimator.getCount();
153              parent.previousSnapshot = parent.estimator.snapshot();
154            } catch (IOException e) {
155              // Couldn't get a new snapshot because the window was empty
156              parent.previousCount = 0;
157              parent.previousSnapshot = null;
158            }
159            parent.estimator.clear();
160          }
161          parent.setChanged();
162        }
163    
164      }
165    }