From 62b1342e9614399326503bb9a4183f8215a6bf4f Mon Sep 17 00:00:00 2001 From: brharrington Date: Sat, 2 Dec 2023 17:09:13 -0600 Subject: [PATCH] support checking for match after the prefix (#1099) For patterns where the prefix is extracted and verified before values are checked against the full matcher, we do not need to recheck the prefix. This change adds a method to the pattern matcher to allow bypassing the prefix and just matching against the remaining part of the pattern. --- .../com/netflix/spectator/impl/PatternMatcher.java | 9 +++++++++ .../netflix/spectator/impl/matcher/SeqMatcher.java | 14 ++++++++++++++ .../spectator/impl/matcher/StartsWithMatcher.java | 5 +++++ .../impl/matcher/AbstractPatternMatcherTest.java | 12 ++++++++++++ .../netflix/spectator/atlas/impl/QueryIndex.java | 11 ++++++++++- 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java index 305d6749a..c8ff257b2 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/PatternMatcher.java @@ -46,6 +46,15 @@ public interface PatternMatcher { */ boolean matches(String str); + /** + * Returns true if the passed in string matches the pattern after the prefix. This method + * can be used to more efficiently check the value if the {@link #prefix()} was already + * verified. + */ + default boolean matchesAfterPrefix(String str) { + return matches(str); + } + /** * Returns a fixed string prefix for the pattern if one is available. This can be used * with indexed data to help select a subset of values that are possible matches. If the diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java index 39bee622e..13cf1a19e 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/SeqMatcher.java @@ -73,6 +73,20 @@ public int matches(String str, int start, int length) { return pos; } + @Override + public boolean matchesAfterPrefix(String str) { + if (matchers[0] instanceof StartsWithMatcher) { + final int end = str.length(); + int pos = matchers[0].prefix().length(); + for (int i = 1; i < matchers.length && pos >= 0; ++i) { + pos = matchers[i].matches(str, pos, end - pos); + } + return pos >= 0; + } else { + return matches(str); + } + } + @Override public String prefix() { return matchers[0].prefix(); diff --git a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java index 7cb790fc1..d719d2dec 100644 --- a/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java +++ b/spectator-api/src/main/java/com/netflix/spectator/impl/matcher/StartsWithMatcher.java @@ -53,6 +53,11 @@ public int matches(String str, int start, int length) { return matched ? pattern.length() : Constants.NO_MATCH; } + @Override + public boolean matchesAfterPrefix(String str) { + return true; + } + @Override public String prefix() { return pattern; diff --git a/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/AbstractPatternMatcherTest.java b/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/AbstractPatternMatcherTest.java index 9f43d57f0..ecb889c9c 100644 --- a/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/AbstractPatternMatcherTest.java +++ b/spectator-api/src/test/java/com/netflix/spectator/impl/matcher/AbstractPatternMatcherTest.java @@ -65,6 +65,18 @@ public void prefix() { Assertions.assertEquals("abc", PatternMatcher.compile("^[a][b][c]+").prefix()); } + @Test + public void matchesAfterPrefix() { + // StartsWithMatcher, bar case should match because it will not get checked and we trust + // that the caller has already verified the prefix. + Assertions.assertTrue(PatternMatcher.compile("^abc").matchesAfterPrefix("abcdef")); + Assertions.assertTrue(PatternMatcher.compile("^abc").matchesAfterPrefix("bardef")); + + // SeqMatcher + Assertions.assertTrue(PatternMatcher.compile("^abc").matchesAfterPrefix("abc[d-f]")); + Assertions.assertTrue(PatternMatcher.compile("^abc").matchesAfterPrefix("bar[d-f]")); + } + @Test public void startAnchor() { testRE("^abc", "abcdef"); diff --git a/spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/QueryIndex.java b/spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/QueryIndex.java index a0c21179c..04c2234cd 100644 --- a/spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/QueryIndex.java +++ b/spectator-reg-atlas/src/main/java/com/netflix/spectator/atlas/impl/QueryIndex.java @@ -499,7 +499,7 @@ public void forEachMatch(Function tags, Consumer consumer) { if (!otherChecks.isEmpty()) { List> tmp = new ArrayList<>(); otherChecksTree.forEach(v, kq -> { - if (kq.matches(v)) { + if (matches(kq, v)) { QueryIndex idx = otherChecks.get(kq); if (idx != null) { tmp.add(idx); @@ -536,6 +536,15 @@ public void forEachMatch(Function tags, Consumer consumer) { } } + private boolean matches(Query.KeyQuery kq, String value) { + if (kq instanceof Query.Regex) { + Query.Regex re = (Query.Regex) kq; + return re.pattern().matchesAfterPrefix(value); + } else { + return kq.matches(value); + } + } + /** * Find hot spots in the index where there is a large set of linear matches, e.g. a bunch * of regex queries for a given key.