All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opensearch.migrations.replay.traffic.expiration.ExpiringKeyQueue Maven / Gradle / Ivy

package org.opensearch.migrations.replay.traffic.expiration;

import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListMap;

import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;

/**
 * This is a sequence of (concurrent) hashmaps segmented by time.  Each element in the sequence is
 * composed of a timestamp and a map.  The timestamp at each element is guaranteed to be greater
 * than all items within all maps that preceded it.
 */
@Slf4j
@EqualsAndHashCode(callSuper = true)
class ExpiringKeyQueue extends ConcurrentSkipListMap> {
    private final Duration granularity;
    private final String partitionId;

    ExpiringKeyQueue(Duration granularity, String partitionId, EpochMillis startingTimestamp) {
        this.granularity = granularity;
        this.partitionId = partitionId;
        addNewSet(startingTimestamp);
    }

    public Instant getLatestPossibleKeyValue() {
        return lastKey().toInstant().plus(granularity);
    }

    private ConcurrentHashMap addNewSet(EpochMillis timestampMillis) {
        var accumulatorMap = new ConcurrentHashMap();
        this.put(timestampMillis, accumulatorMap);
        return accumulatorMap;
    }

    /**
     * Returns null if the requested timestamp is in the expired range of timestamps,
     * otherwise this returns the appropriate bucket.  It either finds it within the map
     * or creates a new one and inserts it into the map (atomically).
     *
     * @param timestamp
     * @return
     */
    ConcurrentHashMap getHashSetForTimestamp(EpochMillis timestamp, Runnable onNewBucketCreated) {
        return Optional.ofNullable(this.floorEntry(timestamp)).map(kvp -> {
            var shiftedKey = kvp.getKey().toInstant().plus(granularity);
            if (timestamp.test(shiftedKey, (newTimestamp, computedFloor) -> newTimestamp >= computedFloor)) {
                try {
                    return createNewSlot(timestamp, kvp.getKey());
                } finally {
                    onNewBucketCreated.run();
                }
            }
            return kvp.getValue();
        }).orElse(null); // floorEntry could be null if the entry was too old
    }

    /**
     * We don't want to have a race condition where many nearby keys could be created.  This could happen
     * if many requests come in with slightly different timestamps, but were being processed before the
     * new bucket was created.  Since we're dealing with a map, the simplest way around this is to reduce,
     * or quantize, the range of potential keys so that each key will uniquely identify an entire range.
     * It should be impossible for two keys to have any overlap over the granularity window.
     * 

* That allows us to put a new entry ONLY IF it isn't already there, which results in a uniqueness * invariant that makes a lot of other things easier to reason with. */ private ConcurrentHashMap createNewSlot(EpochMillis timestamp, EpochMillis referenceKey) { var granularityMs = granularity.toMillis(); var quantizedDifference = (timestamp.millis - referenceKey.millis) / granularityMs; var newKey = referenceKey.millis + (quantizedDifference * granularityMs); var newMap = new ConcurrentHashMap(); var priorMap = putIfAbsent(new EpochMillis(newKey), newMap); return priorMap == null ? newMap : priorMap; } void expireOldSlots( AccumulatorMap connectionAccumulatorMap, BehavioralPolicy behavioralPolicy, Duration minimumGuaranteedLifetime, EpochMillis largestCurrentObservedTimestamp ) { var startOfWindow = new EpochMillis( largestCurrentObservedTimestamp.toInstant().minus(minimumGuaranteedLifetime) ); for (var kvp = firstEntry(); kvp.getKey() .test(startOfWindow, (first, windowStart) -> first < windowStart); kvp = firstEntry()) { expireItemsBefore(connectionAccumulatorMap, behavioralPolicy, kvp.getValue(), startOfWindow); remove(kvp.getKey()); } } private void expireItemsBefore( AccumulatorMap connectionAccumulatorMap, BehavioralPolicy behavioralPolicy, ConcurrentHashMap keyMap, EpochMillis earlierTimesToPreserve ) { log.debug("Expiring entries before " + earlierTimesToPreserve); for (var connectionId : keyMap.keySet()) { var key = new ScopedConnectionIdKey(partitionId, connectionId); var accumulation = connectionAccumulatorMap.get(key); if (accumulation != null && accumulation.getNewestPacketTimestampInMillisReference().get() < earlierTimesToPreserve.millis) { var priorValue = connectionAccumulatorMap.remove(key); if (priorValue != null) { priorValue.expire(); behavioralPolicy.onExpireAccumulation(partitionId, accumulation); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy