/*
* 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 MultiWindowRate implements ReadableMultiWindowRate, WritableMultiWindowStat {
private static final int DEFAULT_TIME_BUCKET_SIZE_MILLIS = 6000; // 6 seconds
// all-time counter is a windowed counter that is effectively unbounded
private final CompositeSum allTimeCounter;
private final CompositeSum hourCounter;
private final CompositeSum tenMinuteCounter;
private final CompositeSum minuteCounter;
private final EventRate hourRate;
private final EventRate tenMinuteRate;
private final EventRate minuteRate;
private final ReadableDateTime start;
private final Object rollLock = new Object();
private final int timeBucketSizeMillis;
private volatile EventCounterIf<EventCounter> currentCounter;
MultiWindowRate(int timeBucketSizeMillis) {
this(
newCompositeEventCounter(Integer.MAX_VALUE),
newCompositeEventCounter(60),
newCompositeEventCounter(10),
newCompositeEventCounter(1),
new DateTime(),
timeBucketSizeMillis
);
}
MultiWindowRate(
CompositeSum allTimeCounter,
CompositeSum hourCounter,
CompositeSum tenMinuteCounter,
CompositeSum minuteCounter,
ReadableDateTime start,
int timeBucketSizeMillis
) {
this.allTimeCounter = allTimeCounter;
this.hourCounter = hourCounter;
this.tenMinuteCounter = tenMinuteCounter;
this.minuteCounter = minuteCounter;
this.start = start;
this.timeBucketSizeMillis = timeBucketSizeMillis;
hourRate =
newEventRate(hourCounter, Duration.standardMinutes(60), start);
tenMinuteRate =
newEventRate(tenMinuteCounter, Duration.standardMinutes(10), start);
minuteRate =
newEventRate(minuteCounter, Duration.standardMinutes(1), start);
currentCounter = nextCurrentCounter(start.toDateTime());
}
public MultiWindowRate() {
this(DEFAULT_TIME_BUCKET_SIZE_MILLIS);
}
private static CompositeSum newCompositeEventCounter(int minutes) {
return new CompositeSum(Duration.standardMinutes(minutes));
}
private EventRate newEventRate(
EventCounterIf<EventCounter> counter, Duration windowSize, ReadableDateTime start
) {
return new EventRateImpl(counter, windowSize, start);
}
@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(new DateTime(now));
}
}
}
}
@Override
public long getMinuteSum() {
rollCurrentIfNeeded();
return minuteCounter.getValue();
}
@Override
public long getMinuteRate() {
rollCurrentIfNeeded();
return minuteRate.getValue();
}
@Override
public long getTenMinuteSum() {
rollCurrentIfNeeded();
return tenMinuteCounter.getValue();
}
@Override
public long getTenMinuteRate() {
rollCurrentIfNeeded();
return tenMinuteRate.getValue();
}
@Override
public long getHourSum() {
rollCurrentIfNeeded();
return hourCounter.getValue();
}
@Override
public long getHourRate() {
rollCurrentIfNeeded();
return hourRate.getValue();
}
@Override
public long getAllTimeSum() {
return allTimeCounter.getValue();
}
@Override
public long getAllTimeRate() {
Duration sinceStart = new Duration(start, getNow());
if (sinceStart.getStandardSeconds() == 0) {
return 0;
}
return allTimeCounter.getValue() / sinceStart.getStandardSeconds();
}
protected ReadableDateTime getNow() {
return new DateTime();
}
// current
private EventCounterIf<EventCounter> nextCurrentCounter(ReadableDateTime now) {
EventCounter eventCounter =
new EventCounterImpl(now, now.toDateTime().plusMillis(timeBucketSizeMillis));
allTimeCounter.addEventCounter(eventCounter);
hourCounter.addEventCounter(eventCounter);
tenMinuteCounter.addEventCounter(eventCounter);
minuteCounter.addEventCounter(eventCounter);
return eventCounter;
}
public MultiWindowRate merge(MultiWindowRate rate) {
return new MultiWindowRate(
(CompositeSum)allTimeCounter.merge(rate.allTimeCounter),
(CompositeSum)hourCounter.merge(rate.hourCounter),
(CompositeSum)tenMinuteCounter.merge(rate.tenMinuteCounter),
(CompositeSum)minuteCounter.merge(rate.minuteCounter),
start.isBefore(rate.start) ? start : rate.start,
timeBucketSizeMillis
);
}
}