/* RollingCounter.java
*
* Copyright 2009-2012 Comcast Interactive Media, LLC.
*
* 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 org.fishwife.jrugged;
import java.util.LinkedList;
/**
* Keeps a count of the number of events that have occurred within a given time window.
*/
public class WindowedEventCounter {
/**
* The {@link Clock} to used to determine current time (override for testing).
*/
private Clock clock = new SystemClock();
/**
* Length of the window in milliseconds.
*/
private long windowMillis;
/**
* Storage for the internal queue.
*/
private final LinkedList<Long> queue = new LinkedList<>();
/**
* The maximum count this WindowedEventCounter can hold. Also, the maximum queue size.
* Immutable.
*/
private int capacity;
/**
* Sole constructor.
*
* @param capacity
* maximum count this WindowedEventCounter can hold.
* @param windowMillis
* length of the interest window in milliseconds.
* @throws IllegalArgumentException
* if capacity is less than 1 or if windowMillis is less than 1.
*/
public WindowedEventCounter(final int capacity, final long windowMillis) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}
if (windowMillis <= 0) {
throw new IllegalArgumentException("windowMillis must be greater than 0");
}
this.windowMillis = windowMillis;
this.capacity = capacity;
}
/**
* Record a new event.
*/
public void mark() {
final long currentTimeMillis = clock.currentTimeMillis();
synchronized (queue) {
if (queue.size() == capacity) {
/*
* we're all filled up already, let's dequeue the oldest timestamp to make
* room for this new one.
*/
queue.removeFirst();
}
queue.addLast(currentTimeMillis);
}
}
/**
* Returns a count of in-window events.
*
* @return the the count of in-window events.
*/
public int tally() {
final long currentTimeMillis = clock.currentTimeMillis();
// calculates time for which we remove any errors before
final long removeTimesBeforeMillis = currentTimeMillis - windowMillis;
synchronized (queue) {
// drain out any expired timestamps but don't drain past empty
while (!queue.isEmpty() && queue.peek() < removeTimesBeforeMillis) {
queue.removeFirst();
}
return queue.size();
}
}
/**
* Returns the length of the currently configured event window in milliseconds.
*
* @return <code>long</code>
*/
public long getWindowMillis() {
return windowMillis;
}
/**
* Specifies the maximum capacity of the counter.
*
* @param capacity
* <code>long</code>
* @throws IllegalArgumentException
* if windowMillis is less than 1.
*/
public void setCapacity(final int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("capacity must be greater than 0");
}
synchronized (queue) {
// If the capacity was reduced, we remove oldest elements until the
// queue fits inside the specified capacity
if (capacity < this.capacity) {
while (queue.size() > capacity) {
queue.removeFirst();
}
}
}
this.capacity = capacity;
}
/**
* Returns the maximum capacity this counter can hold.
*
* @return <code>int</code>
*/
public int getCapacity() {
return capacity;
}
/**
* Specifies the length of the interest window in milliseconds.
*
* @param windowMillis
* <code>long</code>
* @throws IllegalArgumentException
* if windowMillis is less than 1.
*/
public void setWindowMillis(final long windowMillis) {
if (windowMillis <= 0) {
throw new IllegalArgumentException("windowMillis must be greater than 0");
}
// changing windowMillis while tally() is draining expired events could
// lead to weirdness, let's lock for this.
synchronized (queue) {
this.windowMillis = windowMillis;
}
}
/**
* Allow the internal {@link Clock} that is used for current time to be overridden
* (for testing).
*
* @param clock
* <code>Clock</code>
*/
protected void setClock(final Clock clock) {
this.clock = clock;
}
}