/*- * -\-\- * FastForward API * -- * Copyright (C) 2020 Spotify AB * -- * 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.spotify.ffwd.util; import com.google.inject.Inject; import com.google.inject.name.Named; import com.spotify.ffwd.model.Metric; import com.spotify.ffwd.statistics.HighFrequencyDetectorStatistics; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.IntSummaryStatistics; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import java.util.stream.IntStream; import org.slf4j.Logger; /** * Class responsible for high frequency metrics detection. * All batched metrics will be examined. * If configured will drop metrics marked as high frequency. */ public class HighFrequencyDetector { public static final int BURST_THRESHOLD = 5; /** Allow to drop high frequency metrics. */ @Inject @Named("dropHighFrequencyMetric") boolean dropHighFrequencyMetric; /** Minimum number of milliseconds allowed between data points. */ @Inject @Named("minFrequencyMillisAllowed") int minFrequencyMillisAllowed; /** Minimum number of times high frequency detected before metrics are dropped. */ @Inject @Named("minNumberOfTriggers") int minNumberOfTriggers; /** Milliseconds high frequency triggers are refreshed. */ @Inject @Named("highFrequencyDataRecycleMS") long highFrequencyDataRecycleMS; @Inject Logger log; @Inject private HighFrequencyDetectorStatistics statistics; /** High frequency metrics counter. */ final AtomicReference<Map<Integer, Integer>> highFrequencyMetrics = new AtomicReference<>(new HashMap<>()); final AtomicLong highFrequencyTriggersTS; /* TODO 1. unit tests a. sendBatch with many points b. send both with many points c. check swap of high freq hashmap */ @Inject public HighFrequencyDetector() { this.highFrequencyTriggersTS = new AtomicLong(System.currentTimeMillis()); } /** * Detects high frequency metrics by grouping and calculating * time delta between metric timestamps * * @param metrics * @return list of filtered metrics */ public List<Metric> detect(final List<Metric> metrics) { List<Metric> newList = new ArrayList<>(); // Groups metrics by metric hash code and finds times deltas of ordered data points. // {hashcode -> -1, hashcode1 -> 10} Map<Integer, Integer> groupedMetrics = metrics.stream() .sorted(Comparator.comparing(Metric::getTime)) .collect( Collectors.groupingBy( Metric::hashCode, Collectors.collectingAndThen( Collectors.toList(), this::computeTimeDelta))); updateHighFrequencyMetricsStats(groupedMetrics); if (dropHighFrequencyMetric && highFrequencyMetrics.get().size() > 0) { metrics.stream() .filter( metric -> highFrequencyMetrics.get().getOrDefault(metric.hashCode(), 0) < minNumberOfTriggers) .forEach(newList::add); statistics.reportHighFrequencyMetricsDropped(metrics.size() - newList.size()); return newList; } return metrics; } private int computeTimeDelta(List<Metric> list) { int size = list.size(); IntSummaryStatistics stats = IntStream.range(1, size) .map( x -> (int) (list.get(size - x).getTime().getTime() - list.get(size - x - 1).getTime().getTime())) .filter(d -> (d >= 0 && d < minFrequencyMillisAllowed)) .summaryStatistics(); int result = -1; /** * In order to be marked as high frequency metric the number of points * should be above the BURST_THRESHOLD. * It ignores any small bursts of high frequency metrics. */ if (stats.getCount() > BURST_THRESHOLD) { // uses minimal delta time from all consecutive data points result = stats.getMin(); log.info("stats: " + stats); } return result; } /** * Updates internal map of high frequency metrics * * @param groupedMetrics - Grouped metrics */ private void updateHighFrequencyMetricsStats(final Map<Integer, Integer> groupedMetrics) { groupedMetrics.entrySet().stream() .filter(x -> x.getValue() >= 0) .forEach( hashcode -> { highFrequencyMetrics .get() .compute(hashcode.getKey(), (key, val) -> (val == null) ? 1 : val + 1); }); statistics.reportHighFrequencyMetrics(highFrequencyMetrics.get().size()); swapHighFrequencyTriggersData(); } /** Resets high frequency triggers data hashmap */ private synchronized void swapHighFrequencyTriggersData() { if (System.currentTimeMillis() - highFrequencyTriggersTS.get() > highFrequencyDataRecycleMS) { highFrequencyMetrics.set(new HashMap<>()); highFrequencyTriggersTS.set(System.currentTimeMillis()); } } }