From 0515a357c6a5e1cd225d17447b3af317817d2f62 Mon Sep 17 00:00:00 2001 From: Kaituo Li Date: Thu, 25 Feb 2021 10:05:01 -0800 Subject: [PATCH] Extract a new class for entity frequenty tracking (#389) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Extrac a new class for entity frequenty tracking HC detectors use a 1-pass algorithm for estimating heavy hitters in a stream. Our method maintains a time-decayed count for each entity, which allows us to compare the frequencies of entities from different detectors in the stream. To reuse the code in historical detectors, I created a new class PriorityTracker and moved all related logic there.  When an entity is hit, the caller can call PriorityTracker.updatePriority to update the entity's priority.  The callers can find the most frequently occurring entities in the stream using PriorityTracker.getTopNEntities. This PR also adds tests for NodeStateManager. Testing done: 1. manually tested basic workflow of HC detectors still works. 2. added new tests for PriorityTracker. --- build.gradle | 1 - docs/entity-priority.pdf | Bin 0 -> 61652 bytes .../ad/caching/CacheBuffer.java | 189 ++-------- .../ad/caching/PriorityCache.java | 19 +- .../ad/caching/PriorityTracker.java | 349 ++++++++++++++++++ .../ad/NodeStateManagerTests.java | 31 ++ .../ad/caching/CacheBufferTests.java | 14 +- .../ad/caching/PriorityCacheTests.java | 2 +- .../ad/caching/PriorityTrackerTests.java | 97 +++++ 9 files changed, 532 insertions(+), 170 deletions(-) create mode 100644 docs/entity-priority.pdf create mode 100644 src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTracker.java create mode 100644 src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTrackerTests.java diff --git a/build.gradle b/build.gradle index b4b5164a..f293bb00 100644 --- a/build.gradle +++ b/build.gradle @@ -309,7 +309,6 @@ List jacocoExclusions = [ 'com.amazon.opendistroforelasticsearch.ad.transport.SearchAnomalyDetectorInfoTransportAction*', // TODO: hc caused coverage to drop - 'com.amazon.opendistroforelasticsearch.ad.NodeStateManager', 'com.amazon.opendistroforelasticsearch.ad.indices.AnomalyDetectionIndices', 'com.amazon.opendistroforelasticsearch.ad.transport.handler.MultiEntityResultHandler', 'com.amazon.opendistroforelasticsearch.ad.util.ThrowingSupplierWrapper', diff --git a/docs/entity-priority.pdf b/docs/entity-priority.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9de6cfc776664d76593b7fcbf2f3cbbfa32e77d7 GIT binary patch literal 61652 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 literal 0 HcmV?d00001 diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBuffer.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBuffer.java index 821095d9..2f286555 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBuffer.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBuffer.java @@ -18,18 +18,14 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.Comparator; import java.util.List; import java.util.Map.Entry; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentSkipListSet; import java.util.stream.Collectors; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; -import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -37,7 +33,6 @@ import com.amazon.opendistroforelasticsearch.ad.MaintenanceState; import com.amazon.opendistroforelasticsearch.ad.MemoryTracker; import com.amazon.opendistroforelasticsearch.ad.MemoryTracker.Origin; -import com.amazon.opendistroforelasticsearch.ad.annotation.Generated; import com.amazon.opendistroforelasticsearch.ad.ml.CheckpointDao; import com.amazon.opendistroforelasticsearch.ad.ml.EntityModel; import com.amazon.opendistroforelasticsearch.ad.ml.ModelState; @@ -64,90 +59,22 @@ public class CacheBuffer implements ExpiringState, MaintenanceState { private static final Logger LOG = LogManager.getLogger(CacheBuffer.class); - static class PriorityNode { - private String key; - private float priority; - - PriorityNode(String key, float priority) { - this.priority = priority; - this.key = key; - } - - @Generated - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - if (obj instanceof PriorityNode) { - PriorityNode other = (PriorityNode) obj; - - EqualsBuilder equalsBuilder = new EqualsBuilder(); - equalsBuilder.append(key, other.key); - return equalsBuilder.isEquals(); - } - return false; - } - - @Generated - @Override - public int hashCode() { - return new HashCodeBuilder().append(key).toHashCode(); - } - - @Generated - @Override - public String toString() { - ToStringBuilder builder = new ToStringBuilder(this); - builder.append("key", key); - builder.append("priority", priority); - return builder.toString(); - } - } - - static class PriorityNodeComparator implements Comparator { - - @Override - public int compare(PriorityNode priority, PriorityNode priority2) { - int equality = priority.key.compareTo(priority2.key); - if (equality == 0) { - // this is consistent with PriorityNode's equals method - return 0; - } - // if not equal, first check priority - int cmp = Float.compare(priority.priority, priority2.priority); - if (cmp == 0) { - // if priority is equal, use lexicographical order of key - cmp = equality; - } - return cmp; - } - } + // max entities to track per detector + private final int MAX_TRACKING_ENTITIES = 1000000; private final int minimumCapacity; - // key -> Priority node - private final ConcurrentHashMap key2Priority; - private final ConcurrentSkipListSet priorityList; // key -> value private final ConcurrentHashMap> items; - // when detector is created.  Can be reset.  Unit: seconds - private long landmarkSecs; - // length of seconds in one interval.  Used to compute elapsed periods - // since the detector has been enabled. - private long intervalSecs; // memory consumption per entity private final long memoryConsumptionPerEntity; private final MemoryTracker memoryTracker; - private final Clock clock; private final CheckpointDao checkpointDao; private final Duration modelTtl; private final String detectorId; private Instant lastUsedTime; - private final int DECAY_CONSTANT; private final long reservedBytes; + private final PriorityTracker priorityTracker; + private final Clock clock; public CacheBuffer( int minimumCapacity, @@ -163,20 +90,20 @@ public CacheBuffer( throw new IllegalArgumentException("minimum capacity should be larger than 0"); } this.minimumCapacity = minimumCapacity; - this.key2Priority = new ConcurrentHashMap<>(); - this.priorityList = new ConcurrentSkipListSet<>(new PriorityNodeComparator()); + this.items = new ConcurrentHashMap<>(); - this.landmarkSecs = clock.instant().getEpochSecond(); - this.intervalSecs = intervalSecs; + this.memoryConsumptionPerEntity = memoryConsumptionPerEntity; this.memoryTracker = memoryTracker; - this.clock = clock; + this.checkpointDao = checkpointDao; this.modelTtl = modelTtl; this.detectorId = detectorId; this.lastUsedTime = clock.instant(); - this.DECAY_CONSTANT = 3; + this.reservedBytes = memoryConsumptionPerEntity * minimumCapacity; + this.clock = clock; + this.priorityTracker = new PriorityTracker(clock, intervalSecs, clock.instant().getEpochSecond(), MAX_TRACKING_ENTITIES); } /** @@ -186,50 +113,13 @@ public CacheBuffer( * @param entityModelId model Id */ private void update(String entityModelId) { - PriorityNode node = key2Priority.computeIfAbsent(entityModelId, k -> new PriorityNode(entityModelId, 0f)); - // reposition this node - this.priorityList.remove(node); - node.priority = getUpdatedPriority(node.priority); - this.priorityList.add(node); + priorityTracker.updatePriority(entityModelId); Instant now = clock.instant(); items.get(entityModelId).setLastUsedTime(now); lastUsedTime = now; } - public float getUpdatedPriority(float oldPriority) { - long increment = computeWeightedCountIncrement(); - // if overflowed, we take the short cut from now on - oldPriority += Math.log(1 + Math.exp(increment - oldPriority)); - // if overflow happens, using \log(g(t_k-L)) instead. - if (oldPriority == Float.POSITIVE_INFINITY) { - oldPriority = increment; - } - return oldPriority; - } - - /** - * Compute periods relative to landmark and the weighted count increment using 0.125n. - * Multiply by 0.125 is implemented using right shift for efficiency. - * @return the weighted count increment used in the priority update step. - */ - private long computeWeightedCountIncrement() { - long periods = (clock.instant().getEpochSecond() - landmarkSecs) / intervalSecs; - return periods >> DECAY_CONSTANT; - } - - /** - * Compute the weighted total count by considering landmark - * \log(C)=\log(\sum_{i=1}^{n} (g(t_i-L)/g(t-L)))=\log(\sum_{i=1}^{n} (g(t_i-L))-\log(g(t-L)) - * @return the minimum priority entity's ID and priority - */ - public Entry getMinimumPriority() { - PriorityNode smallest = priorityList.first(); - long periods = (clock.instant().getEpochSecond() - landmarkSecs) / intervalSecs; - float detectorWeight = periods >> DECAY_CONSTANT; - return new SimpleImmutableEntry<>(smallest.key, smallest.priority - detectorWeight); - } - /** * Insert the model state associated with a model Id to the cache * @param entityModelId the model Id @@ -257,9 +147,7 @@ public void put(String entityModelId, ModelState value) { private void put(String entityModelId, ModelState value, float priority) { ModelState contentNode = items.get(entityModelId); if (contentNode == null) { - PriorityNode node = new PriorityNode(entityModelId, priority); - key2Priority.put(entityModelId, node); - priorityList.add(node); + priorityTracker.addPriority(entityModelId, priority); items.put(entityModelId, value); Instant now = clock.instant(); value.setLastUsedTime(now); @@ -319,9 +207,9 @@ public ModelState remove() { // The removed one loses references and soon GC will collect it. // We have memory tracking correction to fix incorrect memory usage record. // put: not a problem as it is unlikely we are removing and putting the same thing - PriorityNode smallest = priorityList.first(); - if (smallest != null) { - return remove(smallest.key); + Optional key = priorityTracker.getMinimumPriorityEntityId(); + if (key.isPresent()) { + return remove(key.get()); } return null; } @@ -334,12 +222,11 @@ public ModelState remove() { * is no associated ModelState for the key */ public ModelState remove(String keyToRemove) { - // remove if the key matches; priority does not matter - priorityList.remove(new PriorityNode(keyToRemove, 0)); + priorityTracker.removePriority(keyToRemove); + // if shared cache is empty, we are using reserved memory boolean reserved = sharedCacheEmpty(); - key2Priority.remove(keyToRemove); ModelState valueRemoved = items.remove(keyToRemove); if (valueRemoved != null) { @@ -382,15 +269,17 @@ public long getMemoryConsumptionPerEntity() { /** * - * If the cache is not full, check if some other items can replace internal entities. + * If the cache is not full, check if some other items can replace internal entities + * within the same detector. + * * @param priority another entity's priority * @return whether one entity can be replaced by another entity with a certain priority */ - public boolean canReplace(float priority) { + public boolean canReplaceWithinDetector(float priority) { if (items.isEmpty()) { return false; } - Entry minPriorityItem = getMinimumPriority(); + Entry minPriorityItem = priorityTracker.getMinimumPriority(); return minPriorityItem != null && priority > minPriorityItem.getValue(); } @@ -415,15 +304,6 @@ public void maintenance() { ModelState modelState = entry.getValue(); Instant now = clock.instant(); - // we can have ConcurrentModificationException when serializing - // and updating rcf model at the same time. To prevent this, - // we need to have a deep copy of models or have a lock. Both - // options are costly. - // As we are gonna retry serializing either when the entity is - // evicted out of cache or during the next maintenance period, - // don't do anything when the exception happens. - checkpointDao.write(modelState, entityModelId); - if (modelState.getLastUsedTime().plus(modelTtl).isBefore(now)) { // race conditions can happen between the put and one of the following operations: // remove: not a problem as all of the data structures are concurrent. @@ -433,7 +313,17 @@ public void maintenance() { // We have memory tracking correction to fix incorrect memory usage record. // put: not a problem as we are unlikely to maintain an entry that's not // already in the cache + // remove method saves checkpoint as well remove(entityModelId); + } else { + // we can have ConcurrentModificationException when serializing + // and updating rcf model at the same time. To prevent this, + // we need to have a deep copy of models or have a lock. Both + // options are costly. + // As we are gonna retry serializing either when the entity is + // evicted out of cache or during the next maintenance period, + // don't do anything when the exception happens. + checkpointDao.write(modelState, entityModelId); } } catch (Exception e) { LOG.warn("Failed to finish maintenance for model id " + entityModelId, e); @@ -471,14 +361,6 @@ public long getLastUsedTime(String entityModelId) { return -1; } - /** - * - * @return Get the model of highest priority entity - */ - public Optional getHighestPriorityEntityModelId() { - return Optional.of(priorityList).map(list -> list.last()).map(node -> node.key); - } - /** * * @param entityModelId entity Id @@ -501,8 +383,7 @@ public void clear() { memoryTracker.releaseMemory(getBytesInSharedCache(), false, Origin.MULTI_ENTITY_DETECTOR); } items.clear(); - key2Priority.clear(); - priorityList.clear(); + priorityTracker.clearPriority(); } /** @@ -561,4 +442,8 @@ public String getDetectorId() { public List> getAllModels() { return items.values().stream().collect(Collectors.toList()); } + + public PriorityTracker getPriorityTracker() { + return priorityTracker; + } } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCache.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCache.java index c6a81165..368352df 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCache.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCache.java @@ -187,7 +187,7 @@ public ModelState get(String modelId, AnomalyDetector detector, dou if (state != null) { priority = state.getPriority(); } - priority = buffer.getUpdatedPriority(priority); + priority = buffer.getPriorityTracker().getUpdatedPriority(priority); // update state using new priority or create a new one if (state != null) { @@ -254,7 +254,7 @@ private boolean hostIfPossible( // it is fine we exceed a little. We have regular maintenance to remove // extra memory usage. buffer.put(modelId, state); - } else if (buffer.canReplace(priority)) { + } else if (buffer.canReplaceWithinDetector(priority)) { // can replace an entity in the same CacheBuffer living in reserved // or shared cache // thread safe as each detector has one thread at one time and only the @@ -268,7 +268,8 @@ private boolean hostIfPossible( } else { // If two threads try to remove the same entity and add their own state, the 2nd remove // returns null and only the first one succeeds. - Entry bufferToRemoveEntity = canReplaceInSharedCache(buffer, priority); + float scaledPriority = buffer.getPriorityTracker().getScaledPriority(priority); + Entry bufferToRemoveEntity = canReplaceInSharedCache(buffer, scaledPriority); CacheBuffer bufferToRemove = bufferToRemoveEntity.getKey(); String entityModelId = bufferToRemoveEntity.getValue(); ModelState removed = null; @@ -357,19 +358,19 @@ private long getReservedDetectorMemory(AnomalyDetector detector) { * * * @param originBuffer the CacheBuffer that the entity belongs to (with the same detector Id) - * @param candicatePriority the candidate entity's priority + * @param candidatePriority the candidate entity's priority * @return the CacheBuffer if we can find a CacheBuffer to make room for the candidate entity */ - private Entry canReplaceInSharedCache(CacheBuffer originBuffer, float candicatePriority) { + private Entry canReplaceInSharedCache(CacheBuffer originBuffer, float candidatePriority) { CacheBuffer minPriorityBuffer = null; float minPriority = Float.MAX_VALUE; String minPriorityEntityModelId = null; for (Map.Entry entry : activeEnities.entrySet()) { CacheBuffer buffer = entry.getValue(); if (buffer != originBuffer && buffer.canRemove()) { - Entry priorityEntry = buffer.getMinimumPriority(); + Entry priorityEntry = buffer.getPriorityTracker().getMinimumScaledPriority(); float priority = priorityEntry.getValue(); - if (candicatePriority > priority && priority < minPriority) { + if (candidatePriority > priority && priority < minPriority) { minPriority = priority; minPriorityBuffer = buffer; minPriorityEntityModelId = priorityEntry.getKey(); @@ -408,7 +409,7 @@ private void clearMemory() { while (memoryToShed > 0) { for (Map.Entry entry : activeEnities.entrySet()) { CacheBuffer buffer = entry.getValue(); - Entry priorityEntry = buffer.getMinimumPriority(); + Entry priorityEntry = buffer.getPriorityTracker().getMinimumScaledPriority(); float priority = priorityEntry.getValue(); if (buffer.canRemove() && priority < minPriority) { minPriority = priority; @@ -533,7 +534,7 @@ public long getTotalUpdates(String detectorId) { return Optional .of(activeEnities) .map(entities -> entities.get(detectorId)) - .map(buffer -> buffer.getHighestPriorityEntityModelId()) + .map(buffer -> buffer.getPriorityTracker().getHighestPriorityEntityId()) .map(entityModelIdOptional -> entityModelIdOptional.get()) .map(entityModelId -> getTotalUpdates(detectorId, entityModelId)) .orElse(0L); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTracker.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTracker.java new file mode 100644 index 00000000..6443afa3 --- /dev/null +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTracker.java @@ -0,0 +1,349 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.amazon.opendistroforelasticsearch.ad.caching; + +import java.time.Clock; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import com.amazon.opendistroforelasticsearch.ad.annotation.Generated; + +/** + * A priority tracker for entities. Read docs/entity-priority.pdf for details. + * + * HC detectors use a 1-pass algorithm for estimating heavy hitters in a stream. + * Our method maintains a time-decayed count for each entity, which allows us to + * compare the frequencies/priorities of entities from different detectors in the + * stream. + * This class contains the heavy-hitter tracking logic.  When an entity is hit, + * a user calls PriorityTracker.updatePriority to update the entity's priority. + * The user can find the most frequently occurring entities in the stream using + * PriorityTracker.getTopNEntities. A typical usage is listed below: + * + *
+ * PriorityTracker tracker =  ...
+ *
+ * // at time t1
+ * tracker.updatePriority(entity1);
+ * tracker.updatePriority(entity3);
+ *
+ * //  at time t2
+ * tracker.updatePriority(entity1);
+ * tracker.updatePriority(entity2);
+ *
+ * // we should have entity 1, 2, 3 in order. 2 comes before 3 because it happens later
+ * List<String> top3 = tracker.getTopNEntities(3);
+ * 
+ * + */ +public class PriorityTracker { + private static final Logger LOG = LogManager.getLogger(PriorityTracker.class); + + // data structure for an entity and its priority + static class PriorityNode { + // entity key + private String key; + // time-decayed priority + private float priority; + + PriorityNode(String key, float priority) { + this.priority = priority; + this.key = key; + } + + @Generated + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + if (obj instanceof PriorityNode) { + PriorityNode other = (PriorityNode) obj; + + EqualsBuilder equalsBuilder = new EqualsBuilder(); + equalsBuilder.append(key, other.key); + return equalsBuilder.isEquals(); + } + return false; + } + + @Generated + @Override + public int hashCode() { + return new HashCodeBuilder().append(key).toHashCode(); + } + + @Generated + @Override + public String toString() { + ToStringBuilder builder = new ToStringBuilder(this); + builder.append("key", key); + builder.append("priority", priority); + return builder.toString(); + } + } + + // Comparator between two entities. Used to sort entities in a priority queue + static class PriorityNodeComparator implements Comparator { + + @Override + public int compare(PriorityNode priority, PriorityNode priority2) { + int equality = priority.key.compareTo(priority2.key); + if (equality == 0) { + // this is consistent with PriorityNode's equals method + return 0; + } + // if not equal, first check priority + int cmp = Float.compare(priority.priority, priority2.priority); + if (cmp == 0) { + // if priority is equal, use lexicographical order of key + cmp = equality; + } + return cmp; + } + } + + // key -> Priority node + private final ConcurrentHashMap key2Priority; + // when detector is created.  Can be reset.  Unit: seconds + private long landmarkEpoch; + // a list of priority nodes + private final ConcurrentSkipListSet priorityList; + // Used to get current time. + private final Clock clock; + // length of seconds in one interval.  Used to compute elapsed periods + // since the detector has been enabled. + private final long intervalSecs; + // determines how fast the decay is + // We use the decay constant 0.125. The half life (https://en.wikipedia.org/wiki/Exponential_decay) + // is 8* ln(2). This means the old value falls to one half with roughly 5.6 intervals. + // We chose 0.125 because multiplying 0.125 can be implemented efficiently using 3 right + // shift and the half life is not too fast or slow . + private final int DECAY_CONSTANT; + // the max number of entities to track + private final int maxEntities; + + /** + * Create a priority tracker for a detector. Detector and priority tracker + * have 1:1 mapping. + * + * @param clock Used to get current time. + * @param intervalSecs Detector interval seconds. + * @param landmarkEpoch The epoch time when the priority tracking starts. + * @param maxEntities the max number of entities to track + */ + public PriorityTracker(Clock clock, long intervalSecs, long landmarkEpoch, int maxEntities) { + this.key2Priority = new ConcurrentHashMap<>(); + this.clock = clock; + this.intervalSecs = intervalSecs; + this.landmarkEpoch = landmarkEpoch; + this.priorityList = new ConcurrentSkipListSet<>(new PriorityNodeComparator()); + this.DECAY_CONSTANT = 3; + this.maxEntities = maxEntities; + } + + /** + * Get the minimum priority entity and compute its scaled priority. + * Used to compare entity priorities among detectors. + * @return the minimum priority entity's ID and scaled priority + */ + public Entry getMinimumScaledPriority() { + PriorityNode smallest = priorityList.first(); + return new SimpleImmutableEntry<>(smallest.key, getScaledPriority(smallest.priority)); + } + + /** + * Get the minimum priority entity and compute its scaled priority. + * Used to compare entity priorities within the same detector. + * @return the minimum priority entity's ID and scaled priority + */ + public Entry getMinimumPriority() { + PriorityNode smallest = priorityList.first(); + return new SimpleImmutableEntry<>(smallest.key, smallest.priority); + } + + /** + * + * @return the minimum priority entity's Id + */ + public Optional getMinimumPriorityEntityId() { + return Optional.of(priorityList).map(list -> list.first()).map(node -> node.key); + } + + /** + * + * @return Get maximum priority entity's Id + */ + public Optional getHighestPriorityEntityId() { + return Optional.of(priorityList).map(list -> list.last()).map(node -> node.key); + } + + /** + * Update an entity's priority with count increment + * @param entityId Entity Id + */ + public void updatePriority(String entityId) { + PriorityNode node = key2Priority.computeIfAbsent(entityId, k -> new PriorityNode(entityId, 0f)); + // reposition this node + this.priorityList.remove(node); + node.priority = getUpdatedPriority(node.priority); + this.priorityList.add(node); + + adjustSizeIfRequired(); + } + + /** + * Associate the specified priority with the entity Id + * @param entityId Entity Id + * @param priority priority + */ + protected void addPriority(String entityId, float priority) { + PriorityNode node = new PriorityNode(entityId, priority); + key2Priority.put(entityId, node); + priorityList.add(node); + + adjustSizeIfRequired(); + } + + /** + * Adjust tracking list if the size exceeded the limit + */ + private void adjustSizeIfRequired() { + if (key2Priority.size() > maxEntities) { + Optional minPriorityId = getMinimumPriorityEntityId(); + if (minPriorityId.isPresent()) { + removePriority(minPriorityId.get()); + } + } + } + + /** + * Remove an entity in the tracker + * @param entityId Entity Id + */ + protected void removePriority(String entityId) { + // remove if the key matches; priority does not matter + priorityList.remove(new PriorityNode(entityId, 0)); + key2Priority.remove(entityId); + } + + /** + * Remove all of entities + */ + protected void clearPriority() { + key2Priority.clear(); + priorityList.clear(); + } + + /** + * Return the updated priority with new priority increment. Used when comparing + * entities' priorities within the same detector. + * + * Each detector maintains an ordered map, filled by entities's accumulated sum of g(i−L), + * which is what this function computes. + * + * g(n) = e^{0.125n}. i is current period. L is the landmark: period 0 when the + * detector is enabled. i - L measures the elapsed periods since detector starts. + * 0.125 is the decay constant. + * + * Since g(i−L) is changing and they are the same for all entities of the same detector, + * we can compare entities' priorities by considering the accumulated sum of g(i−L). + * + * @param oldPriority Existing priority + * + * @return new priority + */ + float getUpdatedPriority(float oldPriority) { + long increment = computeWeightedPriorityIncrement(); + oldPriority += Math.log(1 + Math.exp(increment - oldPriority)); + // if overflow happens, using the most recent decayed count instead. + if (oldPriority == Float.POSITIVE_INFINITY) { + oldPriority = increment; + } + return oldPriority; + } + + /** + * Return the scaled priority. Used when comparing entities' priorities among + * different detectors. + * + * Updated priority = current priority - log(g(t - L)), where g(n) = e^{0.125n}, + * t is current time, and L is the landmark. t - L measures the number of elapsed + * periods relative to the landmark. + * + * When replacing an entity, we query the minimum from each ordered map and + * compute w(i,p) for each minimum entity by scaling the sum by g(p−L). Notice g(p−L) + * can be different if detectors start at different timestamps. The minimum of the minimum + * is selected to be replaced. The number of multi-entity detectors is limited (we consider + * to support ten currently), so the computation is cheap. + * + * @param currentPriority Current priority + * @return the scaled priority + */ + float getScaledPriority(float currentPriority) { + return currentPriority - computeWeightedPriorityIncrement(); + } + + /** + * Compute the weighted priority increment using 0.125n, where n is the number of + * periods relative to the landmark. + * Each detector has its own landmark L: period 0 when the detector is enabled. + * + * @return the weighted priority increment used in the priority update step. + */ + long computeWeightedPriorityIncrement() { + long periods = (clock.instant().getEpochSecond() - landmarkEpoch) / intervalSecs; + return periods >> DECAY_CONSTANT; + } + + /** + * + * @param n the number of entities to return. Can be less than n if there are not enough entities stored. + * @return top entities in the descending order of priority + */ + public List getTopNEntities(int n) { + List entities = new ArrayList<>(); + Iterator entityIterator = priorityList.descendingIterator(); + for (int i = 0; i < n && entityIterator.hasNext(); i++) { + entities.add(entityIterator.next().key); + } + return entities; + } + + /** + * + * @return the number of tracked entities + */ + public int size() { + return key2Priority.size(); + } +} diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/NodeStateManagerTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/NodeStateManagerTests.java index 44f7caac..b9d93b3e 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/NodeStateManagerTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/NodeStateManagerTests.java @@ -238,6 +238,37 @@ public void testGetAnomalyDetector() throws IOException, InterruptedException { assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); } + /** + * Test that we caches anomaly detector definition after the first call + * @throws IOException if client throws exception + * @throws InterruptedException if the current thread is interrupted while waiting + */ + @SuppressWarnings("unchecked") + public void testRepeatedGetAnomalyDetector() throws IOException, InterruptedException { + String detectorId = setupDetector(); + final CountDownLatch inProgressLatch = new CountDownLatch(2); + + stateManager.getAnomalyDetector(detectorId, ActionListener.wrap(asDetector -> { + assertEquals(detectorToCheck, asDetector.get()); + inProgressLatch.countDown(); + }, exception -> { + assertTrue(false); + inProgressLatch.countDown(); + })); + + stateManager.getAnomalyDetector(detectorId, ActionListener.wrap(asDetector -> { + assertEquals(detectorToCheck, asDetector.get()); + inProgressLatch.countDown(); + }, exception -> { + assertTrue(false); + inProgressLatch.countDown(); + })); + + assertTrue(inProgressLatch.await(100, TimeUnit.SECONDS)); + + verify(client, times(1)).get(any(), any(ActionListener.class)); + } + public void getCheckpointTestTemplate(boolean exists) throws IOException { setupCheckpoint(exists); when(clock.instant()).thenReturn(Instant.MIN); diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBufferTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBufferTests.java index de93b6ce..c2653fad 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBufferTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/CacheBufferTests.java @@ -68,7 +68,7 @@ public void setUp() throws Exception { AnomalyDetectorSettings.HOURLY_MAINTENANCE, detectorId ); - initialPriority = cacheBuffer.getUpdatedPriority(0); + initialPriority = cacheBuffer.getPriorityTracker().getUpdatedPriority(0); } // cache.put(1, 1); @@ -90,13 +90,13 @@ public void testRemovalCandidate() { cacheBuffer.put(modelId1, MLUtil.randomModelState(initialPriority, modelId1)); cacheBuffer.put(modelId2, MLUtil.randomModelState(initialPriority, modelId2)); assertEquals(modelId1, cacheBuffer.get(modelId1).getModelId()); - Entry removalCandidate = cacheBuffer.getMinimumPriority(); + Entry removalCandidate = cacheBuffer.getPriorityTracker().getMinimumScaledPriority(); assertEquals(modelId2, removalCandidate.getKey()); cacheBuffer.remove(); cacheBuffer.put(modelId3, MLUtil.randomModelState(initialPriority, modelId3)); assertEquals(null, cacheBuffer.get(modelId2)); assertEquals(modelId3, cacheBuffer.get(modelId3).getModelId()); - removalCandidate = cacheBuffer.getMinimumPriority(); + removalCandidate = cacheBuffer.getPriorityTracker().getMinimumScaledPriority(); assertEquals(modelId1, removalCandidate.getKey()); cacheBuffer.remove(modelId1); assertEquals(null, cacheBuffer.get(modelId1)); @@ -114,7 +114,7 @@ public void testRemovalCandidate2() throws InterruptedException { String modelId2 = "2"; String modelId3 = "3"; String modelId4 = "4"; - float initialPriority = cacheBuffer.getUpdatedPriority(0); + float initialPriority = cacheBuffer.getPriorityTracker().getUpdatedPriority(0); cacheBuffer.put(modelId3, MLUtil.randomModelState(initialPriority, modelId3)); cacheBuffer.put(modelId2, MLUtil.randomModelState(initialPriority, modelId2)); cacheBuffer.put(modelId2, MLUtil.randomModelState(initialPriority, modelId2)); @@ -143,10 +143,10 @@ public void testCanRemove() { String modelId2 = "2"; String modelId3 = "3"; assertTrue(cacheBuffer.dedicatedCacheAvailable()); - assertTrue(!cacheBuffer.canReplace(100)); + assertTrue(!cacheBuffer.canReplaceWithinDetector(100)); cacheBuffer.put(modelId1, MLUtil.randomModelState(initialPriority, modelId1)); - assertTrue(cacheBuffer.canReplace(100)); + assertTrue(cacheBuffer.canReplaceWithinDetector(100)); assertTrue(!cacheBuffer.dedicatedCacheAvailable()); assertTrue(!cacheBuffer.canRemove()); cacheBuffer.put(modelId2, MLUtil.randomModelState(initialPriority, modelId2)); @@ -154,7 +154,7 @@ public void testCanRemove() { cacheBuffer.replace(modelId3, MLUtil.randomModelState(initialPriority, modelId3)); assertTrue(cacheBuffer.isActive(modelId2)); assertTrue(cacheBuffer.isActive(modelId3)); - assertEquals(modelId3, cacheBuffer.getHighestPriorityEntityModelId().get()); + assertEquals(modelId3, cacheBuffer.getPriorityTracker().getHighestPriorityEntityId().get()); assertEquals(2, cacheBuffer.getActiveEntities()); } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCacheTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCacheTests.java index c1f3dc1d..1c58d3ec 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCacheTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityCacheTests.java @@ -175,7 +175,7 @@ public void setUp() throws Exception { detectorId ); - initialPriority = cacheBuffer.getUpdatedPriority(0); + initialPriority = cacheBuffer.getPriorityTracker().getUpdatedPriority(0); point = new double[] { 0.1 }; entityName = "1.2.3.4"; } diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTrackerTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTrackerTests.java new file mode 100644 index 00000000..6952d9b7 --- /dev/null +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/caching/PriorityTrackerTests.java @@ -0,0 +1,97 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file 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.amazon.opendistroforelasticsearch.ad.caching; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.time.Clock; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.elasticsearch.test.ESTestCase; +import org.junit.Before; + +public class PriorityTrackerTests extends ESTestCase { + Clock clock; + PriorityTracker tracker; + Instant now; + String entity1, entity2, entity3; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + clock = mock(Clock.class); + now = Instant.now(); + tracker = new PriorityTracker(clock, 1, now.getEpochSecond(), 3); + entity1 = "entity1"; + entity2 = "entity2"; + entity3 = "entity3"; + } + + public void testNormal() { + when(clock.instant()).thenReturn(now); + // first interval entity 1 and 3 + tracker.updatePriority(entity1); + tracker.updatePriority(entity3); + when(clock.instant()).thenReturn(now.plusSeconds(60L)); + // second interval entity 1 and 2 + tracker.updatePriority(entity1); + tracker.updatePriority(entity2); + // we should have entity 1, 2, 3 in order. 2 comes before 3 because it happens later + List top3 = tracker.getTopNEntities(3); + assertEquals(entity1, top3.get(0)); + assertEquals(entity2, top3.get(1)); + assertEquals(entity3, top3.get(2)); + + // even though I want top 4, but there are only 3 entities + List top4 = tracker.getTopNEntities(4); + assertEquals(3, top4.size()); + assertEquals(entity1, top3.get(0)); + assertEquals(entity2, top3.get(1)); + assertEquals(entity3, top3.get(2)); + } + + public void testOverflow() { + when(clock.instant()).thenReturn(now); + tracker.updatePriority(entity1); + float priority1 = tracker.getMinimumScaledPriority().getValue(); + + // when(clock.instant()).thenReturn(now.plusSeconds(60L)); + tracker.updatePriority(entity1); + float priority2 = tracker.getMinimumScaledPriority().getValue(); + // we incremented the priority + assertTrue("The following is expected: " + priority2 + " > " + priority1, priority2 > priority1); + + when(clock.instant()).thenReturn(now.plus(3, ChronoUnit.DAYS)); + tracker.updatePriority(entity1); + // overflow happens, we use increment as the new priority + assertEquals(0, tracker.getMinimumScaledPriority().getValue().floatValue(), 0.001); + } + + public void testTooManyEntities() { + when(clock.instant()).thenReturn(now); + tracker = new PriorityTracker(clock, 1, now.getEpochSecond(), 2); + tracker.updatePriority(entity1); + tracker.updatePriority(entity3); + assertEquals(2, tracker.size()); + tracker.updatePriority(entity2); + // one entity is kicked out due to the size limit is reached. + assertEquals(2, tracker.size()); + } +}