/*
* 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;
import org.joda.time.ReadableDuration;
import org.testng.Assert;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class TestCompositeSum {
private DateTime base;
private DateTime now;
private EventCounter baseCounter1;
private EventCounter baseCounter2;
private EventCounter baseCounter3;
private EventCounter baseCounter4;
private EventCounter baseCounter5;
@BeforeMethod(alwaysRun = true)
public void setUp() throws Exception {
base = new DateTime("2010-01-01T00:00:00");
now = new DateTime(base);
baseCounter1 = nextEventWindow(1);
baseCounter1.add(1);
baseCounter2 = nextEventWindow(1);
baseCounter2.add(2);
baseCounter3 = nextEventWindow(1);
baseCounter3.add(4);
baseCounter4 = nextEventWindow(1);
baseCounter4.add(8);
baseCounter5 = nextEventWindow(1);
baseCounter5.add(16);
setNow(now);
}
@Test(groups = "fast")
public void testFallOff() throws Exception {
EventCounter firstCounter = nextEventWindow(1);
CompositeEventCounterIf<EventCounter> counter =
newCompositeEventCounter(Duration.standardMinutes(3))
.addEventCounter(firstCounter);
// single element set
counter.add(100);
Assert.assertEquals(firstCounter.getValue(), counter.getValue());
EventCounter middleCounter = nextEventWindow(2);
counter.addEventCounter(middleCounter);
// now has two elements
middleCounter.add(1000);
setNow(middleCounter.getEnd());
Assert.assertEquals(counter.getValue(), 1100);
// add one more
EventCounter lastCounter = nextEventWindow(1);
counter.addEventCounter(lastCounter);
setNow(lastCounter.getEnd().toDateTime().minusMillis(1));
DateTimeUtils.setCurrentMillisFixed(now.getMillis());
Assert.assertEquals(counter.getValue(), 1001);
// test updating last counter
lastCounter.add(200);
Assert.assertEquals(counter.getValue(), 1201);
counter.add(5);
Assert.assertEquals(lastCounter.getValue(), 205);
Assert.assertEquals(counter.getValue(), 1206);
}
@Test(groups = "fast")
public void testNestedComposite1() throws Exception {
CompositeEventCounterIf<EventCounter> counter1 =
newCompositeEventCounter(Duration.standardMinutes(3))
.addEventCounter(baseCounter2)
.addEventCounter(baseCounter3);
CompositeEventCounterIf<EventCounter> counter2 =
newCompositeEventCounter(Duration.standardMinutes(3))
.addEventCounter(baseCounter1)
.addEventCounter(
(EventCounter) counter1
);
// all 3 counters are in counter1's range
setNow(baseCounter3.getEnd());
Assert.assertEquals(counter2.getValue(), 4 + 2 + 1);
// this will push baseCounter1 off
counter1.addEventCounter(baseCounter4);
setNow(baseCounter4.getEnd());
Assert.assertEquals(counter2.getValue(), 8 + 4 + 2);
// this should push counter1 out of counter2
counter2.addEventCounter(baseCounter5);
setNow(baseCounter5.getEnd());
Assert.assertEquals(counter2.getValue(), 16 + 8 + 4);
}
@Test(groups = "fast")
public void testNestedComposite2() throws Exception {
CompositeEventCounterIf<EventCounter> counter1 =
newCompositeEventCounter(Duration.standardMinutes(2))
.addEventCounter(baseCounter2)
.addEventCounter(baseCounter3);
CompositeEventCounterIf<EventCounter> counter2 =
newCompositeEventCounter(Duration.standardMinutes(3))
.addEventCounter(baseCounter1)
.addEventCounter(
(EventCounter) counter1
);
// all 3 counters are in counter1's range
setNow(baseCounter3.getEnd());
Assert.assertEquals(counter1.getValue(), 4 + 2);
Assert.assertEquals(counter2.getValue(), 4 + 2 + 1);
// we lose baseCounter2
counter2.addEventCounter(baseCounter4);
setNow(baseCounter4.getEnd());
Assert.assertEquals(counter1.getValue(), 4);
Assert.assertEquals(counter2.getValue(), 8 + 4);
// counter 1 is empty
counter2.addEventCounter(baseCounter5);
setNow(baseCounter5.getEnd());
Assert.assertEquals(counter1.getValue(), 0);
Assert.assertEquals(counter2.getValue(), 16 + 8);
}
@Test(groups = "fast")
public void testSingleWindow() throws Exception {
EventCounter baseCounter = nextEventWindow(1);
baseCounter.add(100);
CompositeEventCounterIf<EventCounter> counter =
newCompositeEventCounter(new Duration(3 * 60000))
.addEventCounter(baseCounter);
Assert.assertEquals(baseCounter.getValue(), counter.getValue());
baseCounter.add(200);
Assert.assertEquals(baseCounter.getValue(), counter.getValue());
counter.add(100);
Assert.assertEquals(baseCounter.getValue(), counter.getValue());
}
@Test(groups = "fast")
public void testCollapseSanity() throws Exception {
CompositeSum counter =
newCompositeEventCounter(Duration.standardMinutes(60));
DateTime start = new DateTime(base.getMillis());
for (int i = 0; i < 60; i++) {
EventCounter eventCounter = nextEventWindow(1);
eventCounter.add(1);
counter.addEventCounter(eventCounter);
}
setNow(base);
Assert.assertEquals(counter.getValue(), 60);
Assert.assertEquals(counter.getStart(), start);
Assert.assertEquals(counter.getEnd(), start.plusMinutes(60));
}
@Test(groups = "fast")
public void testMergeComposite() throws Exception {
setNow(base);
DateTime start = base;
CompositeSum counter1 =
newCompositeEventCounter(Duration.standardMinutes(60));
CompositeSum counter2 =
newCompositeEventCounter(Duration.standardMinutes(60));
counter1.add(100);
counter2.add(10);
advanceNowMinutes(6);
counter1.addEventCounter(windowMinutes(6));
counter2.addEventCounter(windowMinutes(6));
counter1.add(100);
counter2.add(10);
advanceNowMinutes(6);
counter1.addEventCounter(windowMinutes(6));
counter2.addEventCounter(windowMinutes(6));
counter1.add(100);
counter2.add(10);
EventCounter counter3 = counter1.merge(counter2);
Assert.assertEquals(counter3.getStart(), start);
Assert.assertEquals(
counter3.getEnd(), now.plusMinutes(6)
);
Assert.assertEquals(counter3.getValue(), 330);
}
@Test(groups = "fast")
public void testInterleavedMerge() throws Exception {
setNow(base);
CompositeSum counter1 =
newCompositeEventCounter(Duration.standardMinutes(60));
CompositeSum counter2 =
newCompositeEventCounter(Duration.standardMinutes(60));
counter2.add(1);
advanceNowMinutes(30);
counter1.add(10);
advanceNowMinutes(30);
counter2.add(100);
advanceNowMinutes(5);
counter1.add(1000);
// 31 minutes => 1 hr 36m => |100|1000|10000|
// but 10 drops off
advanceNowMinutes(31);
counter2.add(10000);
EventCounter counter3 = counter1.merge(counter2);
Assert.assertEquals(counter3.getValue(), 11100);
}
@Test(groups = "fast")
public void testAutoCreateNewCounter() throws Exception {
CompositeSum counter =
newCompositeEventCounter(Duration.standardMinutes(10));
// this creates a single internal counter with a value of 1
counter.add(1);
Assert.assertEquals(counter.getValue(), 1);
// advancing time and adding 1 creates a second internal counter
advanceNowMinutes(2);
counter.add(1);
Assert.assertEquals(counter.getValue(), 2);
// this will cause the getValue() to roll off the first counter
advanceNowMinutes(9);
Assert.assertEquals(counter.getValue(), 1);
}
@Test(groups = "fast")
public void testPartialExpiration() throws Exception {
setNow(base);
CompositeSum counter = newCompositeEventCounter(Duration.standardMinutes(10));
for (int i = 0; i < 10; i++) {
counter.add(10);
advanceNowMinutes(1);
}
Assert.assertEquals(counter.getValue(), 100);
advanceNowSeconds(30);
Assert.assertEquals(counter.getValue(), 95);
}
private void advanceNowSeconds(int seconds) {
setNow(now.plusSeconds(seconds));
}
private void advanceNowMinutes(int minutes) {
setNow(now.plusMinutes(minutes));
}
private EventCounter windowMinutes(int minutes) {
return new EventCounterImpl(
now, now.plusMinutes(minutes)
);
}
private CompositeSum newCompositeEventCounter(
ReadableDuration maxLength
) {
return new CompositeSum(maxLength);
}
private EventCounter nextEventWindow(int minutes) {
EventCounter counter = new EventCounterImpl(base, base.plusMinutes(minutes));
base = base.plusMinutes(minutes);
return counter;
}
private void setNow(ReadableDateTime value) {
DateTimeUtils.setCurrentMillisFixed(value.getMillis());
now = new DateTime(value);
}
}