/* * 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 com.facebook.collections.PeekableIterator; import com.facebook.stats.mx.StatsUtil; import com.google.common.base.Preconditions; import com.google.common.collect.Iterators; import org.joda.time.DateTime; import org.joda.time.Duration; import org.joda.time.ReadableDateTime; import org.joda.time.ReadableDuration; import javax.annotation.concurrent.GuardedBy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.List; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; /** * Tracks stats over a rolling time period (time window) of maxLength * broken into parts (eventCounters list) of size maxChunkLength. * Meant to be subclassed. Primary use is through repeatedly calling * add() and occasionally calling getValue(). * <p/> * 1. Does trimming of event buckets (based on the window size) * 2. Allows for updates of this window's events based on updates to * component windows (useful for overlapping window stats) * <p/> * Optimized for write-heavy counters. */ public abstract class AbstractCompositeCounter<C extends EventCounterIf<C>> implements CompositeEventCounterIf<C> { // adds/removes to eventCounters happen only when synchronized on "this" @GuardedBy("this") private final Deque<C> eventCounters = new ArrayDeque<C>(); private final ReadableDuration maxLength; // total window size private final ReadableDuration maxChunkLength; // size per counter private ReadableDateTime start; private ReadableDateTime end; /* * Create a CompositeCounter of window size maxLength broken into * individual eventCounters of size maxChunkLength. */ public AbstractCompositeCounter( ReadableDuration maxLength, ReadableDuration maxChunkLength ) { this.maxLength = maxLength; this.maxChunkLength = maxChunkLength; DateTime now = new DateTime(); start = now; end = now; } public AbstractCompositeCounter(ReadableDuration maxLength) { this(maxLength, new Duration(maxLength.getMillis() / 10)); } /** * Create a new counter that is the result of merging this counter and the argument. No deep * copy is performed, so the resulting copy could in theory be a counter that just lists * this and counter in a list * * @param counter : other counter to use in merge * @return */ @Override public abstract C merge(C counter); /** * calldd when a new counter is needed for the range [start, end) * * @param start * @param end * @return new counter for range [start, end) to second resolution */ protected abstract C nextCounter(ReadableDateTime start, ReadableDateTime end); /** * Adds the value to the counter, and may create a new eventCounter * to store the value if needed. */ @Override public void add(long delta) { DateTime now = new DateTime(); C last; synchronized (this) { if (eventCounters.isEmpty() || !now.isBefore(eventCounters.getLast().getEnd())) { addEventCounter(nextCounter(now, now.plus(maxChunkLength))); } last = eventCounters.getLast(); } last.add(delta); } public ReadableDateTime getStart() { trimIfNeeded(); return start; } public ReadableDateTime getEnd() { trimIfNeeded(); return end; } @Override public Duration getLength() { trimIfNeeded(); return new Duration(start, end); } @Override public synchronized CompositeEventCounterIf<C> add( long delta, ReadableDateTime start, ReadableDateTime end ) { C counter = nextCounter(start, end); counter.add(delta); return addEventCounter(counter); } @Override public synchronized CompositeEventCounterIf<C> addEventCounter(C eventCounter) { if (eventCounters.size() >= 2) { mergeChunksIfNeeded(); } // merge above before adding the counter; the invariant is that the // added counter should not be merged until it is not the most recent // counter Preconditions.checkArgument( eventCounters.isEmpty() || !eventCounters.getLast().getEnd().isAfter(eventCounter.getEnd()), "new counter end , %s, is not past the current end %s", eventCounter.getEnd(), eventCounters.isEmpty() ? "NaN" : eventCounters.getLast().getEnd() ); eventCounters.add(eventCounter); if (eventCounter.getStart().isBefore(start)) { start = eventCounter.getStart(); trimIfNeeded(); } if (eventCounter.getEnd().isAfter(end)) { end = eventCounter.getEnd(); trimIfNeeded(); } return this; } /** * testing to see if we can merge counter1 and counter2 and not violate * the maxChunkLength * <p/> * ...| counter2 | counter1 | */ private void mergeChunksIfNeeded() { C counter1 = eventCounters.removeLast(); C counter2 = eventCounters.getLast(); if (StatsUtil.extentOf(counter1, counter2).isLongerThan(maxChunkLength)) { eventCounters.add(counter1); } else { eventCounters.removeLast(); eventCounters.add(counter1.merge(counter2)); } } /** * This merges another sorted list of counters with our own and produces * a new counter. * <p/> * our own counters are protected from mutation via synchronization. The behavior of this function * is not defined if otherCounters changes while a merge is taking place; * * @param otherCounters usually some other object's counters, or a single counter that's being added * via addEventCounter() * @param mergedCounter * @param <C2> * @return */ protected synchronized <C2 extends CompositeEventCounterIf<C>> C2 internalMerge( Collection<? extends C> otherCounters, C2 mergedCounter ) { PeekableIterator<C> iter1 = new PeekableIterator<C>(eventCounters.iterator()); PeekableIterator<C> iter2 = new PeekableIterator<C>(otherCounters.iterator()); while (iter1.hasNext() || iter2.hasNext()) { if (iter1.hasNext() && iter2.hasNext()) { // take the counter that occurs first and merge it if (iter1.peekNext().getStart().isBefore(iter2.peekNext().getStart())) { mergedCounter.addEventCounter(iter1.next()); } else { mergedCounter.addEventCounter(iter2.next()); } } else if (iter1.hasNext()) { mergedCounter.addEventCounter(iter1.next()); } else if (iter2.hasNext()) { mergedCounter.addEventCounter(iter2.next()); } } return mergedCounter; } /** * Updates the current composite counter so that it is up to date with the * current timestamp. * <p/> * This should be called by any method that needs to have the most updated * view of the current set of counters. */ protected synchronized void trimIfNeeded() { Duration delta = new Duration(start, new DateTime()) .minus(maxLength); if (delta.isLongerThan(Duration.ZERO)) { start = start.toDateTime().plus(delta); if (start.isAfter(end)) { end = start; } Iterator<C> iter = eventCounters.iterator(); while (iter.hasNext()) { EventCounterIf<C> counter = iter.next(); // trim any counter with an end up to and including start since our composite counter is // [start, ... and each counter is [..., end) if (!start.isBefore(counter.getEnd())) { iter.remove(); } else { break; } } } } /** * Takes the oldest counter and returns the fraction [0, 1] of it that * has extended outside the current time window of the composite counter. * <p/> * Assumes: * counter.getEnd() >= window.getStart() * counter.getStart() < window.getStart() * * @param oldestCounter * @return fraction [0, 1] */ protected float getExpiredFraction(EventCounterIf<C> oldestCounter) { ReadableDateTime windowStart = getWindowStart(); //counter.getEnd() >= window.getStart() checkArgument( !oldestCounter.getEnd().isBefore(windowStart), "counter should have end %s >= window start %s", oldestCounter.getEnd(), windowStart ); ReadableDateTime counterStart = oldestCounter.getStart(); //counter.getstart() < window.getStart() checkArgument( counterStart.isBefore(windowStart), "counter should have start %s <= window start %s", counterStart, windowStart ); // long expiredPortionMillis = windowStart.getMillis() - counterStart.getMillis(); long lengthMillis = oldestCounter.getEnd().getMillis() - counterStart.getMillis(); float expiredFraction = expiredPortionMillis / (float) lengthMillis; checkState( expiredFraction >= 0 && expiredFraction <= 1.0, "%s not in [0, 1]", expiredFraction ); return expiredFraction; } /** * return a copy of current list of event counters; same properties as getEventCounters, but * a copy * * @deprecated see {@link #getEventCounters()} and make a copy externally if a snapshot is needed */ @Deprecated protected synchronized List<C> getEventCountersCopy() { return new ArrayList<C>(eventCounters); } /** * Get a the current set of event counters. The counters will be * sorted in ascending order according to time, meaning the earliest counter will appear first * in any iteration * * @return unmodifiable Collection of event counters */ protected synchronized Collection<C> getEventCounters() { return Collections.unmodifiableCollection(eventCounters); } /** * Returns the most recently added counter or null if does not exist * * @return EventCounter */ protected synchronized C getMostRecentCounter() { return eventCounters.peekLast(); } /** * @return Unmodifiable iterator across windowed event counters in ascending * (oldest first) order */ protected Iterator<C> eventCounterIterator() { return Iterators.unmodifiableIterator(eventCounters.iterator()); } protected ReadableDateTime getWindowStart() { return start; } protected ReadableDateTime getWindowEnd() { return end; } protected ReadableDuration getMaxLength() { return maxLength; } protected ReadableDuration getMaxChunkLength() { return maxChunkLength; } }