package ru.yandex.market.graphouse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.jdbc.core.JdbcTemplate;
import ru.yandex.market.graphouse.search.MetricSearch;
import ru.yandex.market.graphouse.search.MetricStatus;
import ru.yandex.market.graphouse.search.tree.MetricDescription;
import ru.yandex.market.graphouse.search.tree.MetricTree;
import ru.yandex.market.graphouse.utils.AppendableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Позволяет по таймеру выбирать "мусорные" метрики из кликхауса и автоматически их скрывать.
* Метрика считается "мусорной", если выполняются условия:
* - последние значение по ней было {@link #missingDays}
* - число точек менее {@link #maxValuesCount}
*
* @author Dmitry Andreev <a href="mailto:AndreevDm@yandex-team.ru"></a>
* @date 11/06/15
*/
public class AutoHideService implements Runnable {
private static final Logger log = LogManager.getLogger();
@Value("${graphouse.autohide.step}")
private int stepSize = 10_000;
private final JdbcTemplate clickHouseJdbcTemplate;
private final MetricSearch metricSearch;
@Value("${graphouse.clickhouse.data-table}")
private String graphiteTable;
@Value("${graphouse.autohide.enabled}")
private boolean enabled = true;
@Value("${graphouse.autohide.max-values-count}")
private int maxValuesCount = 200;
@Value("${graphouse.autohide.missing-days}")
private int missingDays = 7;
@Value("${graphouse.autohide.run-delay-minutes}")
private int runDelayMinutes = 10;
@Value("${graphouse.autohide.retry.count}")
private int retryCount = 10;
@Value("${graphouse.autohide.retry.wait_seconds}")
private int retryWaitSeconds = 10;
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
public AutoHideService(JdbcTemplate clickHouseJdbcTemplate, MetricSearch metricSearch) {
this.clickHouseJdbcTemplate = clickHouseJdbcTemplate;
this.metricSearch = metricSearch;
}
public void startService() throws Exception {
if (!enabled) {
log.info("Autohide disabled");
return;
}
scheduler.scheduleAtFixedRate(this, runDelayMinutes, TimeUnit.DAYS.toMinutes(1), TimeUnit.MINUTES);
log.info("Autohide scheduled");
}
@Override
public void run() {
if (metricSearch.isMetricTreeLoaded()) {
hide();
}
}
private void hide() {
log.info("Running autohide.");
try {
MetricMinMaxChecker metricMinMaxChecker = new MetricMinMaxChecker();
final AtomicInteger hiddenMetricCounter = new AtomicInteger();
checkPath(MetricTree.ALL_PATTERN, metricMinMaxChecker, hiddenMetricCounter);
log.info("Autohide completed. " + hiddenMetricCounter.get() + " metrics hidden");
} catch (Exception e) {
log.error("Failed to run autohide.", e);
}
}
private void checkPath(String path, MetricMinMaxChecker metricMinMaxChecker, AtomicInteger hiddenCounter) throws IOException {
AppendableList appendableList = new AppendableList();
metricSearch.search(path, appendableList);
for (MetricDescription metric : appendableList.getSortedList()) {
if (metric.isDir()) {
checkPath(metric.getName() + MetricTree.ALL_PATTERN, metricMinMaxChecker, hiddenCounter);
} else {
metricMinMaxChecker.addToCheck(metric.getName());
}
}
if (path.equals(MetricTree.ALL_PATTERN) || metricMinMaxChecker.needCheckInDb()) {
final int hiddenMetricCount = hideMetricsBetween(metricMinMaxChecker.minMetric, metricMinMaxChecker.maxMetric);
hiddenCounter.addAndGet(hiddenMetricCount);
metricMinMaxChecker.reset();
}
}
/*
* Проверяет метрики в диапазоне и возвращает количество скрытых
* */
private int hideMetricsBetween(String minMetric, String maxMetric) {
if (minMetric == null || maxMetric == null) {
return 0;
}
for (int i = 0; i < retryCount; i++) {
try {
final List<String> metricsToHide = new ArrayList<>();
clickHouseJdbcTemplate.query(
"SELECT metric, count() AS cnt, max(updated) AS ts " +
"FROM " + graphiteTable + " WHERE metric >= ? AND metric <= ?" +
"GROUP BY metric " +
"HAVING cnt < ? AND ts < toUInt32(toDateTime(today() - ?))",
row -> {
metricsToHide.add(row.getString(1));
},
minMetric, maxMetric, maxValuesCount, missingDays
);
metricSearch.modify(metricsToHide, MetricStatus.AUTO_HIDDEN);
log.info(metricsToHide.size() + " metrics hidden between <" + minMetric + "> and <" + maxMetric + ">");
return metricsToHide.size();
} catch (Exception e) {
boolean isLastTry = (i == retryCount - 1);
if (!isLastTry) {
log.error("Write to clickhouse failed. Retry after " + retryWaitSeconds + " seconds", e);
try {
TimeUnit.SECONDS.sleep(retryWaitSeconds);
} catch (InterruptedException ie) {
log.error(ie);
}
} else {
throw e;
}
}
}
throw new IllegalStateException();
}
private class MetricMinMaxChecker {
private String minMetric = null;
private String maxMetric = null;
int lastCheckCounter = 0;
private void reset() {
minMetric = null;
maxMetric = null;
lastCheckCounter = 0;
}
private void addToCheck(String metric) {
if (lastCheckCounter == 0) {
minMetric = metric;
maxMetric = metric;
} else {
if (minMetric.compareTo(metric) > 0) {
minMetric = metric;
}
if (maxMetric.compareTo(metric) < 0) {
maxMetric = metric;
}
}
lastCheckCounter++;
}
private boolean needCheckInDb() {
return lastCheckCounter > stepSize;
}
}
public void setRunDelayMinutes(int runDelayMinutes) {
this.runDelayMinutes = runDelayMinutes;
}
public void setMaxValuesCount(int maxValuesCount) {
this.maxValuesCount = maxValuesCount;
}
public void setMissingDays(int missingDays) {
this.missingDays = missingDays;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void setStepSize(Integer stepSize) {
this.stepSize = stepSize;
}
public void setRetryCount(int retryCount) {
this.retryCount = retryCount;
}
public void setRetryWaitSeconds(int retryWaitSeconds) {
this.retryWaitSeconds = retryWaitSeconds;
}
}