package ru.yandex.market.graphouse.search.tree; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.CharMatcher; import ru.yandex.market.graphouse.MetricUtil; import ru.yandex.market.graphouse.retention.RetentionProvider; import ru.yandex.market.graphouse.search.MetricPath; import ru.yandex.market.graphouse.search.MetricStatus; import ru.yandex.market.graphouse.utils.AppendableResult; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.List; import java.util.Map; import java.util.regex.PatternSyntaxException; /** * @author Dmitry Andreev <a href="mailto:AndreevDm@yandex-team.ru"></a> * @date 07/04/15 */ public class MetricTree { public static final String ALL_PATTERN = "*"; private static final CharMatcher EXPRESSION_MATCHER = CharMatcher.anyOf(ALL_PATTERN + "?[]{}"); private final MetricDir root = new InMemoryMetricDir(null, "", MetricStatus.SIMPLE); private final MetricDirFactory metricDirFactory; private final RetentionProvider retentionProvider; public MetricTree(MetricDirFactory metricDirFactory, RetentionProvider retentionProvider) { this.metricDirFactory = metricDirFactory; this.retentionProvider = retentionProvider; } public void search(String query, AppendableResult result) throws IOException { String[] levels = MetricUtil.splitToLevels(query); search(root, levels, 0, result); } /** * Рекурсивный метод для получения списка метрик внутри дерева. * * @param parentDir внутри какой директории ищем * @param levels узлы дерева, каждый может быть задан явно или паттерном, используя *?[]{} * Пример: five_min.abo-main.timings-method.*.0_95 * @param levelIndex индекс текущего узла * @param result * @throws IOException */ private void search(MetricDir parentDir, String[] levels, int levelIndex, AppendableResult result) throws IOException { if (parentDir == null || !parentDir.visible()) { return; } boolean isLast = (levelIndex == levels.length - 1); String level = levels[levelIndex]; boolean isPattern = containsExpressions(level); if (!isPattern) { if (parentDir.hasDirs()) { if (isLast) { appendSimpleResult(parentDir.getDirs(), level, result); } else { search(parentDir.getDirs().get(level), levels, levelIndex + 1, result); } } if (isLast && parentDir.hasMetrics()) { appendSimpleResult(parentDir.getMetrics(), level, result); } } else if (level.equals(ALL_PATTERN)) { if (parentDir.hasDirs()) { if (isLast) { appendAllResult(parentDir.getDirs(), result); } else { for (MetricDir dir : parentDir.getDirs().values()) { search(dir, levels, levelIndex + 1, result); } } } if (isLast && parentDir.hasMetrics()) { appendAllResult(parentDir.getMetrics(), result); } } else { PathMatcher pathMatcher = createPathMatcher(level); if (pathMatcher == null) { return; } if (parentDir.hasDirs()) { if (isLast) { appendAllPatternResult(parentDir.getDirs(), pathMatcher, result); } else { for (Map.Entry<String, MetricDir> dirEntry : parentDir.getDirs().entrySet()) { if (matches(pathMatcher, dirEntry.getKey())) { search(dirEntry.getValue(), levels, levelIndex + 1, result); } } } } if (isLast && parentDir.hasMetrics()) { appendAllPatternResult(parentDir.getMetrics(), pathMatcher, result); } } } private <T extends MetricBase> void appendAllPatternResult(Map<String, T> map, PathMatcher pathMatcher, AppendableResult result) throws IOException { if (map != null) { for (MetricBase metricBase : map.values()) { if (matches(pathMatcher, metricBase.name)) { appendResult(metricBase, result); } } } } private <T extends MetricBase> void appendAllResult(Map<String, T> map, AppendableResult result) throws IOException { if (map != null) { for (MetricBase metricBase : map.values()) { appendResult(metricBase, result); } } } private <T extends MetricBase> void appendSimpleResult(Map<String, T> map, String name, AppendableResult result) throws IOException { if (map != null) { appendResult(map.get(name), result); } } private static void appendResult(MetricBase metricBase, AppendableResult result) throws IOException { if (metricBase != null && metricBase.visible()) { result.appendMetric(metricBase); } } @VisibleForTesting static PathMatcher createPathMatcher(String globPattern) { try { return FileSystems.getDefault().getPathMatcher("glob:" + globPattern); } catch (PatternSyntaxException e) { return null; } } @VisibleForTesting static boolean matches(PathMatcher pathMatcher, final String fileName) { Path mockPath = new MetricPath(fileName); return pathMatcher.matches(mockPath); } public int metricCount() { return root.loadedMetricCount(); } public int dirCount() { return root.loadedDirCount(); } public MetricDescription add(String metric) { return modify(metric, MetricStatus.SIMPLE); } /** * Try to find metric description. Fast. Can return null, even if metric exists * * @param levels * @return Metric description if it exists and loaded it the tree, otherwise null */ public MetricDescription maybeFindMetric(String[] levels) { MetricDir dir = root; int lastLevel = levels.length - 1; for (int i = 0; i < levels.length; i++) { String level = levels[i]; if (i == lastLevel) { return dir.maybeGetMetric(level); } else { dir = dir.maybeGetDir(level); if (dir == null || dir.getStatus() == MetricStatus.BAN) { return null; } } } throw new IllegalStateException(); } /** * creates or changes the status of a metric or an entire directory. * * @param metric if ends with ".", then it's a directory * @param status * @return MetricDescription, or <code>null</code> if the metric/directory is banned */ public MetricDescription modify(String metric, MetricStatus status) { boolean isDir = MetricUtil.isDir(metric); String[] levels = MetricUtil.splitToLevels(metric); MetricDir dir = root; for (int i = 0; i < levels.length; i++) { boolean isLast = (i == levels.length - 1); if (dir.getStatus() == MetricStatus.BAN) { return null; } String level = levels[i]; if (!isLast) { dir = dir.getOrCreateDir(level, status, metricDirFactory); } else { MetricBase metricBase; if (isDir) { metricBase = dir.getOrCreateDir(level, status, metricDirFactory); } else { metricBase = dir.getOrCreateMetric(level, status, retentionProvider); } metricBase.setStatus(selectStatus(metricBase.getStatus(), status)); return metricBase; } } throw new IllegalStateException(); } /** * Возвращаем новый статус при изменении метрики, учитывая граф возможных переходов. * * @param oldStatus * @param newStatus * @return */ public static MetricStatus selectStatus(MetricStatus oldStatus, MetricStatus newStatus) { if (oldStatus == newStatus) { return oldStatus; } List<MetricStatus> restricted = MetricStatus.RESTRICTED_GRAPH_EDGES.get(oldStatus); return restricted == null || !restricted.contains(newStatus) ? newStatus : oldStatus; } @VisibleForTesting static boolean containsExpressions(String metric) { return EXPRESSION_MATCHER.matchesAnyOf(metric); } }