![JAR search and dependency download from the Maven repository](/logo.png)
io.pcp.parfait.TimeWindowCounter Maven / Gradle / Ivy
Show all versions of parfait-core Show documentation
/*
* Copyright 2009-2017 Aconex
*
* 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 io.pcp.parfait;
import java.util.Arrays;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
/**
*
* Counter which keeps track of the increments only over a particular
* {@link TimeWindow}, expiring old increments after the window has elapsed. For
* example, a period of Uses the resolution of the supplied TimeWindow to group
* events together for efficiency. For example, when supplied with a TimeWindow
* with a period of 60 seconds and a resolution of 5 seconds, 12 'buckets' will
* be created to keep count of events. This means that as each bucket is
* overwritten after 60 seconds, the total count may be understated by as much
* as (5/60) ≅ 8%. This, however, means that the memory footprint is
* approximately that of just the 12 counters, rather than having to track the
* time of each individual event.
*
*
* Currently uses very coarse-grained locking (each {@link #get()} or
* {@link #inc(long)} takes and holds a shared lock for the duration); this may
* prove too contentious and require change later.
*
*/
@ThreadSafe
public class TimeWindowCounter implements Counter {
@GuardedBy("lock")
private long overallValue;
@GuardedBy("lock")
private final long[] interimValues;
@GuardedBy("lock")
private int headIndex = 0;
@GuardedBy("lock")
private long headTime;
private final Object lock = new Object();
private final Supplier timeSource;
private final TimeWindow window;
public TimeWindowCounter(TimeWindow window) {
this(window, new SystemTimePoller());
}
@VisibleForTesting
TimeWindowCounter(TimeWindow window, Supplier timeSource) {
this.window = window;
this.interimValues = new long[(int) (window.getBuckets())];
this.timeSource = timeSource;
this.headTime = timeSource.get();
}
@Override
public void inc(long increment) {
synchronized (lock) {
cleanState();
overallValue += increment;
interimValues[headIndex] += increment;
}
}
/**
* Clean out old data from the buckets, getting us ready to enter a new
* bucket. interimValues, headTime, and headIndex comprise a circular buffer
* of the last n sub-values, and the start time of the head bucket. On each
* write or get, we progressively clear out entries in the circular buffer
* until headTime is within one 'tick' of the current time; we have then
* found the correct bucket.
*/
@GuardedBy("lock")
private void cleanState() {
long eventTime = timeSource.get();
long bucketsToSkip = (eventTime - headTime) / window.getResolution();
while (bucketsToSkip > 0) {
headIndex = (headIndex + 1) % interimValues.length;
bucketsToSkip--;
overallValue -= interimValues[headIndex];
interimValues[headIndex] = 0L;
headTime += window.getResolution();
}
}
public Long get() {
synchronized (lock) {
cleanState();
return overallValue;
}
}
@Override
public void inc() {
inc(1L);
}
@Override
public String toString() {
return String.format("last %s=%s", window.getName(), overallValue);
}
@VisibleForTesting
String counterState() {
synchronized (lock) {
return Arrays.toString(interimValues);
}
}
}