/*
* Copyright (C) 2012 Facebook, Inc.
*
* 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 com.facebook.stats;
import org.joda.time.DateTime;
import org.joda.time.DateTimeUtils;
import org.joda.time.Duration;
import org.joda.time.ReadableDateTime;
public class MultiWindowGauge implements ReadableMultiWindowGauge, WritableMultiWindowStat {
private final GaugeCounterFactory gaugeCounterFactory;
// all-time counter is a windowed counter that is effectively unbounded
private final CompositeGaugeCounter allTimeCounter;
private final CompositeGaugeCounter hourCounter;
private final CompositeGaugeCounter tenMinuteCounter;
private final CompositeGaugeCounter minuteCounter;
private volatile GaugeCounter currentCounter;
private final ReadableDateTime start;
private final Object rollLock = new Object();
public MultiWindowGauge() {
this(DefaultGaugeCounterFactory.INSTANCE);
}
public MultiWindowGauge(GaugeCounterFactory gaugeCounterFactory) {
this(
gaugeCounterFactory,
newCompositeGaugeCounter(Integer.MAX_VALUE, gaugeCounterFactory),
newCompositeGaugeCounter(60, gaugeCounterFactory),
newCompositeGaugeCounter(10, gaugeCounterFactory),
newCompositeGaugeCounter(1, gaugeCounterFactory),
new DateTime()
);
}
MultiWindowGauge(
GaugeCounterFactory gaugeCounterFactory,
CompositeGaugeCounter allTimeCounter,
CompositeGaugeCounter hourCounter,
CompositeGaugeCounter tenMinuteCounter,
CompositeGaugeCounter minuteCounter,
ReadableDateTime start
) {
this.gaugeCounterFactory = gaugeCounterFactory;
this.allTimeCounter = allTimeCounter;
this.hourCounter = hourCounter;
this.tenMinuteCounter = tenMinuteCounter;
this.minuteCounter = minuteCounter;
this.start = start;
currentCounter = nextCurrentCounter();
}
private static CompositeGaugeCounter newCompositeGaugeCounter(
int minutes, GaugeCounterFactory gaugeCounterFactory
) {
return new CompositeGaugeCounter(
Duration.standardMinutes(minutes), gaugeCounterFactory
);
}
@Override
public void add(long delta) {
rollCurrentIfNeeded();
currentCounter.add(delta);
}
private void rollCurrentIfNeeded() {
// do outside the synchronized block
long now = DateTimeUtils.currentTimeMillis();
// this is false for the majority of calls, so skip lock acquisition
if (currentCounter.getEnd().getMillis() < now) {
synchronized (rollLock) {
// lock and re-check
if (currentCounter.getEnd().getMillis() < now) {
currentCounter = nextCurrentCounter();
}
}
}
}
private long calcRate(EventCounterIf<GaugeCounter> counter) {
long value = counter.getValue();
ReadableDateTime end = counter.getEnd();
ReadableDateTime start = counter.getStart();
ReadableDateTime now = new DateTime();
Duration duration = now.isBefore(end) ?
new Duration(start, now) : // so far
new Duration(start, end);
long secs = duration.getStandardSeconds();
return secs > 0 ? value / secs : value;
}
@Override
public long getMinuteSum() {
rollCurrentIfNeeded();
return minuteCounter.getValue();
}
@Override
public long getMinuteSamples() {
rollCurrentIfNeeded();
return minuteCounter.getSamples();
}
@Override
public long getMinuteAvg() {
rollCurrentIfNeeded();
return minuteCounter.getAverage();
}
@Override
public long getMinuteRate() {
rollCurrentIfNeeded();
return calcRate(minuteCounter);
}
@Override
public long getTenMinuteSum() {
rollCurrentIfNeeded();
return tenMinuteCounter.getValue();
}
@Override
public long getTenMinuteSamples() {
rollCurrentIfNeeded();
return tenMinuteCounter.getSamples();
}
@Override
public long getTenMinuteAvg() {
rollCurrentIfNeeded();
return tenMinuteCounter.getAverage();
}
@Override
public long getTenMinuteRate() {
rollCurrentIfNeeded();
return calcRate(tenMinuteCounter);
}
@Override
public long getHourSum() {
rollCurrentIfNeeded();
return hourCounter.getValue();
}
@Override
public long getHourSamples() {
rollCurrentIfNeeded();
return hourCounter.getSamples();
}
@Override
public long getHourAvg() {
rollCurrentIfNeeded();
return hourCounter.getAverage();
}
@Override
public long getHourRate() {
rollCurrentIfNeeded();
return calcRate(hourCounter);
}
@Override
public long getAllTimeSum() {
return allTimeCounter.getValue();
}
@Override
public long getAllTimeSamples() {
return allTimeCounter.getSamples();
}
@Override
public long getAllTimeAvg() {
return allTimeCounter.getAverage();
}
@Override
public long getAllTimeRate() {
Duration sinceStart = new Duration(start, new DateTime());
if (sinceStart.getStandardSeconds() == 0) {
return 0;
}
return allTimeCounter.getValue() / sinceStart.getStandardSeconds();
}
/*
* Create a new counter for the next 6 secs and add it to all the
* composite counters.
*/
private GaugeCounter nextCurrentCounter() {
ReadableDateTime now = new DateTime();
GaugeCounter gaugeCounter = gaugeCounterFactory.create(
now, now.toDateTime().plusSeconds(6)
);
allTimeCounter.addEventCounter(gaugeCounter);
hourCounter.addEventCounter(gaugeCounter);
tenMinuteCounter.addEventCounter(gaugeCounter);
minuteCounter.addEventCounter(gaugeCounter);
return gaugeCounter;
}
public MultiWindowGauge merge(MultiWindowGauge rhs) {
return new MultiWindowGauge(
gaugeCounterFactory,
(CompositeGaugeCounter)allTimeCounter.merge(rhs.allTimeCounter),
(CompositeGaugeCounter)hourCounter.merge(rhs.hourCounter),
(CompositeGaugeCounter)tenMinuteCounter.merge(rhs.tenMinuteCounter),
(CompositeGaugeCounter)minuteCounter.merge(rhs.minuteCounter),
start.isBefore(rhs.start) ? start : rhs.start
);
}
}