com.facebook.stats.AbstractCompositeCounter.java Source code

Java tutorial

Introduction

Here is the source code for com.facebook.stats.AbstractCompositeCounter.java

Source

/*
 * 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),
                String.format("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,
                String.format("%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;
    }
}