001package io.prometheus.metrics.core.metrics;
002
003import java.lang.reflect.Array;
004import java.util.concurrent.TimeUnit;
005import java.util.function.LongSupplier;
006import java.util.function.ObjDoubleConsumer;
007import java.util.function.Supplier;
008
009/**
010 * Maintains a ring buffer of T to implement a sliding time window.
011 * <p>
012 * This is used to maintain a sliding window of {@link CKMSQuantiles} for {@link Summary} metrics.
013 * <p>
014 * It is implemented in a generic way so that 3rd party libraries can use it for implementing sliding windows.
015 * <p>
016 * TODO: The current implementation is {@code synchronized}. There is likely room for optimization.
017 */
018public class SlidingWindow<T> {
019
020    private final Supplier<T> constructor;
021    private final ObjDoubleConsumer<T> observeFunction;
022    private final T[] ringBuffer;
023    private int currentBucket;
024    private long lastRotateTimestampMillis;
025    private final long durationBetweenRotatesMillis;
026    LongSupplier currentTimeMillis = System::currentTimeMillis; // to be replaced in unit tests
027
028    /**
029     * Example: If the {@code maxAgeSeconds} is 60 and {@code ageBuckets} is 3, then 3 instances of {@code T}
030     * are maintained and the sliding window moves to the next instance of T every 20 seconds.
031     *
032     * @param clazz type of T
033     * @param constructor for creating a new instance of T as the old one gets evicted
034     * @param observeFunction for observing a value (e.g. calling {@code t.observe(value)}
035     * @param maxAgeSeconds after this amount of time an instance of T gets evicted.
036     * @param ageBuckets number of age buckets.
037     */
038    public SlidingWindow(Class<T> clazz, Supplier<T> constructor, ObjDoubleConsumer<T> observeFunction, long maxAgeSeconds, int ageBuckets) {
039        this.constructor = constructor;
040        this.observeFunction = observeFunction;
041        this.ringBuffer = (T[]) Array.newInstance(clazz, ageBuckets);
042        for (int i = 0; i < ringBuffer.length; i++) {
043            this.ringBuffer[i] = constructor.get();
044        }
045        this.currentBucket = 0;
046        this.lastRotateTimestampMillis = System.currentTimeMillis();
047        this.durationBetweenRotatesMillis = TimeUnit.SECONDS.toMillis(maxAgeSeconds) / ageBuckets;
048    }
049
050    /**
051     * Get the currently active instance of {@code T}.
052     */
053    public synchronized T current() {
054        return rotate();
055    }
056
057    /**
058     * Observe a value.
059     */
060    public synchronized void observe(double value) {
061        rotate();
062        for (T t : ringBuffer) {
063            observeFunction.accept(t, value);
064        }
065    }
066
067    private T rotate() {
068        long timeSinceLastRotateMillis = currentTimeMillis.getAsLong() - lastRotateTimestampMillis;
069        while (timeSinceLastRotateMillis > durationBetweenRotatesMillis) {
070            ringBuffer[currentBucket] = constructor.get();
071            if (++currentBucket >= ringBuffer.length) {
072                currentBucket = 0;
073            }
074            timeSinceLastRotateMillis -= durationBetweenRotatesMillis;
075            lastRotateTimestampMillis += durationBetweenRotatesMillis;
076        }
077        return ringBuffer[currentBucket];
078    }
079}