From bfec57bd2e11c12077bc82e9ec144843e0a890af Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 10:40:57 +0100 Subject: [PATCH 01/29] Further optimise FastRegexMatcher Signed-off-by: Marco Pracucci --- model/labels/matcher.go | 27 + model/labels/matcher_test.go | 94 ++- model/labels/regexp.go | 894 ++++++++++++++++++++++++++-- model/labels/regexp_test.go | 1055 +++++++++++++++++++++++++++++++--- 4 files changed, 1960 insertions(+), 110 deletions(-) diff --git a/model/labels/matcher.go b/model/labels/matcher.go index f299c40f64..1282f80d63 100644 --- a/model/labels/matcher.go +++ b/model/labels/matcher.go @@ -118,3 +118,30 @@ func (m *Matcher) GetRegexString() string { } return m.re.GetRegexString() } + +// SetMatches returns a set of equality matchers for the current regex matchers if possible. +// For examples the regexp `a(b|f)` will returns "ab" and "af". +// Returns nil if we can't replace the regexp by only equality matchers. +func (m *Matcher) SetMatches() []string { + if m.re == nil { + return nil + } + return m.re.SetMatches() +} + +// Prefix returns the required prefix of the value to match, if possible. +// It will be empty if it's an equality matcher or if the prefix can't be determined. +func (m *Matcher) Prefix() string { + if m.re == nil { + return "" + } + return m.re.prefix +} + +// IsRegexOptimized returns whether regex is optimized. +func (m *Matcher) IsRegexOptimized() bool { + if m.re == nil { + return false + } + return m.re.IsOptimized() +} diff --git a/model/labels/matcher_test.go b/model/labels/matcher_test.go index d26e9329f2..c23deafe61 100644 --- a/model/labels/matcher_test.go +++ b/model/labels/matcher_test.go @@ -14,13 +14,14 @@ package labels import ( + "fmt" "testing" "github.com/stretchr/testify/require" ) func mustNewMatcher(t *testing.T, mType MatchType, value string) *Matcher { - m, err := NewMatcher(mType, "", value) + m, err := NewMatcher(mType, "test_label_name", value) require.NoError(t, err) return m } @@ -81,6 +82,21 @@ func TestMatcher(t *testing.T) { value: "foo-bar", match: false, }, + { + matcher: mustNewMatcher(t, MatchRegexp, "$*bar"), + value: "foo-bar", + match: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "bar^+"), + value: "foo-bar", + match: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "$+bar"), + value: "foo-bar", + match: false, + }, } for _, test := range tests { @@ -118,6 +134,82 @@ func TestInverse(t *testing.T) { } } +func TestPrefix(t *testing.T) { + for i, tc := range []struct { + matcher *Matcher + prefix string + }{ + { + matcher: mustNewMatcher(t, MatchEqual, "abc"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchNotEqual, "abc"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abcd|abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchNotRegexp, "abcd|abc.+"), + prefix: "abc", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc(def|ghj)|ab|a."), + prefix: "a", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "foo.+bar|foo.*baz"), + prefix: "foo", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc|.*"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc|def"), + prefix: "", + }, + { + matcher: mustNewMatcher(t, MatchRegexp, ".+def"), + prefix: "", + }, + } { + t.Run(fmt.Sprintf("%d: %s", i, tc.matcher), func(t *testing.T) { + require.Equal(t, tc.prefix, tc.matcher.Prefix()) + }) + } +} + +func TestIsRegexOptimized(t *testing.T) { + for i, tc := range []struct { + matcher *Matcher + isRegexOptimized bool + }{ + { + matcher: mustNewMatcher(t, MatchEqual, "abc"), + isRegexOptimized: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "."), + isRegexOptimized: false, + }, + { + matcher: mustNewMatcher(t, MatchRegexp, "abc.+"), + isRegexOptimized: true, + }, + } { + t.Run(fmt.Sprintf("%d: %s", i, tc.matcher), func(t *testing.T) { + require.Equal(t, tc.isRegexOptimized, tc.matcher.IsRegexOptimized()) + }) + } +} + func BenchmarkMatchType_String(b *testing.B) { for i := 0; i <= b.N; i++ { _ = MatchType(i % int(MatchNotRegexp+1)).String() diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 14319c7f7a..5e470afa3b 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -18,69 +18,344 @@ import ( "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" + "golang.org/x/exp/slices" +) + +const ( + maxSetMatches = 256 + + // The minimum number of alternate values a regex should have to trigger + // the optimization done by optimizeEqualStringMatchers() and so use a map + // to match values instead of iterating over a list. This value has + // been computed running BenchmarkOptimizeEqualStringMatchers. + minEqualMultiStringMatcherMapThreshold = 16 ) type FastRegexMatcher struct { + // Under some conditions, re is nil because the expression is never parsed. + // We store the original string to be able to return it in GetRegexString(). + reString string re *regexp.Regexp - prefix string - suffix string - contains string - // shortcut for literals - literal bool - value string + setMatches []string + stringMatcher StringMatcher + prefix string + suffix string + contains string + + // matchString is the "compiled" function to run by MatchString(). + matchString func(string) bool } func NewFastRegexMatcher(v string) (*FastRegexMatcher, error) { - if isLiteral(v) { - return &FastRegexMatcher{literal: true, value: v}, nil - } - re, err := regexp.Compile("^(?:" + v + ")$") - if err != nil { - return nil, err - } - - parsed, err := syntax.Parse(v, syntax.Perl) - if err != nil { - return nil, err - } - m := &FastRegexMatcher{ - re: re, + reString: v, } - if parsed.Op == syntax.OpConcat { - m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed) + m.stringMatcher, m.setMatches = optimizeAlternatingLiterals(v) + if m.stringMatcher != nil { + // If we already have a string matcher, we don't need to parse the regex + // or compile the matchString function. This also avoids the behavior in + // compileMatchStringFunction where it prefers to use setMatches when + // available, even if the string matcher is faster. + m.matchString = m.stringMatcher.Matches + } else { + parsed, err := syntax.Parse(v, syntax.Perl) + if err != nil { + return nil, err + } + // Simplify the syntax tree to run faster. + parsed = parsed.Simplify() + m.re, err = regexp.Compile("^(?:" + parsed.String() + ")$") + if err != nil { + return nil, err + } + if parsed.Op == syntax.OpConcat { + m.prefix, m.suffix, m.contains = optimizeConcatRegex(parsed) + } + if matches, caseSensitive := findSetMatches(parsed); caseSensitive { + m.setMatches = matches + } + m.stringMatcher = stringMatcherFromRegexp(parsed) + m.matchString = m.compileMatchStringFunction() } return m, nil } +// compileMatchStringFunction returns the function to run by MatchString(). +func (m *FastRegexMatcher) compileMatchStringFunction() func(string) bool { + // If the only optimization available is the string matcher, then we can just run it. + if len(m.setMatches) == 0 && m.prefix == "" && m.suffix == "" && m.contains == "" && m.stringMatcher != nil { + return m.stringMatcher.Matches + } + + return func(s string) bool { + if len(m.setMatches) != 0 { + for _, match := range m.setMatches { + if match == s { + return true + } + } + return false + } + if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { + return false + } + if m.suffix != "" && !strings.HasSuffix(s, m.suffix) { + return false + } + if m.contains != "" && !strings.Contains(s, m.contains) { + return false + } + if m.stringMatcher != nil { + return m.stringMatcher.Matches(s) + } + return m.re.MatchString(s) + } +} + +// IsOptimized returns true if any fast-path optimization is applied to the +// regex matcher. +func (m *FastRegexMatcher) IsOptimized() bool { + return len(m.setMatches) > 0 || m.stringMatcher != nil || m.prefix != "" || m.suffix != "" || m.contains != "" +} + +// findSetMatches extract equality matches from a regexp. +// Returns nil if we can't replace the regexp by only equality matchers or the regexp contains +// a mix of case sensitive and case insensitive matchers. +func findSetMatches(re *syntax.Regexp) (matches []string, caseSensitive bool) { + clearBeginEndText(re) + + return findSetMatchesInternal(re, "") +} + +func findSetMatchesInternal(re *syntax.Regexp, base string) (matches []string, caseSensitive bool) { + switch re.Op { + case syntax.OpBeginText: + // Correctly handling the begin text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil, false + case syntax.OpEndText: + // Correctly handling the end text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil, false + case syntax.OpLiteral: + return []string{base + string(re.Rune)}, isCaseSensitive(re) + case syntax.OpEmptyMatch: + if base != "" { + return []string{base}, isCaseSensitive(re) + } + case syntax.OpAlternate: + return findSetMatchesFromAlternate(re, base) + case syntax.OpCapture: + clearCapture(re) + return findSetMatchesInternal(re, base) + case syntax.OpConcat: + return findSetMatchesFromConcat(re, base) + case syntax.OpCharClass: + if len(re.Rune)%2 != 0 { + return nil, false + } + var matches []string + var totalSet int + for i := 0; i+1 < len(re.Rune); i += 2 { + totalSet += int(re.Rune[i+1]-re.Rune[i]) + 1 + } + // limits the total characters that can be used to create matches. + // In some case like negation [^0-9] a lot of possibilities exists and that + // can create thousands of possible matches at which points we're better off using regexp. + if totalSet > maxSetMatches { + return nil, false + } + for i := 0; i+1 < len(re.Rune); i += 2 { + lo, hi := re.Rune[i], re.Rune[i+1] + for c := lo; c <= hi; c++ { + matches = append(matches, base+string(c)) + } + } + return matches, isCaseSensitive(re) + default: + return nil, false + } + return nil, false +} + +func findSetMatchesFromConcat(re *syntax.Regexp, base string) (matches []string, matchesCaseSensitive bool) { + if len(re.Sub) == 0 { + return nil, false + } + clearCapture(re.Sub...) + + matches = []string{base} + + for i := 0; i < len(re.Sub); i++ { + var newMatches []string + for j, b := range matches { + m, caseSensitive := findSetMatchesInternal(re.Sub[i], b) + if m == nil { + return nil, false + } + if tooManyMatches(newMatches, m...) { + return nil, false + } + + // All matches must have the same case sensitivity. If it's the first set of matches + // returned, we store its sensitivity as the expected case, and then we'll check all + // other ones. + if i == 0 && j == 0 { + matchesCaseSensitive = caseSensitive + } + if matchesCaseSensitive != caseSensitive { + return nil, false + } + + newMatches = append(newMatches, m...) + } + matches = newMatches + } + + return matches, matchesCaseSensitive +} + +func findSetMatchesFromAlternate(re *syntax.Regexp, base string) (matches []string, matchesCaseSensitive bool) { + for i, sub := range re.Sub { + found, caseSensitive := findSetMatchesInternal(sub, base) + if found == nil { + return nil, false + } + if tooManyMatches(matches, found...) { + return nil, false + } + + // All matches must have the same case sensitivity. If it's the first set of matches + // returned, we store its sensitivity as the expected case, and then we'll check all + // other ones. + if i == 0 { + matchesCaseSensitive = caseSensitive + } + if matchesCaseSensitive != caseSensitive { + return nil, false + } + + matches = append(matches, found...) + } + + return matches, matchesCaseSensitive +} + +// clearCapture removes capture operation as they are not used for matching. +func clearCapture(regs ...*syntax.Regexp) { + for _, r := range regs { + // Iterate on the regexp because capture groups could be nested. + for r.Op == syntax.OpCapture { + *r = *r.Sub[0] + } + } +} + +// clearBeginEndText removes the begin and end text from the regexp. Prometheus regexp are anchored to the beginning and end of the string. +func clearBeginEndText(re *syntax.Regexp) { + // Do not clear begin/end text from an alternate operator because it could + // change the actual regexp properties. + if re.Op == syntax.OpAlternate { + return + } + + if len(re.Sub) == 0 { + return + } + if len(re.Sub) == 1 { + if re.Sub[0].Op == syntax.OpBeginText || re.Sub[0].Op == syntax.OpEndText { + // We need to remove this element. Since it's the only one, we convert into a matcher of an empty string. + // OpEmptyMatch is regexp's nop operator. + re.Op = syntax.OpEmptyMatch + re.Sub = nil + return + } + } + if re.Sub[0].Op == syntax.OpBeginText { + re.Sub = re.Sub[1:] + } + if re.Sub[len(re.Sub)-1].Op == syntax.OpEndText { + re.Sub = re.Sub[:len(re.Sub)-1] + } +} + +// isCaseInsensitive tells if a regexp is case insensitive. +// The flag should be check at each level of the syntax tree. +func isCaseInsensitive(reg *syntax.Regexp) bool { + return (reg.Flags & syntax.FoldCase) != 0 +} + +// isCaseSensitive tells if a regexp is case sensitive. +// The flag should be check at each level of the syntax tree. +func isCaseSensitive(reg *syntax.Regexp) bool { + return !isCaseInsensitive(reg) +} + +// tooManyMatches guards against creating too many set matches. +func tooManyMatches(matches []string, added ...string) bool { + return len(matches)+len(added) > maxSetMatches +} + func (m *FastRegexMatcher) MatchString(s string) bool { - if m.literal { - return s == m.value - } - if m.prefix != "" && !strings.HasPrefix(s, m.prefix) { - return false - } - if m.suffix != "" && !strings.HasSuffix(s, m.suffix) { - return false - } - if m.contains != "" && !strings.Contains(s, m.contains) { - return false - } - return m.re.MatchString(s) + return m.matchString(s) +} + +func (m *FastRegexMatcher) SetMatches() []string { + // IMPORTANT: always return a copy, otherwise if the caller manipulate this slice it will + // also get manipulated in the cached FastRegexMatcher instance. + return slices.Clone(m.setMatches) } func (m *FastRegexMatcher) GetRegexString() string { - if m.literal { - return m.value - } - return m.re.String() + return m.reString } -func isLiteral(re string) bool { - return regexp.QuoteMeta(re) == re +// optimizeAlternatingLiterals optimizes a regex of the form +// +// `literal1|literal2|literal3|...` +// +// this function returns an optimized StringMatcher or nil if the regex +// cannot be optimized in this way, and a list of setMatches up to maxSetMatches. +func optimizeAlternatingLiterals(s string) (StringMatcher, []string) { + if len(s) == 0 { + return emptyStringMatcher{}, nil + } + + estimatedAlternates := strings.Count(s, "|") + 1 + + // If there are no alternates, check if the string is a literal + if estimatedAlternates == 1 { + if regexp.QuoteMeta(s) == s { + return &equalStringMatcher{s: s, caseSensitive: true}, []string{s} + } + return nil, nil + } + + multiMatcher := newEqualMultiStringMatcher(true, estimatedAlternates) + + for end := strings.IndexByte(s, '|'); end > -1; end = strings.IndexByte(s, '|') { + // Split the string into the next literal and the remainder + subMatch := s[:end] + s = s[end+1:] + + // break if any of the submatches are not literals + if regexp.QuoteMeta(subMatch) != subMatch { + return nil, nil + } + + multiMatcher.add(subMatch) + } + + // break if the remainder is not a literal + if regexp.QuoteMeta(s) != s { + return nil, nil + } + multiMatcher.add(s) + + return multiMatcher, multiMatcher.setMatches() } // optimizeConcatRegex returns literal prefix/suffix text that can be safely @@ -123,3 +398,540 @@ func optimizeConcatRegex(r *syntax.Regexp) (prefix, suffix, contains string) { return } + +// StringMatcher is a matcher that matches a string in place of a regular expression. +type StringMatcher interface { + Matches(s string) bool +} + +// stringMatcherFromRegexp attempts to replace a common regexp with a string matcher. +// It returns nil if the regexp is not supported. +func stringMatcherFromRegexp(re *syntax.Regexp) StringMatcher { + clearBeginEndText(re) + + m := stringMatcherFromRegexpInternal(re) + m = optimizeEqualStringMatchers(m, minEqualMultiStringMatcherMapThreshold) + + return m +} + +func stringMatcherFromRegexpInternal(re *syntax.Regexp) StringMatcher { + clearCapture(re) + + switch re.Op { + case syntax.OpBeginText: + // Correctly handling the begin text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil + case syntax.OpEndText: + // Correctly handling the end text operator inside a regex is tricky, + // so in this case we fallback to the regex engine. + return nil + case syntax.OpPlus: + if re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL { + return nil + } + return &anyNonEmptyStringMatcher{ + matchNL: re.Sub[0].Op == syntax.OpAnyChar, + } + case syntax.OpStar: + if re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL { + return nil + } + + // If the newline is valid, than this matcher literally match any string (even empty). + if re.Sub[0].Op == syntax.OpAnyChar { + return trueMatcher{} + } + + // Any string is fine (including an empty one), as far as it doesn't contain any newline. + return anyStringWithoutNewlineMatcher{} + case syntax.OpQuest: + // Only optimize for ".?". + if len(re.Sub) != 1 || (re.Sub[0].Op != syntax.OpAnyChar && re.Sub[0].Op != syntax.OpAnyCharNotNL) { + return nil + } + + return &zeroOrOneCharacterStringMatcher{ + matchNL: re.Sub[0].Op == syntax.OpAnyChar, + } + case syntax.OpEmptyMatch: + return emptyStringMatcher{} + + case syntax.OpLiteral: + return &equalStringMatcher{ + s: string(re.Rune), + caseSensitive: !isCaseInsensitive(re), + } + case syntax.OpAlternate: + or := make([]StringMatcher, 0, len(re.Sub)) + for _, sub := range re.Sub { + m := stringMatcherFromRegexpInternal(sub) + if m == nil { + return nil + } + or = append(or, m) + } + return orStringMatcher(or) + case syntax.OpConcat: + clearCapture(re.Sub...) + + if len(re.Sub) == 0 { + return emptyStringMatcher{} + } + if len(re.Sub) == 1 { + return stringMatcherFromRegexpInternal(re.Sub[0]) + } + + var left, right StringMatcher + + // Let's try to find if there's a first and last any matchers. + if re.Sub[0].Op == syntax.OpPlus || re.Sub[0].Op == syntax.OpStar || re.Sub[0].Op == syntax.OpQuest { + left = stringMatcherFromRegexpInternal(re.Sub[0]) + if left == nil { + return nil + } + re.Sub = re.Sub[1:] + } + if re.Sub[len(re.Sub)-1].Op == syntax.OpPlus || re.Sub[len(re.Sub)-1].Op == syntax.OpStar || re.Sub[len(re.Sub)-1].Op == syntax.OpQuest { + right = stringMatcherFromRegexpInternal(re.Sub[len(re.Sub)-1]) + if right == nil { + return nil + } + re.Sub = re.Sub[:len(re.Sub)-1] + } + + matches, matchesCaseSensitive := findSetMatchesInternal(re, "") + + if len(matches) == 0 && len(re.Sub) == 2 { + // We have not find fixed set matches. We look for other known cases that + // we can optimize. + switch { + // Prefix is literal. + case right == nil && re.Sub[0].Op == syntax.OpLiteral: + right = stringMatcherFromRegexpInternal(re.Sub[1]) + if right != nil { + matches = []string{string(re.Sub[0].Rune)} + matchesCaseSensitive = !isCaseInsensitive(re.Sub[0]) + } + + // Suffix is literal. + case left == nil && re.Sub[1].Op == syntax.OpLiteral: + left = stringMatcherFromRegexpInternal(re.Sub[0]) + if left != nil { + matches = []string{string(re.Sub[1].Rune)} + matchesCaseSensitive = !isCaseInsensitive(re.Sub[1]) + } + } + } + + // Ensure we've found some literals to match (optionally with a left and/or right matcher). + // If not, then this optimization doesn't trigger. + if len(matches) == 0 { + return nil + } + + // Use the right (and best) matcher based on what we've found. + switch { + // No left and right matchers (only fixed set matches). + case left == nil && right == nil: + // if there's no any matchers on both side it's a concat of literals + or := make([]StringMatcher, 0, len(matches)) + for _, match := range matches { + or = append(or, &equalStringMatcher{ + s: match, + caseSensitive: matchesCaseSensitive, + }) + } + return orStringMatcher(or) + + // Right matcher with 1 fixed set match. + case left == nil && len(matches) == 1: + return &literalPrefixStringMatcher{ + prefix: matches[0], + prefixCaseSensitive: matchesCaseSensitive, + right: right, + } + + // Left matcher with 1 fixed set match. + case right == nil && len(matches) == 1: + return &literalSuffixStringMatcher{ + left: left, + suffix: matches[0], + suffixCaseSensitive: matchesCaseSensitive, + } + + // We found literals in the middle. We can trigger the fast path only if + // the matches are case sensitive because containsStringMatcher doesn't + // support case insensitive. + case matchesCaseSensitive: + return &containsStringMatcher{ + substrings: matches, + left: left, + right: right, + } + } + } + return nil +} + +// containsStringMatcher matches a string if it contains any of the substrings. +// If left and right are not nil, it's a contains operation where left and right must match. +// If left is nil, it's a hasPrefix operation and right must match. +// Finally, if right is nil it's a hasSuffix operation and left must match. +type containsStringMatcher struct { + // The matcher that must match the left side. Can be nil. + left StringMatcher + + // At least one of these strings must match in the "middle", between left and right matchers. + substrings []string + + // The matcher that must match the right side. Can be nil. + right StringMatcher +} + +func (m *containsStringMatcher) Matches(s string) bool { + for _, substr := range m.substrings { + switch { + case m.right != nil && m.left != nil: + searchStartPos := 0 + + for { + pos := strings.Index(s[searchStartPos:], substr) + if pos < 0 { + break + } + + // Since we started searching from searchStartPos, we have to add that offset + // to get the actual position of the substring inside the text. + pos += searchStartPos + + // If both the left and right matchers match, then we can stop searching because + // we've found a match. + if m.left.Matches(s[:pos]) && m.right.Matches(s[pos+len(substr):]) { + return true + } + + // Continue searching for another occurrence of the substring inside the text. + searchStartPos = pos + 1 + } + case m.left != nil: + // If we have to check for characters on the left then we need to match a suffix. + if strings.HasSuffix(s, substr) && m.left.Matches(s[:len(s)-len(substr)]) { + return true + } + case m.right != nil: + if strings.HasPrefix(s, substr) && m.right.Matches(s[len(substr):]) { + return true + } + } + } + return false +} + +// literalPrefixStringMatcher matches a string with the given literal prefix and right side matcher. +type literalPrefixStringMatcher struct { + prefix string + prefixCaseSensitive bool + + // The matcher that must match the right side. Can be nil. + right StringMatcher +} + +func (m *literalPrefixStringMatcher) Matches(s string) bool { + // Ensure the prefix matches. + if m.prefixCaseSensitive && !strings.HasPrefix(s, m.prefix) { + return false + } + if !m.prefixCaseSensitive && !hasPrefixCaseInsensitive(s, m.prefix) { + return false + } + + // Ensure the right side matches. + return m.right.Matches(s[len(m.prefix):]) +} + +// literalSuffixStringMatcher matches a string with the given literal suffix and left side matcher. +type literalSuffixStringMatcher struct { + // The matcher that must match the left side. Can be nil. + left StringMatcher + + suffix string + suffixCaseSensitive bool +} + +func (m *literalSuffixStringMatcher) Matches(s string) bool { + // Ensure the suffix matches. + if m.suffixCaseSensitive && !strings.HasSuffix(s, m.suffix) { + return false + } + if !m.suffixCaseSensitive && !hasSuffixCaseInsensitive(s, m.suffix) { + return false + } + + // Ensure the left side matches. + return m.left.Matches(s[:len(s)-len(m.suffix)]) +} + +// emptyStringMatcher matches an empty string. +type emptyStringMatcher struct{} + +func (m emptyStringMatcher) Matches(s string) bool { + return len(s) == 0 +} + +// orStringMatcher matches any of the sub-matchers. +type orStringMatcher []StringMatcher + +func (m orStringMatcher) Matches(s string) bool { + for _, matcher := range m { + if matcher.Matches(s) { + return true + } + } + return false +} + +// equalStringMatcher matches a string exactly and support case insensitive. +type equalStringMatcher struct { + s string + caseSensitive bool +} + +func (m *equalStringMatcher) Matches(s string) bool { + if m.caseSensitive { + return m.s == s + } + return strings.EqualFold(m.s, s) +} + +type multiStringMatcherBuilder interface { + StringMatcher + add(s string) + setMatches() []string +} + +func newEqualMultiStringMatcher(caseSensitive bool, estimatedSize int) multiStringMatcherBuilder { + // If the estimated size is low enough, it's faster to use a slice instead of a map. + if estimatedSize < minEqualMultiStringMatcherMapThreshold { + return &equalMultiStringSliceMatcher{caseSensitive: caseSensitive, values: make([]string, 0, estimatedSize)} + } + + return &equalMultiStringMapMatcher{ + values: make(map[string]struct{}, estimatedSize), + caseSensitive: caseSensitive, + } +} + +// equalMultiStringSliceMatcher matches a string exactly against a slice of valid values. +type equalMultiStringSliceMatcher struct { + values []string + + caseSensitive bool +} + +func (m *equalMultiStringSliceMatcher) add(s string) { + m.values = append(m.values, s) +} + +func (m *equalMultiStringSliceMatcher) setMatches() []string { + return m.values +} + +func (m *equalMultiStringSliceMatcher) Matches(s string) bool { + if m.caseSensitive { + for _, v := range m.values { + if s == v { + return true + } + } + } else { + for _, v := range m.values { + if strings.EqualFold(s, v) { + return true + } + } + } + return false +} + +// equalMultiStringMapMatcher matches a string exactly against a map of valid values. +type equalMultiStringMapMatcher struct { + // values contains values to match a string against. If the matching is case insensitive, + // the values here must be lowercase. + values map[string]struct{} + + caseSensitive bool +} + +func (m *equalMultiStringMapMatcher) add(s string) { + if !m.caseSensitive { + s = strings.ToLower(s) + } + + m.values[s] = struct{}{} +} + +func (m *equalMultiStringMapMatcher) setMatches() []string { + if len(m.values) >= maxSetMatches { + return nil + } + + matches := make([]string, 0, len(m.values)) + for s := range m.values { + matches = append(matches, s) + } + return matches +} + +func (m *equalMultiStringMapMatcher) Matches(s string) bool { + if !m.caseSensitive { + s = strings.ToLower(s) + } + + _, ok := m.values[s] + return ok +} + +// anyStringWithoutNewlineMatcher is a stringMatcher which matches any string +// (including an empty one) as far as it doesn't contain any newline character. +type anyStringWithoutNewlineMatcher struct{} + +func (m anyStringWithoutNewlineMatcher) Matches(s string) bool { + // We need to make sure it doesn't contain a newline. Since the newline is + // an ASCII character, we can use strings.IndexByte(). + return strings.IndexByte(s, '\n') == -1 +} + +// anyNonEmptyStringMatcher is a stringMatcher which matches any non-empty string. +type anyNonEmptyStringMatcher struct { + matchNL bool +} + +func (m *anyNonEmptyStringMatcher) Matches(s string) bool { + if m.matchNL { + // It's OK if the string contains a newline so we just need to make + // sure it's non-empty. + return len(s) > 0 + } + + // We need to make sure it non-empty and doesn't contain a newline. + // Since the newline is an ASCII character, we can use strings.IndexByte(). + return len(s) > 0 && strings.IndexByte(s, '\n') == -1 +} + +// zeroOrOneCharacterStringMatcher is a StringMatcher which matches zero or one occurrence +// of any character. The newline character is matches only if matchNL is set to true. +type zeroOrOneCharacterStringMatcher struct { + matchNL bool +} + +func (m *zeroOrOneCharacterStringMatcher) Matches(s string) bool { + // Zero or one. + if len(s) > 1 { + return false + } + + // No need to check for the newline if the string is empty or matching a newline is OK. + if m.matchNL || len(s) == 0 { + return true + } + + return s[0] != '\n' +} + +// trueMatcher is a stringMatcher which matches any string (always returns true). +type trueMatcher struct{} + +func (m trueMatcher) Matches(_ string) bool { + return true +} + +// optimizeEqualStringMatchers optimize a specific case where all matchers are made by an +// alternation (orStringMatcher) of strings checked for equality (equalStringMatcher). In +// this specific case, when we have many strings to match against we can use a map instead +// of iterating over the list of strings. +func optimizeEqualStringMatchers(input StringMatcher, threshold int) StringMatcher { + var ( + caseSensitive bool + caseSensitiveSet bool + numValues int + ) + + // Analyse the input StringMatcher to count the number of occurrences + // and ensure all of them have the same case sensitivity. + analyseCallback := func(matcher *equalStringMatcher) bool { + // Ensure we don't have mixed case sensitivity. + if caseSensitiveSet && caseSensitive != matcher.caseSensitive { + return false + } else if !caseSensitiveSet { + caseSensitive = matcher.caseSensitive + caseSensitiveSet = true + } + + numValues++ + return true + } + + if !findEqualStringMatchers(input, analyseCallback) { + return input + } + + // If the number of values found is less than the threshold, then we should skip the optimization. + if numValues < threshold { + return input + } + + // Parse again the input StringMatcher to extract all values and storing them. + // We can skip the case sensitivity check because we've already checked it and + // if the code reach this point then it means all matchers have the same case sensitivity. + multiMatcher := newEqualMultiStringMatcher(caseSensitive, numValues) + + // Ignore the return value because we already iterated over the input StringMatcher + // and it was all good. + findEqualStringMatchers(input, func(matcher *equalStringMatcher) bool { + multiMatcher.add(matcher.s) + return true + }) + + return multiMatcher +} + +// findEqualStringMatchers analyze the input StringMatcher and calls the callback for each +// equalStringMatcher found. Returns true if and only if the input StringMatcher is *only* +// composed by an alternation of equalStringMatcher. +func findEqualStringMatchers(input StringMatcher, callback func(matcher *equalStringMatcher) bool) bool { + orInput, ok := input.(orStringMatcher) + if !ok { + return false + } + + for _, m := range orInput { + switch casted := m.(type) { + case orStringMatcher: + if !findEqualStringMatchers(m, callback) { + return false + } + + case *equalStringMatcher: + if !callback(casted) { + return false + } + + default: + // It's not an equal string matcher, so we have to stop searching + // cause this optimization can't be applied. + return false + } + } + + return true +} + +func hasPrefixCaseInsensitive(s, prefix string) bool { + return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix) +} + +func hasSuffixCaseInsensitive(s, suffix string) bool { + return len(s) >= len(suffix) && strings.EqualFold(s[len(s)-len(suffix):], suffix) +} diff --git a/model/labels/regexp_test.go b/model/labels/regexp_test.go index 3188f7cefc..fc21459ed0 100644 --- a/model/labels/regexp_test.go +++ b/model/labels/regexp_test.go @@ -14,49 +14,100 @@ package labels import ( + "fmt" + "math/rand" "strings" "testing" + "time" + "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" "github.com/stretchr/testify/require" ) -func TestNewFastRegexMatcher(t *testing.T) { - cases := []struct { - regex string - value string - expected bool - }{ - {regex: "(foo|bar)", value: "foo", expected: true}, - {regex: "(foo|bar)", value: "foo bar", expected: false}, - {regex: "(foo|bar)", value: "bar", expected: true}, - {regex: "foo.*", value: "foo bar", expected: true}, - {regex: "foo.*", value: "bar foo", expected: false}, - {regex: ".*foo", value: "foo bar", expected: false}, - {regex: ".*foo", value: "bar foo", expected: true}, - {regex: ".*foo", value: "foo", expected: true}, - {regex: "^.*foo$", value: "foo", expected: true}, - {regex: "^.+foo$", value: "foo", expected: false}, - {regex: "^.+foo$", value: "bfoo", expected: true}, - {regex: ".*", value: "\n", expected: false}, - {regex: ".*", value: "\nfoo", expected: false}, - {regex: ".*foo", value: "\nfoo", expected: false}, - {regex: "foo.*", value: "foo\n", expected: false}, - {regex: "foo\n.*", value: "foo\n", expected: true}, - {regex: ".*foo.*", value: "foo", expected: true}, - {regex: ".*foo.*", value: "foo bar", expected: true}, - {regex: ".*foo.*", value: "hello foo world", expected: true}, - {regex: ".*foo.*", value: "hello foo\n world", expected: false}, - {regex: ".*foo\n.*", value: "hello foo\n world", expected: true}, - {regex: ".*", value: "foo", expected: true}, - {regex: "", value: "foo", expected: false}, - {regex: "", value: "", expected: true}, +var ( + asciiRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_") + regexes = []string{ + "", + "foo", + "^foo", + "(foo|bar)", + "foo.*", + ".*foo", + "^.*foo$", + "^.+foo$", + ".*", + ".+", + "foo.+", + ".+foo", + "foo\n.+", + "foo\n.*", + ".*foo.*", + ".+foo.+", + "(?s:.*)", + "(?s:.+)", + "(?s:^.*foo$)", + "(?i:foo)", + "(?i:(foo|bar))", + "(?i:(foo1|foo2|bar))", + "^(?i:foo|oo)|(bar)$", + "(?i:(foo1|foo2|aaa|bbb|ccc|ddd|eee|fff|ggg|hhh|iii|lll|mmm|nnn|ooo|ppp|qqq|rrr|sss|ttt|uuu|vvv|www|xxx|yyy|zzz))", + "((.*)(bar|b|buzz)(.+)|foo)$", + "^$", + "(prometheus|api_prom)_api_v1_.+", + "10\\.0\\.(1|2)\\.+", + "10\\.0\\.(1|2).+", + "((fo(bar))|.+foo)", + // A long case sensitive alternation. + "zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKMimS|IecrXtPa|seTckYqt|NxnyHkgB|fIDlOgKb|UhlWIygH|OtNoJxHG|cUTkFVIV|mTgFIHjr|jQkoIDtE|PPMKxRXl|AwMfwVkQ|CQyMrTQJ|BzrqxVSi|nTpcWuhF|PertdywG|ZZDgCtXN|WWdDPyyE|uVtNQsKk|BdeCHvPZ|wshRnFlH|aOUIitIp|RxZeCdXT|CFZMslCj|AVBZRDxl|IzIGCnhw|ythYuWiz|oztXVXhl|VbLkwqQx|qvaUgyVC|VawUjPWC|ecloYJuj|boCLTdSU|uPrKeAZx|hrMWLWBq|JOnUNHRM|rYnujkPq|dDEdZhIj|DRrfvugG|yEGfDxVV|YMYdJWuP|PHUQZNWM|AmKNrLis|zTxndVfn|FPsHoJnc|EIulZTua|KlAPhdzg|ScHJJCLt|NtTfMzME|eMCwuFdo|SEpJVJbR|cdhXZeCx|sAVtBwRh|kVFEVcMI|jzJrxraA|tGLHTell|NNWoeSaw|DcOKSetX|UXZAJyka|THpMphDP|rizheevl|kDCBRidd|pCZZRqyu|pSygkitl|SwZGkAaW|wILOrfNX|QkwVOerj|kHOMxPDr|EwOVycJv|AJvtzQFS|yEOjKYYB|LizIINLL|JBRSsfcG|YPiUqqNl|IsdEbvee|MjEpGcBm|OxXZVgEQ|xClXGuxa|UzRCGFEb|buJbvfvA|IPZQxRet|oFYShsMc|oBHffuHO|bzzKrcBR|KAjzrGCl|IPUsAVls|OGMUMbIU|gyDccHuR|bjlalnDd|ZLWjeMna|fdsuIlxQ|dVXtiomV|XxedTjNg|XWMHlNoA|nnyqArQX|opfkWGhb|wYtnhdYb", + // An extremely long case sensitive alternation. This is a special + // case because the values share common prefixes rather than being + // entirely random. This is common in the real world. For example, the + // values of a label like kubernetes pod will often include the + // deployment name as a prefix. + "jyyfj00j0061|jyyfj00j0062|jyyfj94j0093|jyyfj99j0093|jyyfm01j0021|jyyfm02j0021|jyefj00j0192|jyefj00j0193|jyefj00j0194|jyefj00j0195|jyefj00j0196|jyefj00j0197|jyefj00j0290|jyefj00j0291|jyefj00j0292|jyefj00j0293|jyefj00j0294|jyefj00j0295|jyefj00j0296|jyefj00j0297|jyefj89j0394|jyefj90j0394|jyefj91j0394|jyefj95j0347|jyefj96j0322|jyefj96j0347|jyefj97j0322|jyefj97j0347|jyefj98j0322|jyefj98j0347|jyefj99j0320|jyefj99j0322|jyefj99j0323|jyefj99j0335|jyefj99j0336|jyefj99j0344|jyefj99j0347|jyefj99j0349|jyefj99j0351|jyeff00j0117|lyyfm01j0025|lyyfm01j0028|lyyfm01j0041|lyyfm01j0133|lyyfm01j0701|lyyfm02j0025|lyyfm02j0028|lyyfm02j0041|lyyfm02j0133|lyyfm02j0701|lyyfm03j0701|lyefj00j0775|lyefj00j0776|lyefj00j0777|lyefj00j0778|lyefj00j0779|lyefj00j0780|lyefj00j0781|lyefj00j0782|lyefj50j3807|lyefj50j3852|lyefj51j3807|lyefj51j3852|lyefj52j3807|lyefj52j3852|lyefj53j3807|lyefj53j3852|lyefj54j3807|lyefj54j3852|lyefj54j3886|lyefj55j3807|lyefj55j3852|lyefj55j3886|lyefj56j3807|lyefj56j3852|lyefj56j3886|lyefj57j3807|lyefj57j3852|lyefj57j3886|lyefj58j3807|lyefj58j3852|lyefj58j3886|lyefj59j3807|lyefj59j3852|lyefj59j3886|lyefj60j3807|lyefj60j3852|lyefj60j3886|lyefj61j3807|lyefj61j3852|lyefj61j3886|lyefj62j3807|lyefj62j3852|lyefj62j3886|lyefj63j3807|lyefj63j3852|lyefj63j3886|lyefj64j3807|lyefj64j3852|lyefj64j3886|lyefj65j3807|lyefj65j3852|lyefj65j3886|lyefj66j3807|lyefj66j3852|lyefj66j3886|lyefj67j3807|lyefj67j3852|lyefj67j3886|lyefj68j3807|lyefj68j3852|lyefj68j3886|lyefj69j3807|lyefj69j3846|lyefj69j3852|lyefj69j3886|lyefj70j3807|lyefj70j3846|lyefj70j3852|lyefj70j3886|lyefj71j3807|lyefj71j3846|lyefj71j3852|lyefj71j3886|lyefj72j3807|lyefj72j3846|lyefj72j3852|lyefj72j3886|lyefj73j3807|lyefj73j3846|lyefj73j3852|lyefj73j3886|lyefj74j3807|lyefj74j3846|lyefj74j3852|lyefj74j3886|lyefj75j3807|lyefj75j3808|lyefj75j3846|lyefj75j3852|lyefj75j3886|lyefj76j3732|lyefj76j3807|lyefj76j3808|lyefj76j3846|lyefj76j3852|lyefj76j3886|lyefj77j3732|lyefj77j3807|lyefj77j3808|lyefj77j3846|lyefj77j3852|lyefj77j3886|lyefj78j3278|lyefj78j3732|lyefj78j3807|lyefj78j3808|lyefj78j3846|lyefj78j3852|lyefj78j3886|lyefj79j3732|lyefj79j3807|lyefj79j3808|lyefj79j3846|lyefj79j3852|lyefj79j3886|lyefj80j3732|lyefj80j3807|lyefj80j3808|lyefj80j3846|lyefj80j3852|lyefj80j3886|lyefj81j3732|lyefj81j3807|lyefj81j3808|lyefj81j3846|lyefj81j3852|lyefj81j3886|lyefj82j3732|lyefj82j3807|lyefj82j3808|lyefj82j3846|lyefj82j3852|lyefj82j3886|lyefj83j3732|lyefj83j3807|lyefj83j3808|lyefj83j3846|lyefj83j3852|lyefj83j3886|lyefj84j3732|lyefj84j3807|lyefj84j3808|lyefj84j3846|lyefj84j3852|lyefj84j3886|lyefj85j3732|lyefj85j3807|lyefj85j3808|lyefj85j3846|lyefj85j3852|lyefj85j3886|lyefj86j3278|lyefj86j3732|lyefj86j3807|lyefj86j3808|lyefj86j3846|lyefj86j3852|lyefj86j3886|lyefj87j3278|lyefj87j3732|lyefj87j3807|lyefj87j3808|lyefj87j3846|lyefj87j3852|lyefj87j3886|lyefj88j3732|lyefj88j3807|lyefj88j3808|lyefj88j3846|lyefj88j3852|lyefj88j3886|lyefj89j3732|lyefj89j3807|lyefj89j3808|lyefj89j3846|lyefj89j3852|lyefj89j3886|lyefj90j3732|lyefj90j3807|lyefj90j3808|lyefj90j3846|lyefj90j3852|lyefj90j3886|lyefj91j3732|lyefj91j3807|lyefj91j3808|lyefj91j3846|lyefj91j3852|lyefj91j3886|lyefj92j3732|lyefj92j3807|lyefj92j3808|lyefj92j3846|lyefj92j3852|lyefj92j3886|lyefj93j3732|lyefj93j3807|lyefj93j3808|lyefj93j3846|lyefj93j3852|lyefj93j3885|lyefj93j3886|lyefj94j3525|lyefj94j3732|lyefj94j3807|lyefj94j3808|lyefj94j3846|lyefj94j3852|lyefj94j3885|lyefj94j3886|lyefj95j3525|lyefj95j3732|lyefj95j3807|lyefj95j3808|lyefj95j3846|lyefj95j3852|lyefj95j3886|lyefj96j3732|lyefj96j3803|lyefj96j3807|lyefj96j3808|lyefj96j3846|lyefj96j3852|lyefj96j3886|lyefj97j3333|lyefj97j3732|lyefj97j3792|lyefj97j3803|lyefj97j3807|lyefj97j3808|lyefj97j3838|lyefj97j3843|lyefj97j3846|lyefj97j3852|lyefj97j3886|lyefj98j3083|lyefj98j3333|lyefj98j3732|lyefj98j3807|lyefj98j3808|lyefj98j3838|lyefj98j3843|lyefj98j3846|lyefj98j3852|lyefj98j3873|lyefj98j3877|lyefj98j3882|lyefj98j3886|lyefj99j2984|lyefj99j3083|lyefj99j3333|lyefj99j3732|lyefj99j3807|lyefj99j3808|lyefj99j3846|lyefj99j3849|lyefj99j3852|lyefj99j3873|lyefj99j3877|lyefj99j3882|lyefj99j3884|lyefj99j3886|lyeff00j0106|lyeff00j0107|lyeff00j0108|lyeff00j0129|lyeff00j0130|lyeff00j0131|lyeff00j0132|lyeff00j0133|lyeff00j0134|lyeff00j0444|lyeff00j0445|lyeff91j0473|lyeff92j0473|lyeff92j3877|lyeff93j3877|lyeff94j0501|lyeff94j3525|lyeff94j3877|lyeff95j0501|lyeff95j3525|lyeff95j3877|lyeff96j0503|lyeff96j3877|lyeff97j3877|lyeff98j3333|lyeff98j3877|lyeff99j2984|lyeff99j3333|lyeff99j3877|mfyr9149ej|mfyr9149ek|mfyr9156ej|mfyr9156ek|mfyr9157ej|mfyr9157ek|mfyr9159ej|mfyr9159ek|mfyr9203ej|mfyr9204ej|mfyr9205ej|mfyr9206ej|mfyr9207ej|mfyr9207ek|mfyr9217ej|mfyr9217ek|mfyr9222ej|mfyr9222ek|mfyu0185ej|mfye9187ej|mfye9187ek|mfye9188ej|mfye9188ek|mfye9189ej|mfye9189ek|mfyf0185ej|oyefj87j0007|oyefj88j0007|oyefj89j0007|oyefj90j0007|oyefj91j0007|oyefj95j0001|oyefj96j0001|oyefj98j0004|oyefj99j0004|oyeff91j0004|oyeff92j0004|oyeff93j0004|oyeff94j0004|oyeff95j0004|oyeff96j0004|rklvyaxmany|ryefj93j0001|ryefj94j0001|tyyfj00a0001|tyyfj84j0005|tyyfj85j0005|tyyfj86j0005|tyyfj87j0005|tyyfj88j0005|tyyfj89j0005|tyyfj90j0005|tyyfj91j0005|tyyfj92j0005|tyyfj93j0005|tyyfj94j0005|tyyfj95j0005|tyyfj96j0005|tyyfj97j0005|tyyfj98j0005|tyyfj99j0005|tyefj50j0015|tyefj50j0017|tyefj50j0019|tyefj50j0020|tyefj50j0021|tyefj51j0015|tyefj51j0017|tyefj51j0019|tyefj51j0020|tyefj51j0021|tyefj52j0015|tyefj52j0017|tyefj52j0019|tyefj52j0020|tyefj52j0021|tyefj53j0015|tyefj53j0017|tyefj53j0019|tyefj53j0020|tyefj53j0021|tyefj54j0015|tyefj54j0017|tyefj54j0019|tyefj54j0020|tyefj54j0021|tyefj55j0015|tyefj55j0017|tyefj55j0019|tyefj55j0020|tyefj55j0021|tyefj56j0015|tyefj56j0017|tyefj56j0019|tyefj56j0020|tyefj56j0021|tyefj57j0015|tyefj57j0017|tyefj57j0019|tyefj57j0020|tyefj57j0021|tyefj58j0015|tyefj58j0017|tyefj58j0019|tyefj58j0020|tyefj58j0021|tyefj59j0015|tyefj59j0017|tyefj59j0019|tyefj59j0020|tyefj59j0021|tyefj60j0015|tyefj60j0017|tyefj60j0019|tyefj60j0020|tyefj60j0021|tyefj61j0015|tyefj61j0017|tyefj61j0019|tyefj61j0020|tyefj61j0021|tyefj62j0015|tyefj62j0017|tyefj62j0019|tyefj62j0020|tyefj62j0021|tyefj63j0015|tyefj63j0017|tyefj63j0019|tyefj63j0020|tyefj63j0021|tyefj64j0015|tyefj64j0017|tyefj64j0019|tyefj64j0020|tyefj64j0021|tyefj65j0015|tyefj65j0017|tyefj65j0019|tyefj65j0020|tyefj65j0021|tyefj66j0015|tyefj66j0017|tyefj66j0019|tyefj66j0020|tyefj66j0021|tyefj67j0015|tyefj67j0017|tyefj67j0019|tyefj67j0020|tyefj67j0021|tyefj68j0015|tyefj68j0017|tyefj68j0019|tyefj68j0020|tyefj68j0021|tyefj69j0015|tyefj69j0017|tyefj69j0019|tyefj69j0020|tyefj69j0021|tyefj70j0015|tyefj70j0017|tyefj70j0019|tyefj70j0020|tyefj70j0021|tyefj71j0015|tyefj71j0017|tyefj71j0019|tyefj71j0020|tyefj71j0021|tyefj72j0015|tyefj72j0017|tyefj72j0019|tyefj72j0020|tyefj72j0021|tyefj72j0022|tyefj73j0015|tyefj73j0017|tyefj73j0019|tyefj73j0020|tyefj73j0021|tyefj73j0022|tyefj74j0015|tyefj74j0017|tyefj74j0019|tyefj74j0020|tyefj74j0021|tyefj74j0022|tyefj75j0015|tyefj75j0017|tyefj75j0019|tyefj75j0020|tyefj75j0021|tyefj75j0022|tyefj76j0015|tyefj76j0017|tyefj76j0019|tyefj76j0020|tyefj76j0021|tyefj76j0022|tyefj76j0119|tyefj77j0015|tyefj77j0017|tyefj77j0019|tyefj77j0020|tyefj77j0021|tyefj77j0022|tyefj77j0119|tyefj78j0015|tyefj78j0017|tyefj78j0019|tyefj78j0020|tyefj78j0021|tyefj78j0022|tyefj78j0119|tyefj79j0015|tyefj79j0017|tyefj79j0019|tyefj79j0020|tyefj79j0021|tyefj79j0022|tyefj79j0119|tyefj80j0015|tyefj80j0017|tyefj80j0019|tyefj80j0020|tyefj80j0021|tyefj80j0022|tyefj80j0114|tyefj80j0119|tyefj81j0015|tyefj81j0017|tyefj81j0019|tyefj81j0020|tyefj81j0021|tyefj81j0022|tyefj81j0114|tyefj81j0119|tyefj82j0015|tyefj82j0017|tyefj82j0019|tyefj82j0020|tyefj82j0021|tyefj82j0022|tyefj82j0119|tyefj83j0015|tyefj83j0017|tyefj83j0019|tyefj83j0020|tyefj83j0021|tyefj83j0022|tyefj83j0119|tyefj84j0014|tyefj84j0015|tyefj84j0017|tyefj84j0019|tyefj84j0020|tyefj84j0021|tyefj84j0022|tyefj84j0119|tyefj85j0014|tyefj85j0015|tyefj85j0017|tyefj85j0019|tyefj85j0020|tyefj85j0021|tyefj85j0022|tyefj85j0119|tyefj86j0014|tyefj86j0015|tyefj86j0017|tyefj86j0019|tyefj86j0020|tyefj86j0021|tyefj86j0022|tyefj87j0014|tyefj87j0015|tyefj87j0017|tyefj87j0019|tyefj87j0020|tyefj87j0021|tyefj87j0022|tyefj88j0014|tyefj88j0015|tyefj88j0017|tyefj88j0019|tyefj88j0020|tyefj88j0021|tyefj88j0022|tyefj88j0100|tyefj88j0115|tyefj89j0003|tyefj89j0014|tyefj89j0015|tyefj89j0017|tyefj89j0019|tyefj89j0020|tyefj89j0021|tyefj89j0022|tyefj89j0100|tyefj89j0115|tyefj90j0014|tyefj90j0015|tyefj90j0016|tyefj90j0017|tyefj90j0018|tyefj90j0019|tyefj90j0020|tyefj90j0021|tyefj90j0022|tyefj90j0100|tyefj90j0111|tyefj90j0115|tyefj91j0014|tyefj91j0015|tyefj91j0016|tyefj91j0017|tyefj91j0018|tyefj91j0019|tyefj91j0020|tyefj91j0021|tyefj91j0022|tyefj91j0100|tyefj91j0111|tyefj91j0115|tyefj92j0014|tyefj92j0015|tyefj92j0016|tyefj92j0017|tyefj92j0018|tyefj92j0019|tyefj92j0020|tyefj92j0021|tyefj92j0022|tyefj92j0100|tyefj92j0105|tyefj92j0115|tyefj92j0121|tyefj93j0004|tyefj93j0014|tyefj93j0015|tyefj93j0017|tyefj93j0018|tyefj93j0019|tyefj93j0020|tyefj93j0021|tyefj93j0022|tyefj93j0100|tyefj93j0105|tyefj93j0115|tyefj93j0121|tyefj94j0002|tyefj94j0004|tyefj94j0008|tyefj94j0014|tyefj94j0015|tyefj94j0017|tyefj94j0019|tyefj94j0020|tyefj94j0021|tyefj94j0022|tyefj94j0084|tyefj94j0088|tyefj94j0100|tyefj94j0106|tyefj94j0116|tyefj94j0121|tyefj94j0123|tyefj95j0002|tyefj95j0004|tyefj95j0008|tyefj95j0014|tyefj95j0015|tyefj95j0017|tyefj95j0019|tyefj95j0020|tyefj95j0021|tyefj95j0022|tyefj95j0084|tyefj95j0088|tyefj95j0100|tyefj95j0101|tyefj95j0106|tyefj95j0112|tyefj95j0116|tyefj95j0121|tyefj95j0123|tyefj96j0014|tyefj96j0015|tyefj96j0017|tyefj96j0019|tyefj96j0020|tyefj96j0021|tyefj96j0022|tyefj96j0082|tyefj96j0084|tyefj96j0100|tyefj96j0101|tyefj96j0112|tyefj96j0117|tyefj96j0121|tyefj96j0124|tyefj97j0014|tyefj97j0015|tyefj97j0017|tyefj97j0019|tyefj97j0020|tyefj97j0021|tyefj97j0022|tyefj97j0081|tyefj97j0087|tyefj97j0098|tyefj97j0100|tyefj97j0107|tyefj97j0109|tyefj97j0113|tyefj97j0117|tyefj97j0118|tyefj97j0121|tyefj98j0003|tyefj98j0006|tyefj98j0014|tyefj98j0015|tyefj98j0017|tyefj98j0019|tyefj98j0020|tyefj98j0021|tyefj98j0022|tyefj98j0083|tyefj98j0085|tyefj98j0086|tyefj98j0100|tyefj98j0104|tyefj98j0118|tyefj98j0121|tyefj99j0003|tyefj99j0006|tyefj99j0007|tyefj99j0014|tyefj99j0015|tyefj99j0017|tyefj99j0019|tyefj99j0020|tyefj99j0021|tyefj99j0022|tyefj99j0023|tyefj99j0100|tyefj99j0108|tyefj99j0110|tyefj99j0121|tyefj99j0125|tyeff94j0002|tyeff94j0008|tyeff94j0010|tyeff94j0011|tyeff94j0035|tyeff95j0002|tyeff95j0006|tyeff95j0008|tyeff95j0010|tyeff95j0011|tyeff95j0035|tyeff96j0003|tyeff96j0006|tyeff96j0009|tyeff96j0010|tyeff97j0004|tyeff97j0009|tyeff97j0116|tyeff98j0007|tyeff99j0007|tyeff99j0125|uyyfj00j0484|uyyfj00j0485|uyyfj00j0486|uyyfj00j0487|uyyfj00j0488|uyyfj00j0489|uyyfj00j0490|uyyfj00j0491|uyyfj00j0492|uyyfj00j0493|uyyfj00j0494|uyyfj00j0495|uyyfj00j0496|uyyfj00j0497|uyyfj00j0498|uyyfj00j0499|uyyfj00j0500|uyyfj00j0501|uyyfj00j0502|uyyfj00j0503|uyyfj00j0504|uyyfj00j0505|uyyfj00j0506|uyyfj00j0507|uyyfj00j0508|uyyfj00j0509|uyyfj00j0510|uyyfj00j0511|uyyfj00j0512|uyyfj00j0513|uyyfj00j0514|uyyfj00j0515|uyyfj00j0516|uyyfj00j0517|uyyfj00j0518|uyyfj00j0519|uyyfj00j0520|uyyfj00j0521|uyyfj00j0522|uyyfj00j0523|uyyfj00j0524|uyyfj00j0525|uyyfj00j0526|uyyfj00j0527|uyyfj00j0528|uyyfj00j0529|uyyfj00j0530|uyyfj00j0531|uyyfj00j0532|uyyfj00j0533|uyyfj00j0534|uyyfj00j0535|uyyfj00j0536|uyyfj00j0537|uyyfj00j0538|uyyfj00j0539|uyyfj00j0540|uyyfj00j0541|uyyfj00j0542|uyyfj00j0543|uyyfj00j0544|uyyfj00j0545|uyyfj00j0546|uyyfj00j0547|uyyfj00j0548|uyyfj00j0549|uyyfj00j0550|uyyfj00j0551|uyyfj00j0553|uyyfj00j0554|uyyfj00j0555|uyyfj00j0556|uyyfj00j0557|uyyfj00j0558|uyyfj00j0559|uyyfj00j0560|uyyfj00j0561|uyyfj00j0562|uyyfj00j0563|uyyfj00j0564|uyyfj00j0565|uyyfj00j0566|uyyfj00j0614|uyyfj00j0615|uyyfj00j0616|uyyfj00j0617|uyyfj00j0618|uyyfj00j0619|uyyfj00j0620|uyyfj00j0621|uyyfj00j0622|uyyfj00j0623|uyyfj00j0624|uyyfj00j0625|uyyfj00j0626|uyyfj00j0627|uyyfj00j0628|uyyfj00j0629|uyyfj00j0630|uyyfj00j0631|uyyfj00j0632|uyyfj00j0633|uyyfj00j0634|uyyfj00j0635|uyyfj00j0636|uyyfj00j0637|uyyfj00j0638|uyyfj00j0639|uyyfj00j0640|uyyfj00j0641|uyyfj00j0642|uyyfj00j0643|uyyfj00j0644|uyyfj00j0645|uyyfj00j0646|uyyfj00j0647|uyyfj00j0648|uyyfj00j0649|uyyfj00j0650|uyyfj00j0651|uyyfj00j0652|uyyfj00j0653|uyyfj00j0654|uyyfj00j0655|uyyfj00j0656|uyyfj00j0657|uyyfj00j0658|uyyfj00j0659|uyyfj00j0660|uyyfj00j0661|uyyfj00j0662|uyyfj00j0663|uyyfj00j0664|uyyfj00j0665|uyyfj00j0666|uyyfj00j0667|uyyfj00j0668|uyyfj00j0669|uyyfj00j0670|uyyfj00j0671|uyyfj00j0672|uyyfj00j0673|uyyfj00j0674|uyyfj00j0675|uyyfj00j0676|uyyfj00j0677|uyyfj00j0678|uyyfj00j0679|uyyfj00j0680|uyyfj00j0681|uyyfj00j0682|uyyfj00j0683|uyyfj00j0684|uyyfj00j0685|uyyfj00j0686|uyyfj00j0687|uyyfj00j0688|uyyfj00j0689|uyyfj00j0690|uyyfj00j0691|uyyfj00j0692|uyyfj00j0693|uyyfj00j0694|uyyfj00j0695|uyyfj00j0696|uyyfj00j0697|uyyfj00j0698|uyyfj00j0699|uyyfj00j0700|uyyfj00j0701|uyyfj00j0702|uyyfj00j0703|uyyfj00j0704|uyyfj00j0705|uyyfj00j0706|uyyfj00j0707|uyyfj00j0708|uyyfj00j0709|uyyfj00j0710|uyyfj00j0711|uyyfj00j0712|uyyfj00j0713|uyyfj00j0714|uyyfj00j0715|uyyfj00j0716|uyyfj00j0717|uyyfj00j0718|uyyfj00j0719|uyyfj00j0720|uyyfj00j0721|uyyfj00j0722|uyyfj00j0723|uyyfj00j0724|uyyfj00j0725|uyyfj00j0726|uyyfj00j0727|uyyfj00j0728|uyyfj00j0729|uyyfj00j0730|uyyfj00j0731|uyyfj00j0732|uyyfj00j0733|uyyfj00j0734|uyyfj00j0735|uyyfj00j0736|uyyfj00j0737|uyyfj00j0738|uyyfj00j0739|uyyfj00j0740|uyyfj00j0741|uyyfj00j0742|uyyfj00j0743|uyyfj00j0744|uyyfj00j0745|uyyfj00j0746|uyyfj00j0747|uyyfj00j0748|uyyfj00j0749|uyyfj00j0750|uyyfj00j0751|uyyfj00j0752|uyyfj00j0753|uyyfj00j0754|uyyfj00j0755|uyyfj00j0756|uyyfj00j0757|uyyfj00j0758|uyyfj00j0759|uyyfj00j0760|uyyfj00j0761|uyyfj00j0762|uyyfj00j0763|uyyfj00j0764|uyyfj00j0765|uyyfj00j0766|uyyfj00j0767|uyyfj00j0768|uyyfj00j0769|uyyfj00j0770|uyyfj00j0771|uyyfj00j0772|uyyfj00j0773|uyyfj00j0774|uyyfj00j0775|uyyfj00j0776|uyyfj00j0777|uyyfj00j0778|uyyfj00j0779|uyyfj00j0780|uyyfj00j0781|uyyfj00j0782|uyyff00j0011|uyyff00j0031|uyyff00j0032|uyyff00j0033|uyyff00j0034|uyyff99j0012|uyefj00j0071|uyefj00j0455|uyefj00j0456|uyefj00j0582|uyefj00j0583|uyefj00j0584|uyefj00j0585|uyefj00j0586|uyefj00j0590|uyeff00j0188|xyrly-f-jyy-y01|xyrly-f-jyy-y02|xyrly-f-jyy-y03|xyrly-f-jyy-y04|xyrly-f-jyy-y05|xyrly-f-jyy-y06|xyrly-f-jyy-y07|xyrly-f-jyy-y08|xyrly-f-jyy-y09|xyrly-f-jyy-y10|xyrly-f-jyy-y11|xyrly-f-jyy-y12|xyrly-f-jyy-y13|xyrly-f-jyy-y14|xyrly-f-jyy-y15|xyrly-f-jyy-y16|xyrly-f-url-y01|xyrly-f-url-y02|yyefj97j0005|ybyfcy4000|ybyfcy4001|ayefj99j0035|by-b-y-bzu-l01|by-b-y-bzu-l02|by-b-e-079|by-b-e-080|by-b-e-082|by-b-e-083|byefj72j0002|byefj73j0002|byefj74j0002|byefj75j0002|byefj76j0002|byefj77j0002|byefj78j0002|byefj79j0002|byefj91j0007|byefj92j0007|byefj98j0003|byefj99j0003|byefj99j0005|byefj99j0006|byeff88j0002|byeff89j0002|byeff90j0002|byeff91j0002|byeff92j0002|byeff93j0002|byeff96j0003|byeff97j0003|byeff98j0003|byeff99j0003|fymfj98j0001|fymfj99j0001|fyyaj98k0297|fyyaj99k0297|fyyfj00j0109|fyyfj00j0110|fyyfj00j0122|fyyfj00j0123|fyyfj00j0201|fyyfj00j0202|fyyfj00j0207|fyyfj00j0208|fyyfj00j0227|fyyfj00j0228|fyyfj00j0229|fyyfj00j0230|fyyfj00j0231|fyyfj00j0232|fyyfj00j0233|fyyfj00j0234|fyyfj00j0235|fyyfj00j0236|fyyfj00j0237|fyyfj00j0238|fyyfj00j0239|fyyfj00j0240|fyyfj00j0241|fyyfj00j0242|fyyfj00j0243|fyyfj00j0244|fyyfj00j0245|fyyfj00j0246|fyyfj00j0247|fyyfj00j0248|fyyfj00j0249|fyyfj00j0250|fyyfj00j0251|fyyfj00j0252|fyyfj00j0253|fyyfj00j0254|fyyfj00j0255|fyyfj00j0256|fyyfj00j0257|fyyfj00j0258|fyyfj00j0259|fyyfj00j0260|fyyfj00j0261|fyyfj00j0262|fyyfj00j0263|fyyfj00j0264|fyyfj00j0265|fyyfj00j0266|fyyfj00j0267|fyyfj00j0268|fyyfj00j0290|fyyfj00j0291|fyyfj00j0292|fyyfj00j0293|fyyfj00j0294|fyyfj00j0295|fyyfj00j0296|fyyfj00j0297|fyyfj00j0298|fyyfj00j0299|fyyfj00j0300|fyyfj00j0301|fyyfj00j0302|fyyfj00j0303|fyyfj00j0304|fyyfj00j0305|fyyfj00j0306|fyyfj00j0307|fyyfj00j0308|fyyfj00j0309|fyyfj00j0310|fyyfj00j0311|fyyfj00j0312|fyyfj00j0313|fyyfj00j0314|fyyfj00j0315|fyyfj00j0316|fyyfj00j0317|fyyfj00j0318|fyyfj00j0319|fyyfj00j0320|fyyfj00j0321|fyyfj00j0322|fyyfj00j0323|fyyfj00j0324|fyyfj00j0325|fyyfj00j0326|fyyfj00j0327|fyyfj00j0328|fyyfj00j0329|fyyfj00j0330|fyyfj00j0331|fyyfj00j0332|fyyfj00j0333|fyyfj00j0334|fyyfj00j0335|fyyfj00j0340|fyyfj00j0341|fyyfj00j0342|fyyfj00j0343|fyyfj00j0344|fyyfj00j0345|fyyfj00j0346|fyyfj00j0347|fyyfj00j0348|fyyfj00j0349|fyyfj00j0367|fyyfj00j0368|fyyfj00j0369|fyyfj00j0370|fyyfj00j0371|fyyfj00j0372|fyyfj00j0373|fyyfj00j0374|fyyfj00j0375|fyyfj00j0376|fyyfj00j0377|fyyfj00j0378|fyyfj00j0379|fyyfj00j0380|fyyfj00j0381|fyyfj00j0382|fyyfj00j0383|fyyfj00j0384|fyyfj00j0385|fyyfj00j0386|fyyfj00j0387|fyyfj00j0388|fyyfj00j0415|fyyfj00j0416|fyyfj00j0417|fyyfj00j0418|fyyfj00j0419|fyyfj00j0420|fyyfj00j0421|fyyfj00j0422|fyyfj00j0423|fyyfj00j0424|fyyfj00j0425|fyyfj00j0426|fyyfj00j0427|fyyfj00j0428|fyyfj00j0429|fyyfj00j0430|fyyfj00j0431|fyyfj00j0432|fyyfj00j0433|fyyfj00j0434|fyyfj00j0435|fyyfj00j0436|fyyfj00j0437|fyyfj00j0438|fyyfj00j0439|fyyfj00j0440|fyyfj00j0441|fyyfj00j0446|fyyfj00j0447|fyyfj00j0448|fyyfj00j0449|fyyfj00j0451|fyyfj00j0452|fyyfj00j0453|fyyfj00j0454|fyyfj00j0455|fyyfj00j0456|fyyfj00j0457|fyyfj00j0459|fyyfj00j0460|fyyfj00j0461|fyyfj00j0462|fyyfj00j0463|fyyfj00j0464|fyyfj00j0465|fyyfj00j0466|fyyfj00j0467|fyyfj00j0468|fyyfj00j0469|fyyfj00j0470|fyyfj00j0471|fyyfj00j0474|fyyfj00j0475|fyyfj00j0476|fyyfj00j0477|fyyfj00j0478|fyyfj00j0479|fyyfj00j0480|fyyfj00j0481|fyyfj00j0482|fyyfj00j0483|fyyfj00j0484|fyyfj00j0485|fyyfj00j0486|fyyfj00j0487|fyyfj00j0488|fyyfj00j0489|fyyfj00j0490|fyyfj00j0491|fyyfj00j0492|fyyfj00j0493|fyyfj00j0494|fyyfj00j0495|fyyfj00j0496|fyyfj00j0497|fyyfj00j0498|fyyfj00j0499|fyyfj00j0500|fyyfj00j0501|fyyfj00j0502|fyyfj00j0503|fyyfj00j0504|fyyfj00j0505|fyyfj00j0506|fyyfj00j0507|fyyfj00j0508|fyyfj00j0509|fyyfj00j0510|fyyfj00j0511|fyyfj00j0512|fyyfj00j0513|fyyfj00j0514|fyyfj00j0515|fyyfj00j0516|fyyfj00j0517|fyyfj00j0518|fyyfj00j0521|fyyfj00j0522|fyyfj00j0523|fyyfj00j0524|fyyfj00j0526|fyyfj00j0527|fyyfj00j0528|fyyfj00j0529|fyyfj00j0530|fyyfj00j0531|fyyfj00j0532|fyyfj00j0533|fyyfj00j0534|fyyfj00j0535|fyyfj00j0536|fyyfj00j0537|fyyfj00j0538|fyyfj00j0539|fyyfj00j0540|fyyfj00j0541|fyyfj00j0542|fyyfj00j0543|fyyfj00j0544|fyyfj00j0545|fyyfj00j0546|fyyfj00j0564|fyyfj00j0565|fyyfj00j0566|fyyfj00j0567|fyyfj00j0568|fyyfj00j0569|fyyfj00j0570|fyyfj00j0571|fyyfj00j0572|fyyfj00j0574|fyyfj00j0575|fyyfj00j0576|fyyfj00j0577|fyyfj00j0578|fyyfj00j0579|fyyfj00j0580|fyyfj01j0473|fyyfj02j0473|fyyfj36j0289|fyyfj37j0209|fyyfj37j0289|fyyfj38j0209|fyyfj38j0289|fyyfj39j0209|fyyfj39j0289|fyyfj40j0209|fyyfj40j0289|fyyfj41j0209|fyyfj41j0289|fyyfj42j0209|fyyfj42j0289|fyyfj43j0209|fyyfj43j0289|fyyfj44j0209|fyyfj44j0289|fyyfj45j0104|fyyfj45j0209|fyyfj45j0289|fyyfj46j0104|fyyfj46j0209|fyyfj46j0289|fyyfj47j0104|fyyfj47j0209|fyyfj47j0289|fyyfj48j0104|fyyfj48j0209|fyyfj48j0289|fyyfj49j0104|fyyfj49j0209|fyyfj49j0289|fyyfj50j0104|fyyfj50j0209|fyyfj50j0289|fyyfj50j0500|fyyfj51j0104|fyyfj51j0209|fyyfj51j0289|fyyfj51j0500|fyyfj52j0104|fyyfj52j0209|fyyfj52j0289|fyyfj52j0500|fyyfj53j0104|fyyfj53j0209|fyyfj53j0289|fyyfj53j0500|fyyfj54j0104|fyyfj54j0209|fyyfj54j0289|fyyfj54j0500|fyyfj55j0104|fyyfj55j0209|fyyfj55j0289|fyyfj55j0500|fyyfj56j0104|fyyfj56j0209|fyyfj56j0289|fyyfj56j0500|fyyfj57j0104|fyyfj57j0209|fyyfj57j0289|fyyfj57j0500|fyyfj58j0104|fyyfj58j0209|fyyfj58j0289|fyyfj58j0500|fyyfj59j0104|fyyfj59j0209|fyyfj59j0289|fyyfj59j0500|fyyfj60j0104|fyyfj60j0209|fyyfj60j0289|fyyfj60j0500|fyyfj61j0104|fyyfj61j0209|fyyfj61j0289|fyyfj61j0500|fyyfj62j0104|fyyfj62j0209|fyyfj62j0289|fyyfj62j0500|fyyfj63j0104|fyyfj63j0209|fyyfj63j0289|fyyfj63j0500|fyyfj64j0104|fyyfj64j0107|fyyfj64j0209|fyyfj64j0289|fyyfj64j0500|fyyfj64j0573|fyyfj65j0104|fyyfj65j0107|fyyfj65j0209|fyyfj65j0289|fyyfj65j0500|fyyfj65j0573|fyyfj66j0104|fyyfj66j0107|fyyfj66j0209|fyyfj66j0289|fyyfj66j0500|fyyfj66j0573|fyyfj67j0104|fyyfj67j0107|fyyfj67j0209|fyyfj67j0289|fyyfj67j0500|fyyfj67j0573|fyyfj68j0104|fyyfj68j0107|fyyfj68j0209|fyyfj68j0289|fyyfj68j0500|fyyfj68j0573|fyyfj69j0104|fyyfj69j0107|fyyfj69j0209|fyyfj69j0289|fyyfj69j0500|fyyfj69j0573|fyyfj70j0104|fyyfj70j0107|fyyfj70j0209|fyyfj70j0289|fyyfj70j0472|fyyfj70j0500|fyyfj70j0573|fyyfj71j0104|fyyfj71j0107|fyyfj71j0209|fyyfj71j0289|fyyfj71j0472|fyyfj71j0500|fyyfj71j0573|fyyfj72j0104|fyyfj72j0107|fyyfj72j0209|fyyfj72j0289|fyyfj72j0472|fyyfj72j0500|fyyfj72j0573|fyyfj73j0104|fyyfj73j0107|fyyfj73j0209|fyyfj73j0289|fyyfj73j0472|fyyfj73j0500|fyyfj73j0573|fyyfj74j0104|fyyfj74j0107|fyyfj74j0209|fyyfj74j0289|fyyfj74j0472|fyyfj74j0500|fyyfj74j0573|fyyfj75j0104|fyyfj75j0107|fyyfj75j0108|fyyfj75j0209|fyyfj75j0289|fyyfj75j0472|fyyfj75j0500|fyyfj75j0573|fyyfj76j0104|fyyfj76j0107|fyyfj76j0108|fyyfj76j0209|fyyfj76j0289|fyyfj76j0472|fyyfj76j0500|fyyfj76j0573|fyyfj77j0104|fyyfj77j0107|fyyfj77j0108|fyyfj77j0209|fyyfj77j0289|fyyfj77j0472|fyyfj77j0500|fyyfj77j0573|fyyfj78j0104|fyyfj78j0107|fyyfj78j0108|fyyfj78j0209|fyyfj78j0289|fyyfj78j0472|fyyfj78j0500|fyyfj78j0573|fyyfj79j0104|fyyfj79j0107|fyyfj79j0108|fyyfj79j0209|fyyfj79j0289|fyyfj79j0339|fyyfj79j0472|fyyfj79j0500|fyyfj79j0573|fyyfj80j0104|fyyfj80j0107|fyyfj80j0108|fyyfj80j0209|fyyfj80j0289|fyyfj80j0339|fyyfj80j0352|fyyfj80j0472|fyyfj80j0500|fyyfj80j0573|fyyfj81j0104|fyyfj81j0107|fyyfj81j0108|fyyfj81j0209|fyyfj81j0289|fyyfj81j0339|fyyfj81j0352|fyyfj81j0472|fyyfj81j0500|fyyfj81j0573|fyyfj82j0104|fyyfj82j0107|fyyfj82j0108|fyyfj82j0209|fyyfj82j0289|fyyfj82j0339|fyyfj82j0352|fyyfj82j0472|fyyfj82j0500|fyyfj82j0573|fyyfj83j0104|fyyfj83j0107|fyyfj83j0108|fyyfj83j0209|fyyfj83j0289|fyyfj83j0339|fyyfj83j0352|fyyfj83j0472|fyyfj83j0500|fyyfj83j0573|fyyfj84j0104|fyyfj84j0107|fyyfj84j0108|fyyfj84j0209|fyyfj84j0289|fyyfj84j0339|fyyfj84j0352|fyyfj84j0472|fyyfj84j0500|fyyfj84j0573|fyyfj85j0104|fyyfj85j0107|fyyfj85j0108|fyyfj85j0209|fyyfj85j0289|fyyfj85j0301|fyyfj85j0339|fyyfj85j0352|fyyfj85j0472|fyyfj85j0500|fyyfj85j0573|fyyfj86j0104|fyyfj86j0107|fyyfj86j0108|fyyfj86j0209|fyyfj86j0289|fyyfj86j0301|fyyfj86j0339|fyyfj86j0352|fyyfj86j0472|fyyfj86j0500|fyyfj86j0573|fyyfj87j0067|fyyfj87j0104|fyyfj87j0107|fyyfj87j0108|fyyfj87j0209|fyyfj87j0289|fyyfj87j0301|fyyfj87j0339|fyyfj87j0352|fyyfj87j0472|fyyfj87j0500|fyyfj87j0573|fyyfj88j0067|fyyfj88j0104|fyyfj88j0107|fyyfj88j0108|fyyfj88j0209|fyyfj88j0289|fyyfj88j0301|fyyfj88j0339|fyyfj88j0352|fyyfj88j0472|fyyfj88j0500|fyyfj88j0573|fyyfj89j0067|fyyfj89j0104|fyyfj89j0107|fyyfj89j0108|fyyfj89j0209|fyyfj89j0289|fyyfj89j0301|fyyfj89j0339|fyyfj89j0352|fyyfj89j0358|fyyfj89j0472|fyyfj89j0500|fyyfj89j0573|fyyfj90j0067|fyyfj90j0104|fyyfj90j0107|fyyfj90j0108|fyyfj90j0209|fyyfj90j0289|fyyfj90j0301|fyyfj90j0321|fyyfj90j0339|fyyfj90j0352|fyyfj90j0358|fyyfj90j0452|fyyfj90j0472|fyyfj90j0500|fyyfj90j0573|fyyfj91j0067|fyyfj91j0104|fyyfj91j0107|fyyfj91j0108|fyyfj91j0209|fyyfj91j0289|fyyfj91j0301|fyyfj91j0321|fyyfj91j0339|fyyfj91j0352|fyyfj91j0358|fyyfj91j0452|fyyfj91j0472|fyyfj91j0500|fyyfj91j0573|fyyfj92j0067|fyyfj92j0104|fyyfj92j0107|fyyfj92j0108|fyyfj92j0209|fyyfj92j0289|fyyfj92j0301|fyyfj92j0321|fyyfj92j0339|fyyfj92j0352|fyyfj92j0358|fyyfj92j0452|fyyfj92j0472|fyyfj92j0500|fyyfj92j0573|fyyfj93j0067|fyyfj93j0099|fyyfj93j0104|fyyfj93j0107|fyyfj93j0108|fyyfj93j0209|fyyfj93j0289|fyyfj93j0301|fyyfj93j0321|fyyfj93j0352|fyyfj93j0358|fyyfj93j0452|fyyfj93j0472|fyyfj93j0500|fyyfj93j0573|fyyfj94j0067|fyyfj94j0099|fyyfj94j0104|fyyfj94j0107|fyyfj94j0108|fyyfj94j0209|fyyfj94j0211|fyyfj94j0289|fyyfj94j0301|fyyfj94j0321|fyyfj94j0352|fyyfj94j0358|fyyfj94j0359|fyyfj94j0452|fyyfj94j0472|fyyfj94j0500|fyyfj94j0573|fyyfj95j0067|fyyfj95j0099|fyyfj95j0104|fyyfj95j0107|fyyfj95j0108|fyyfj95j0209|fyyfj95j0211|fyyfj95j0289|fyyfj95j0298|fyyfj95j0301|fyyfj95j0321|fyyfj95j0339|fyyfj95j0352|fyyfj95j0358|fyyfj95j0359|fyyfj95j0414|fyyfj95j0452|fyyfj95j0472|fyyfj95j0500|fyyfj95j0573|fyyfj96j0067|fyyfj96j0099|fyyfj96j0104|fyyfj96j0107|fyyfj96j0108|fyyfj96j0209|fyyfj96j0211|fyyfj96j0289|fyyfj96j0298|fyyfj96j0301|fyyfj96j0321|fyyfj96j0339|fyyfj96j0352|fyyfj96j0358|fyyfj96j0359|fyyfj96j0414|fyyfj96j0452|fyyfj96j0472|fyyfj96j0500|fyyfj96j0573|fyyfj97j0067|fyyfj97j0099|fyyfj97j0100|fyyfj97j0104|fyyfj97j0107|fyyfj97j0108|fyyfj97j0209|fyyfj97j0211|fyyfj97j0289|fyyfj97j0298|fyyfj97j0301|fyyfj97j0321|fyyfj97j0339|fyyfj97j0352|fyyfj97j0358|fyyfj97j0359|fyyfj97j0414|fyyfj97j0445|fyyfj97j0452|fyyfj97j0472|fyyfj97j0500|fyyfj97j0573|fyyfj98j0067|fyyfj98j0099|fyyfj98j0100|fyyfj98j0104|fyyfj98j0107|fyyfj98j0108|fyyfj98j0178|fyyfj98j0209|fyyfj98j0211|fyyfj98j0289|fyyfj98j0298|fyyfj98j0301|fyyfj98j0303|fyyfj98j0321|fyyfj98j0339|fyyfj98j0352|fyyfj98j0358|fyyfj98j0359|fyyfj98j0413|fyyfj98j0414|fyyfj98j0445|fyyfj98j0452|fyyfj98j0472|fyyfj98j0500|fyyfj98j0573|fyyfj99j0067|fyyfj99j0099|fyyfj99j0100|fyyfj99j0104|fyyfj99j0107|fyyfj99j0108|fyyfj99j0131|fyyfj99j0209|fyyfj99j0211|fyyfj99j0285|fyyfj99j0289|fyyfj99j0298|fyyfj99j0301|fyyfj99j0303|fyyfj99j0321|fyyfj99j0339|fyyfj99j0352|fyyfj99j0358|fyyfj99j0359|fyyfj99j0413|fyyfj99j0414|fyyfj99j0445|fyyfj99j0452|fyyfj99j0472|fyyfj99j0500|fyyfj99j0573|fyyfm01j0064|fyyfm01j0070|fyyfm01j0071|fyyfm01j0088|fyyfm01j0091|fyyfm01j0108|fyyfm01j0111|fyyfm01j0112|fyyfm01j0114|fyyfm01j0115|fyyfm01j0133|fyyfm01j0140|fyyfm01j0141|fyyfm01j0142|fyyfm01j0143|fyyfm01j0148|fyyfm01j0149|fyyfm01j0152|fyyfm01j0153|fyyfm01j0155|fyyfm01j0159|fyyfm01j0160|fyyfm01j0163|fyyfm01j0165|fyyfm01j0168|fyyfm01j0169|fyyfm01j0221|fyyfm01j0223|fyyfm01j0268|fyyfm01j0271|fyyfm01j0285|fyyfm01j0299|fyyfm01j0320|fyyfm01j0321|fyyfm01j0360|fyyfm01j0369|fyyfm01j0400|fyyfm01j0401|fyyfm01j0411|fyyfm01j0572|fyyfm01j0765|fyyfm02j0064|fyyfm02j0069|fyyfm02j0070|fyyfm02j0071|fyyfm02j0088|fyyfm02j0091|fyyfm02j0108|fyyfm02j0111|fyyfm02j0112|fyyfm02j0114|fyyfm02j0115|fyyfm02j0133|fyyfm02j0140|fyyfm02j0141|fyyfm02j0142|fyyfm02j0143|fyyfm02j0148|fyyfm02j0149|fyyfm02j0152|fyyfm02j0153|fyyfm02j0155|fyyfm02j0159|fyyfm02j0160|fyyfm02j0163|fyyfm02j0165|fyyfm02j0168|fyyfm02j0169|fyyfm02j0221|fyyfm02j0223|fyyfm02j0268|fyyfm02j0271|fyyfm02j0285|fyyfm02j0299|fyyfm02j0320|fyyfm02j0321|fyyfm02j0360|fyyfm02j0369|fyyfm02j0400|fyyfm02j0572|fyyfm02j0765|fyyfm03j0064|fyyfm03j0070|fyyfm03j0091|fyyfm03j0108|fyyfm03j0111|fyyfm03j0115|fyyfm03j0160|fyyfm03j0165|fyyfm03j0299|fyyfm03j0400|fyyfm03j0572|fyyfm04j0111|fyyfm51j0064|fyyfm51j0369|fyyfm52j0064|fyyfm52j0369|fyyfr88j0003|fyyfr89j0003|fyyff98j0071|fyyff98j0303|fyyff99j0029|fyyff99j0303|fyefj00j0112|fyefj00j0545|fyefj00j0546|fyefj00j0633|fyefj00j0634|fyefj00j0635|fyefj00j0636|fyefj00j0637|fyefj00j0649|fyefj00j0651|fyefj00j0652|fyefj00j0656|fyefj00j0657|fyefj00j0658|fyefj00j0659|fyefj00j0660|fyefj00j0685|fyefj00j0686|fyefj00j0688|fyefj00j0701|fyefj00j0702|fyefj00j0703|fyefj00j0715|fyefj00j0720|fyefj00j0721|fyefj00j0722|fyefj00j0724|fyefj00j0725|fyefj00j0726|fyefj00j0731|fyefj00j0751|fyefj00j0752|fyefj00j0756|fyefj00j0757|fyefj00j0758|fyefj00j0759|fyefj00j0761|fyefj00j0762|fyefj00j0763|fyefj00j0764|fyefj00j0768|fyefj00j0769|fyefj00j0785|fyefj00j0786|fyefj00j0789|fyefj00j0790|fyefj00j0793|fyefj00j0794|fyefj00j0803|fyefj00j0811|fyefj00j0821|fyefj00j0822|fyefj00j0823|fyefj00j0824|fyefj00j0825|fyefj00j0826|fyefj00j0827|fyefj00j0828|fyefj00j0829|fyefj00j0831|fyefj00j0832|fyefj00j0833|fyefj00j0838|fyefj00j0839|fyefj00j0840|fyefj00j0854|fyefj00j0855|fyefj00j0856|fyefj00j0859|fyefj00j0860|fyefj00j0861|fyefj00j0869|fyefj00j0870|fyefj00j0879|fyefj00j0887|fyefj00j0888|fyefj00j0889|fyefj00j0900|fyefj00j0901|fyefj00j0903|fyefj00j0904|fyefj00j0905|fyefj00j0959|fyefj00j0960|fyefj00j0961|fyefj00j1004|fyefj00j1005|fyefj00j1012|fyefj00j1013|fyefj00j1014|fyefj00j1015|fyefj00j1016|fyefj00j1017|fyefj00j1018|fyefj00j1019|fyefj00j1020|fyefj00j1021|fyefj00j1218|fyefj00j1219|fyefj00j1220|fyefj00j1221|fyefj00j1222|fyefj00j1811|fyefj00j1854|fyefj00j1855|fyefj00j1856|fyefj01j0707|fyefj02j0707|fyefj03j0707|fyefj66j0001|fyefj67j0001|fyefj68j0001|fyefj68j1064|fyefj69j0001|fyefj69j1064|fyefj70j0001|fyefj70j0859|fyefj70j1064|fyefj71j0001|fyefj71j1064|fyefj72j0001|fyefj72j1064|fyefj73j0001|fyefj73j1064|fyefj74j0001|fyefj74j1064|fyefj75j0001|fyefj75j1064|fyefj75j1092|fyefj76j0001|fyefj76j1064|fyefj76j1092|fyefj77j0001|fyefj77j1064|fyefj77j1092|fyefj78j0001|fyefj78j1064|fyefj78j1092|fyefj79j0001|fyefj79j1064|fyefj79j1092|fyefj80j0001|fyefj80j0859|fyefj80j1064|fyefj80j1077|fyefj80j1092|fyefj81j0001|fyefj81j1064|fyefj81j1077|fyefj81j1092|fyefj82j0001|fyefj82j1064|fyefj82j1092|fyefj83j0001|fyefj83j1064|fyefj83j1092|fyefj84j0001|fyefj84j1064|fyefj84j1092|fyefj85j0001|fyefj85j0356|fyefj85j1064|fyefj85j1092|fyefj86j0001|fyefj86j0356|fyefj86j1064|fyefj87j0001|fyefj87j0356|fyefj87j1064|fyefj88j0001|fyefj88j0356|fyefj88j1064|fyefj89j0001|fyefj89j0356|fyefj89j1064|fyefj89j1067|fyefj90j0001|fyefj90j0758|fyefj90j1021|fyefj90j1064|fyefj90j1067|fyefj91j0001|fyefj91j0758|fyefj91j0791|fyefj91j1021|fyefj91j1064|fyefj91j1067|fyefj91j1077|fyefj92j0001|fyefj92j0359|fyefj92j0678|fyefj92j0758|fyefj92j0791|fyefj92j0867|fyefj92j1021|fyefj92j1064|fyefj92j1077|fyefj93j0001|fyefj93j0359|fyefj93j0678|fyefj93j0758|fyefj93j0791|fyefj93j0867|fyefj93j1010|fyefj93j1021|fyefj93j1049|fyefj93j1064|fyefj93j1077|fyefj94j0001|fyefj94j0678|fyefj94j0758|fyefj94j0791|fyefj94j0867|fyefj94j1010|fyefj94j1021|fyefj94j1049|fyefj94j1064|fyefj94j1070|fyefj94j1077|fyefj94j1085|fyefj95j0001|fyefj95j0678|fyefj95j0758|fyefj95j0791|fyefj95j0867|fyefj95j0965|fyefj95j0966|fyefj95j1010|fyefj95j1011|fyefj95j1021|fyefj95j1055|fyefj95j1064|fyefj95j1069|fyefj95j1077|fyefj95j1085|fyefj95j1089|fyefj96j0001|fyefj96j0106|fyefj96j0671|fyefj96j0678|fyefj96j0758|fyefj96j0791|fyefj96j0814|fyefj96j0836|fyefj96j0867|fyefj96j0931|fyefj96j0965|fyefj96j0966|fyefj96j0976|fyefj96j1010|fyefj96j1021|fyefj96j1051|fyefj96j1055|fyefj96j1064|fyefj96j1068|fyefj96j1070|fyefj96j1077|fyefj96j1079|fyefj96j1081|fyefj96j1086|fyefj96j1088|fyefj96j1091|fyefj96j1093|fyefj96j1094|fyefj97j0001|fyefj97j0106|fyefj97j0584|fyefj97j0586|fyefj97j0671|fyefj97j0678|fyefj97j0758|fyefj97j0791|fyefj97j0814|fyefj97j0825|fyefj97j0836|fyefj97j0863|fyefj97j0865|fyefj97j0867|fyefj97j0914|fyefj97j0931|fyefj97j0952|fyefj97j0965|fyefj97j0966|fyefj97j0969|fyefj97j0971|fyefj97j0972|fyefj97j0976|fyefj97j0985|fyefj97j1010|fyefj97j1021|fyefj97j1051|fyefj97j1052|fyefj97j1055|fyefj97j1058|fyefj97j1059|fyefj97j1064|fyefj97j1068|fyefj97j1077|fyefj97j1079|fyefj97j1081|fyefj97j1086|fyefj97j1088|fyefj97j1095|fyefj98j0001|fyefj98j0243|fyefj98j0326|fyefj98j0329|fyefj98j0343|fyefj98j0344|fyefj98j0380|fyefj98j0472|fyefj98j0584|fyefj98j0586|fyefj98j0604|fyefj98j0671|fyefj98j0673|fyefj98j0676|fyefj98j0677|fyefj98j0678|fyefj98j0694|fyefj98j0758|fyefj98j0814|fyefj98j0825|fyefj98j0836|fyefj98j0863|fyefj98j0865|fyefj98j0867|fyefj98j0896|fyefj98j0898|fyefj98j0901|fyefj98j0906|fyefj98j0910|fyefj98j0913|fyefj98j0914|fyefj98j0922|fyefj98j0931|fyefj98j0934|fyefj98j0936|fyefj98j0951|fyefj98j0952|fyefj98j0963|fyefj98j0965|fyefj98j0966|fyefj98j0969|fyefj98j0971|fyefj98j0972|fyefj98j0974|fyefj98j0975|fyefj98j0976|fyefj98j0977|fyefj98j0978|fyefj98j0985|fyefj98j0992|fyefj98j1008|fyefj98j1009|fyefj98j1010|fyefj98j1011|fyefj98j1012|fyefj98j1019|fyefj98j1021|fyefj98j1028|fyefj98j1034|fyefj98j1039|fyefj98j1046|fyefj98j1047|fyefj98j1048|fyefj98j1054|fyefj98j1055|fyefj98j1064|fyefj98j1068|fyefj98j1077|fyefj98j1079|fyefj98j1080|fyefj98j1081|fyefj98j1082|fyefj98j1084|fyefj98j1087|fyefj98j1088|fyefj98j1090|fyefj99j0010|fyefj99j0188|fyefj99j0243|fyefj99j0268|fyefj99j0280|fyefj99j0301|fyefj99j0329|fyefj99j0343|fyefj99j0344|fyefj99j0380|fyefj99j0552|fyefj99j0573|fyefj99j0584|fyefj99j0586|fyefj99j0604|fyefj99j0671|fyefj99j0673|fyefj99j0676|fyefj99j0677|fyefj99j0678|fyefj99j0694|fyefj99j0722|fyefj99j0757|fyefj99j0758|fyefj99j0771|fyefj99j0772|fyefj99j0804|fyefj99j0806|fyefj99j0809|fyefj99j0814|fyefj99j0825|fyefj99j0836|fyefj99j0862|fyefj99j0863|fyefj99j0865|fyefj99j0866|fyefj99j0867|fyefj99j0875|fyefj99j0896|fyefj99j0898|fyefj99j0901|fyefj99j0906|fyefj99j0907|fyefj99j0908|fyefj99j0910|fyefj99j0912|fyefj99j0913|fyefj99j0914|fyefj99j0921|fyefj99j0922|fyefj99j0923|fyefj99j0931|fyefj99j0934|fyefj99j0936|fyefj99j0937|fyefj99j0949|fyefj99j0951|fyefj99j0952|fyefj99j0962|fyefj99j0963|fyefj99j0965|fyefj99j0966|fyefj99j0969|fyefj99j0971|fyefj99j0972|fyefj99j0974|fyefj99j0975|fyefj99j0976|fyefj99j0977|fyefj99j0978|fyefj99j0982|fyefj99j0985|fyefj99j0986|fyefj99j0988|fyefj99j0991|fyefj99j0992|fyefj99j0995|fyefj99j0997|fyefj99j0999|fyefj99j1003|fyefj99j1006|fyefj99j1008|fyefj99j1009|fyefj99j1010|fyefj99j1011|fyefj99j1016|fyefj99j1019|fyefj99j1020|fyefj99j1021|fyefj99j1024|fyefj99j1026|fyefj99j1028|fyefj99j1031|fyefj99j1033|fyefj99j1034|fyefj99j1036|fyefj99j1039|fyefj99j1042|fyefj99j1045|fyefj99j1046|fyefj99j1048|fyefj99j1053|fyefj99j1054|fyefj99j1055|fyefj99j1061|fyefj99j1062|fyefj99j1063|fyefj99j1064|fyefj99j1068|fyefj99j1072|fyefj99j1076|fyefj99j1077|fyefj99j1079|fyefj99j1080|fyefj99j1081|fyefj99j1083|fyefj99j1084|fyefj99j1087|fyefj99j1088|fyefm00j0113|fyefm01j0057|fyefm01j0088|fyefm01j0091|fyefm01j0101|fyefm01j0104|fyefm01j0107|fyefm01j0112|fyefm01j0379|fyefm02j0057|fyefm02j0101|fyefm02j0104|fyefm02j0107|fyefm02j0112|fyefm02j0379|fyefm98j0066|fyefm99j0066|fyefm99j0090|fyefm99j0093|fyefm99j0110|fyefm99j0165|fyefm99j0208|fyefm99j0209|fyefm99j0295|fyefm99j0401|fyefm99j0402|fyefm99j0907|fyefm99j1054|fyefn98j0015|fyefn98j0024|fyefn98j0030|fyefn99j0015|fyefn99j0024|fyefn99j0030|fyefr94j0559|fyefr95j0559|fyefr96j0559|fyefr97j0559|fyefr98j0559|fyefr99j0012|fyefr99j0559|fyefb01305|fyeff00j0170|fyeff00j0224|fyeff00j0227|fyeff00j0228|fyeff00j0229|fyeff00j0280|fyeff00j0281|fyeff00j0282|fyeff00j0283|fyeff00j0288|fyeff00j0289|fyeff00j0331|fyeff00j0332|fyeff00j0333|fyeff00j0334|fyeff00j0335|fyeff00j0336|fyeff00j0337|fyeff00j0338|fyeff00j0346|fyeff00j0347|fyeff00j0348|fyeff00j0349|fyeff00j0350|fyeff00j0351|fyeff00j0357|fyeff00j0358|fyeff00j0371|fyeff00j0372|fyeff00j0396|fyeff00j0397|fyeff00j0424|fyeff00j0425|fyeff01j0416|fyeff02j0416|fyeff78j0418|fyeff79j0418|fyeff79j1051|fyeff80j1051|fyeff81j1051|fyeff82j1051|fyeff83j1051|fyeff84j1051|fyeff85j1051|fyeff86j1051|fyeff87j1051|fyeff88j0422|fyeff89j0422|fyeff90j0422|fyeff90j0434|fyeff90j0440|fyeff91j0422|fyeff91j0434|fyeff91j0440|fyeff92j0440|fyeff93j0440|fyeff93j1045|fyeff93j1067|fyeff94j0392|fyeff94j0440|fyeff94j0443|fyeff94j1045|fyeff94j1067|fyeff95j0219|fyeff95j0392|fyeff95j0439|fyeff95j0440|fyeff95j0443|fyeff96j0053|fyeff96j0219|fyeff96j0392|fyeff96j0429|fyeff96j0434|fyeff96j0950|fyeff96j1019|fyeff96j1028|fyeff97j0053|fyeff97j0178|fyeff97j0191|fyeff97j0219|fyeff97j0221|fyeff97j0258|fyeff97j0324|fyeff97j0355|fyeff97j0370|fyeff97j0377|fyeff97j0392|fyeff97j0429|fyeff97j0434|fyeff97j0950|fyeff97j1019|fyeff98j0053|fyeff98j0065|fyeff98j0101|fyeff98j0144|fyeff98j0156|fyeff98j0178|fyeff98j0191|fyeff98j0193|fyeff98j0196|fyeff98j0197|fyeff98j0209|fyeff98j0210|fyeff98j0211|fyeff98j0214|fyeff98j0215|fyeff98j0218|fyeff98j0219|fyeff98j0221|fyeff98j0258|fyeff98j0260|fyeff98j0279|fyeff98j0284|fyeff98j0295|fyeff98j0296|fyeff98j0298|fyeff98j0324|fyeff98j0355|fyeff98j0370|fyeff98j0376|fyeff98j0379|fyeff98j0381|fyeff98j0392|fyeff98j0401|fyeff98j0404|fyeff98j0405|fyeff98j0407|fyeff98j0411|fyeff98j0418|fyeff98j0421|fyeff98j0423|fyeff98j0433|fyeff98j0436|fyeff98j0673|fyeff98j0896|fyeff98j0950|fyeff98j0985|fyeff98j1012|fyeff99j0053|fyeff99j0065|fyeff99j0152|fyeff99j0156|fyeff99j0159|fyeff99j0178|fyeff99j0191|fyeff99j0193|fyeff99j0196|fyeff99j0197|fyeff99j0209|fyeff99j0210|fyeff99j0211|fyeff99j0214|fyeff99j0215|fyeff99j0218|fyeff99j0219|fyeff99j0220|fyeff99j0221|fyeff99j0260|fyeff99j0279|fyeff99j0284|fyeff99j0291|fyeff99j0295|fyeff99j0296|fyeff99j0297|fyeff99j0298|fyeff99j0324|fyeff99j0339|fyeff99j0355|fyeff99j0370|fyeff99j0376|fyeff99j0379|fyeff99j0381|fyeff99j0392|fyeff99j0401|fyeff99j0404|fyeff99j0405|fyeff99j0407|fyeff99j0410|fyeff99j0411|fyeff99j0413|fyeff99j0414|fyeff99j0415|fyeff99j0418|fyeff99j0421|fyeff99j0423|fyeff99j0436|fyeff99j0673|fyeff99j0896|fyeff99j0950|fyeff99j0962|fyeff99j0985|fyeff99j1010|fyeff99j1012|fyeff99j1028|fyeff99j1090|fyeff99j1370|fayfm01j0148|fayfm01j0149|fayfm01j0155|fayfm02j0148|fayfm02j0149|fayfm02j0155|faefj00j0594|faefj00j0595|faefj00j0596|faefj00j0597|faefj01j0707|faefj02j0707|faefj03j0707|faefj90j1023|faefj91j1023|faefj92j1023|faefj94j1056|faefj95j1023|faefj95j1056|faefj96j1056|faefj98j1038|faefj99j1078|fdeff99j9001|fdeff99j9002|gyefj99j0005", + // A long case insensitive alternation. + "(?i:(zQPbMkNO|NNSPdvMi|iWuuSoAl|qbvKMimS|IecrXtPa|seTckYqt|NxnyHkgB|fIDlOgKb|UhlWIygH|OtNoJxHG|cUTkFVIV|mTgFIHjr|jQkoIDtE|PPMKxRXl|AwMfwVkQ|CQyMrTQJ|BzrqxVSi|nTpcWuhF|PertdywG|ZZDgCtXN|WWdDPyyE|uVtNQsKk|BdeCHvPZ|wshRnFlH|aOUIitIp|RxZeCdXT|CFZMslCj|AVBZRDxl|IzIGCnhw|ythYuWiz|oztXVXhl|VbLkwqQx|qvaUgyVC|VawUjPWC|ecloYJuj|boCLTdSU|uPrKeAZx|hrMWLWBq|JOnUNHRM|rYnujkPq|dDEdZhIj|DRrfvugG|yEGfDxVV|YMYdJWuP|PHUQZNWM|AmKNrLis|zTxndVfn|FPsHoJnc|EIulZTua|KlAPhdzg|ScHJJCLt|NtTfMzME|eMCwuFdo|SEpJVJbR|cdhXZeCx|sAVtBwRh|kVFEVcMI|jzJrxraA|tGLHTell|NNWoeSaw|DcOKSetX|UXZAJyka|THpMphDP|rizheevl|kDCBRidd|pCZZRqyu|pSygkitl|SwZGkAaW|wILOrfNX|QkwVOerj|kHOMxPDr|EwOVycJv|AJvtzQFS|yEOjKYYB|LizIINLL|JBRSsfcG|YPiUqqNl|IsdEbvee|MjEpGcBm|OxXZVgEQ|xClXGuxa|UzRCGFEb|buJbvfvA|IPZQxRet|oFYShsMc|oBHffuHO|bzzKrcBR|KAjzrGCl|IPUsAVls|OGMUMbIU|gyDccHuR|bjlalnDd|ZLWjeMna|fdsuIlxQ|dVXtiomV|XxedTjNg|XWMHlNoA|nnyqArQX|opfkWGhb|wYtnhdYb))", + // A long case insensitive alternation where each entry ends with ".*". + "(?i:(zQPbMkNO.*|NNSPdvMi.*|iWuuSoAl.*|qbvKMimS.*|IecrXtPa.*|seTckYqt.*|NxnyHkgB.*|fIDlOgKb.*|UhlWIygH.*|OtNoJxHG.*|cUTkFVIV.*|mTgFIHjr.*|jQkoIDtE.*|PPMKxRXl.*|AwMfwVkQ.*|CQyMrTQJ.*|BzrqxVSi.*|nTpcWuhF.*|PertdywG.*|ZZDgCtXN.*|WWdDPyyE.*|uVtNQsKk.*|BdeCHvPZ.*|wshRnFlH.*|aOUIitIp.*|RxZeCdXT.*|CFZMslCj.*|AVBZRDxl.*|IzIGCnhw.*|ythYuWiz.*|oztXVXhl.*|VbLkwqQx.*|qvaUgyVC.*|VawUjPWC.*|ecloYJuj.*|boCLTdSU.*|uPrKeAZx.*|hrMWLWBq.*|JOnUNHRM.*|rYnujkPq.*|dDEdZhIj.*|DRrfvugG.*|yEGfDxVV.*|YMYdJWuP.*|PHUQZNWM.*|AmKNrLis.*|zTxndVfn.*|FPsHoJnc.*|EIulZTua.*|KlAPhdzg.*|ScHJJCLt.*|NtTfMzME.*|eMCwuFdo.*|SEpJVJbR.*|cdhXZeCx.*|sAVtBwRh.*|kVFEVcMI.*|jzJrxraA.*|tGLHTell.*|NNWoeSaw.*|DcOKSetX.*|UXZAJyka.*|THpMphDP.*|rizheevl.*|kDCBRidd.*|pCZZRqyu.*|pSygkitl.*|SwZGkAaW.*|wILOrfNX.*|QkwVOerj.*|kHOMxPDr.*|EwOVycJv.*|AJvtzQFS.*|yEOjKYYB.*|LizIINLL.*|JBRSsfcG.*|YPiUqqNl.*|IsdEbvee.*|MjEpGcBm.*|OxXZVgEQ.*|xClXGuxa.*|UzRCGFEb.*|buJbvfvA.*|IPZQxRet.*|oFYShsMc.*|oBHffuHO.*|bzzKrcBR.*|KAjzrGCl.*|IPUsAVls.*|OGMUMbIU.*|gyDccHuR.*|bjlalnDd.*|ZLWjeMna.*|fdsuIlxQ.*|dVXtiomV.*|XxedTjNg.*|XWMHlNoA.*|nnyqArQX.*|opfkWGhb.*|wYtnhdYb.*))", + // A long case insensitive alternation where each entry starts with ".*". + "(?i:(.*zQPbMkNO|.*NNSPdvMi|.*iWuuSoAl|.*qbvKMimS|.*IecrXtPa|.*seTckYqt|.*NxnyHkgB|.*fIDlOgKb|.*UhlWIygH|.*OtNoJxHG|.*cUTkFVIV|.*mTgFIHjr|.*jQkoIDtE|.*PPMKxRXl|.*AwMfwVkQ|.*CQyMrTQJ|.*BzrqxVSi|.*nTpcWuhF|.*PertdywG|.*ZZDgCtXN|.*WWdDPyyE|.*uVtNQsKk|.*BdeCHvPZ|.*wshRnFlH|.*aOUIitIp|.*RxZeCdXT|.*CFZMslCj|.*AVBZRDxl|.*IzIGCnhw|.*ythYuWiz|.*oztXVXhl|.*VbLkwqQx|.*qvaUgyVC|.*VawUjPWC|.*ecloYJuj|.*boCLTdSU|.*uPrKeAZx|.*hrMWLWBq|.*JOnUNHRM|.*rYnujkPq|.*dDEdZhIj|.*DRrfvugG|.*yEGfDxVV|.*YMYdJWuP|.*PHUQZNWM|.*AmKNrLis|.*zTxndVfn|.*FPsHoJnc|.*EIulZTua|.*KlAPhdzg|.*ScHJJCLt|.*NtTfMzME|.*eMCwuFdo|.*SEpJVJbR|.*cdhXZeCx|.*sAVtBwRh|.*kVFEVcMI|.*jzJrxraA|.*tGLHTell|.*NNWoeSaw|.*DcOKSetX|.*UXZAJyka|.*THpMphDP|.*rizheevl|.*kDCBRidd|.*pCZZRqyu|.*pSygkitl|.*SwZGkAaW|.*wILOrfNX|.*QkwVOerj|.*kHOMxPDr|.*EwOVycJv|.*AJvtzQFS|.*yEOjKYYB|.*LizIINLL|.*JBRSsfcG|.*YPiUqqNl|.*IsdEbvee|.*MjEpGcBm|.*OxXZVgEQ|.*xClXGuxa|.*UzRCGFEb|.*buJbvfvA|.*IPZQxRet|.*oFYShsMc|.*oBHffuHO|.*bzzKrcBR|.*KAjzrGCl|.*IPUsAVls|.*OGMUMbIU|.*gyDccHuR|.*bjlalnDd|.*ZLWjeMna|.*fdsuIlxQ|.*dVXtiomV|.*XxedTjNg|.*XWMHlNoA|.*nnyqArQX|.*opfkWGhb|.*wYtnhdYb))", + // Quest ".?". + "fo.?", + "foo.?", + "f.?o", + ".*foo.?", + ".?foo.+", + "foo.?|bar", } + values = []string{ + "foo", " foo bar", "bar", "buzz\nbar", "bar foo", "bfoo", "\n", "\nfoo", "foo\n", "hello foo world", "hello foo\n world", "", + "FOO", "Foo", "OO", "Oo", "\nfoo\n", strings.Repeat("f", 20), "prometheus", "prometheus_api_v1", "prometheus_api_v1_foo", + "10.0.1.20", "10.0.2.10", "10.0.3.30", "10.0.4.40", + "foofoo0", "foofoo", - for _, c := range cases { - m, err := NewFastRegexMatcher(c.regex) - require.NoError(t, err) - require.Equal(t, c.expected, m.MatchString(c.value)) + // Values matching / not matching the test regexps on long alternations. + "zQPbMkNO", "zQPbMkNo", "jyyfj00j0061", "jyyfj00j006", "jyyfj00j00612", "NNSPdvMi", "NNSPdvMiXXX", "NNSPdvMixxx", "nnSPdvMi", "nnSPdvMiXXX", + } +) + +func TestFastRegexMatcher_MatchString(t *testing.T) { + // Run the test both against a set of predefined values and a set of random ones. + testValues := append([]string{}, values...) + testValues = append(testValues, generateRandomValues()...) + + for _, r := range regexes { + r := r + for _, v := range testValues { + v := v + t.Run(r+` on "`+v+`"`, func(t *testing.T) { + t.Parallel() + m, err := NewFastRegexMatcher(r) + require.NoError(t, err) + re := regexp.MustCompile("^(?:" + r + ")$") + require.Equal(t, re.MatchString(v), m.MatchString(v)) + }) + } } } @@ -85,6 +136,9 @@ func TestOptimizeConcatRegex(t *testing.T) { {regex: "(?i).*(?-i:abc)def", prefix: "", suffix: "", contains: "abc"}, {regex: ".*(?msU:abc).*", prefix: "", suffix: "", contains: "abc"}, {regex: "[aA]bc.*", prefix: "", suffix: "", contains: "bc"}, + {regex: "^5..$", prefix: "5", suffix: "", contains: ""}, + {regex: "^release.*", prefix: "release", suffix: "", contains: ""}, + {regex: "^env-[0-9]+laio[1]?[^0-9].*", prefix: "env-", suffix: "", contains: "laio"}, } for _, c := range cases { @@ -98,41 +152,906 @@ func TestOptimizeConcatRegex(t *testing.T) { } } -func BenchmarkFastRegexMatcher(b *testing.B) { - var ( - x = strings.Repeat("x", 50) - y = "foo" + x - z = x + "foo" - ) - regexes := []string{ - "foo", - "^foo", - "(foo|bar)", - "foo.*", - ".*foo", - "^.*foo$", - "^.+foo$", - ".*", - ".+", - "foo.+", - ".+foo", - ".*foo.*", - "(?i:foo)", - "(prometheus|api_prom)_api_v1_.+", - "((fo(bar))|.+foo)", - } - for _, r := range regexes { - r := r - b.Run(r, func(b *testing.B) { - m, err := NewFastRegexMatcher(r) - require.NoError(b, err) - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = m.MatchString(x) - _ = m.MatchString(y) - _ = m.MatchString(z) +// Refer to https://github.com/prometheus/prometheus/issues/2651. +func TestFindSetMatches(t *testing.T) { + for _, c := range []struct { + pattern string + expMatches []string + expCaseSensitive bool + }{ + // Single value, coming from a `bar=~"foo"` selector. + {"foo", []string{"foo"}, true}, + {"^foo", []string{"foo"}, true}, + {"^foo$", []string{"foo"}, true}, + // Simple sets alternates. + {"foo|bar|zz", []string{"foo", "bar", "zz"}, true}, + // Simple sets alternate and concat (bar|baz is parsed as "ba[rz]"). + {"foo|bar|baz", []string{"foo", "bar", "baz"}, true}, + // Simple sets alternate and concat and capture + {"foo|bar|baz|(zz)", []string{"foo", "bar", "baz", "zz"}, true}, + // Simple sets alternate and concat and alternates with empty matches + // parsed as b(ar|(?:)|uzz) where b(?:) means literal b. + {"bar|b|buzz", []string{"bar", "b", "buzz"}, true}, + // Skip nested capture groups. + {"^((bar|b|buzz))$", []string{"bar", "b", "buzz"}, true}, + // Skip outer anchors (it's enforced anyway at the root). + {"^(bar|b|buzz)$", []string{"bar", "b", "buzz"}, true}, + {"^(?:prod|production)$", []string{"prod", "production"}, true}, + // Do not optimize regexp with inner anchors. + {"(bar|b|b^uz$z)", nil, false}, + // Do not optimize regexp with empty string matcher. + {"^$|Running", nil, false}, + // Simple sets containing escaped characters. + {"fo\\.o|bar\\?|\\^baz", []string{"fo.o", "bar?", "^baz"}, true}, + // using charclass + {"[abc]d", []string{"ad", "bd", "cd"}, true}, + // high low charset different => A(B[CD]|EF)|BC[XY] + {"ABC|ABD|AEF|BCX|BCY", []string{"ABC", "ABD", "AEF", "BCX", "BCY"}, true}, + // triple concat + {"api_(v1|prom)_push", []string{"api_v1_push", "api_prom_push"}, true}, + // triple concat with multiple alternates + {"(api|rpc)_(v1|prom)_push", []string{"api_v1_push", "api_prom_push", "rpc_v1_push", "rpc_prom_push"}, true}, + {"(api|rpc)_(v1|prom)_(push|query)", []string{"api_v1_push", "api_v1_query", "api_prom_push", "api_prom_query", "rpc_v1_push", "rpc_v1_query", "rpc_prom_push", "rpc_prom_query"}, true}, + // class starting with "-" + {"[-1-2][a-c]", []string{"-a", "-b", "-c", "1a", "1b", "1c", "2a", "2b", "2c"}, true}, + {"[1^3]", []string{"1", "3", "^"}, true}, + // OpPlus with concat + {"(.+)/(foo|bar)", nil, false}, + // Simple sets containing special characters without escaping. + {"fo.o|bar?|^baz", nil, false}, + // case sensitive wrapper. + {"(?i)foo", []string{"FOO"}, false}, + // case sensitive wrapper on alternate. + {"(?i)foo|bar|baz", []string{"FOO", "BAR", "BAZ", "BAr", "BAz"}, false}, + // mixed case sensitivity. + {"(api|rpc)_(v1|prom)_((?i)push|query)", nil, false}, + // mixed case sensitivity concatenation only without capture group. + {"api_v1_(?i)push", nil, false}, + // mixed case sensitivity alternation only without capture group. + {"api|(?i)rpc", nil, false}, + // case sensitive after unsetting insensitivity. + {"rpc|(?i)(?-i)api", []string{"rpc", "api"}, true}, + // case sensitive after unsetting insensitivity in all alternation options. + {"(?i)((?-i)api|(?-i)rpc)", []string{"api", "rpc"}, true}, + // mixed case sensitivity after unsetting insensitivity. + {"(?i)rpc|(?-i)api", nil, false}, + // too high charset combination + {"(api|rpc)_[^0-9]", nil, false}, + // too many combinations + {"[a-z][a-z]", nil, false}, + } { + c := c + t.Run(c.pattern, func(t *testing.T) { + t.Parallel() + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + matches, actualCaseSensitive := findSetMatches(parsed) + require.Equal(t, c.expMatches, matches) + require.Equal(t, c.expCaseSensitive, actualCaseSensitive) + + if c.expCaseSensitive { + // When the regexp is case sensitive, we want to ensure that the + // set matches are maintained in the final matcher. + r, err := NewFastRegexMatcher(c.pattern) + require.NoError(t, err) + require.Equal(t, c.expMatches, r.SetMatches()) } }) - + } +} + +func TestFastRegexMatcher_SetMatches_ShouldReturnACopy(t *testing.T) { + m, err := NewFastRegexMatcher("a|b") + require.NoError(t, err) + require.Equal(t, []string{"a", "b"}, m.SetMatches()) + + // Manipulate the returned slice. + matches := m.SetMatches() + matches[0] = "xxx" + matches[1] = "yyy" + + // Ensure that if we call SetMatches() again we get the original one. + require.Equal(t, []string{"a", "b"}, m.SetMatches()) +} + +func BenchmarkFastRegexMatcher(b *testing.B) { + texts := generateRandomValues() + + for _, r := range regexes { + b.Run(getTestNameFromRegexp(r), func(b *testing.B) { + m, err := NewFastRegexMatcher(r) + require.NoError(b, err) + + b.ResetTimer() + for i := 0; i < b.N; i++ { + for _, text := range texts { + _ = m.MatchString(text) + } + } + }) + } +} + +func TestStringMatcherFromRegexp(t *testing.T) { + for _, c := range []struct { + pattern string + exp StringMatcher + }{ + {".*", anyStringWithoutNewlineMatcher{}}, + {".*?", anyStringWithoutNewlineMatcher{}}, + {"(?s:.*)", trueMatcher{}}, + {"(.*)", anyStringWithoutNewlineMatcher{}}, + {"^.*$", anyStringWithoutNewlineMatcher{}}, + {".+", &anyNonEmptyStringMatcher{matchNL: false}}, + {"(?s:.+)", &anyNonEmptyStringMatcher{matchNL: true}}, + {"^.+$", &anyNonEmptyStringMatcher{matchNL: false}}, + {"(.+)", &anyNonEmptyStringMatcher{matchNL: false}}, + {"", emptyStringMatcher{}}, + {"^$", emptyStringMatcher{}}, + {"^foo$", &equalStringMatcher{s: "foo", caseSensitive: true}}, + {"^(?i:foo)$", &equalStringMatcher{s: "FOO", caseSensitive: false}}, + {"^((?i:foo)|(bar))$", orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "bar", caseSensitive: true}})}, + {`(?i:((foo|bar)))`, orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {`(?i:((foo1|foo2|bar)))`, orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {"^((?i:foo|oo)|(bar))$", orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO", caseSensitive: false}, &equalStringMatcher{s: "OO", caseSensitive: false}, &equalStringMatcher{s: "bar", caseSensitive: true}})}, + {"(?i:(foo1|foo2|bar))", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "FOO1", caseSensitive: false}, &equalStringMatcher{s: "FOO2", caseSensitive: false}}), &equalStringMatcher{s: "BAR", caseSensitive: false}})}, + {".*foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.*)foo.*", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.*)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"(.+)foo(.*)", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: anyStringWithoutNewlineMatcher{}}}, + {"^.+foo.+", &containsStringMatcher{substrings: []string{"foo"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^(.*)(foo)(.*)$", &containsStringMatcher{substrings: []string{"foo"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"^(.*)(foo|foobar)(.*)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringWithoutNewlineMatcher{}, right: anyStringWithoutNewlineMatcher{}}}, + {"^(.*)(foo|foobar)(.+)$", &containsStringMatcher{substrings: []string{"foo", "foobar"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^(.*)(bar|b|buzz)(.+)$", &containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"10\\.0\\.(1|2)\\.+", nil}, + {"10\\.0\\.(1|2).+", &containsStringMatcher{substrings: []string{"10.0.1", "10.0.2"}, left: nil, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^.+foo", &literalSuffixStringMatcher{left: &anyNonEmptyStringMatcher{}, suffix: "foo", suffixCaseSensitive: true}}, + {"foo-.*$", &literalPrefixStringMatcher{prefix: "foo-", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}, + {"(prometheus|api_prom)_api_v1_.+", &containsStringMatcher{substrings: []string{"prometheus_api_v1_", "api_prom_api_v1_"}, left: nil, right: &anyNonEmptyStringMatcher{matchNL: false}}}, + {"^((.*)(bar|b|buzz)(.+)|foo)$", orStringMatcher([]StringMatcher{&containsStringMatcher{substrings: []string{"bar", "b", "buzz"}, left: anyStringWithoutNewlineMatcher{}, right: &anyNonEmptyStringMatcher{matchNL: false}}, &equalStringMatcher{s: "foo", caseSensitive: true}})}, + {"((fo(bar))|.+foo)", orStringMatcher([]StringMatcher{orStringMatcher([]StringMatcher{&equalStringMatcher{s: "fobar", caseSensitive: true}}), &literalSuffixStringMatcher{suffix: "foo", suffixCaseSensitive: true, left: &anyNonEmptyStringMatcher{matchNL: false}}})}, + {"(.+)/(gateway|cortex-gw|cortex-gw-internal)", &containsStringMatcher{substrings: []string{"/gateway", "/cortex-gw", "/cortex-gw-internal"}, left: &anyNonEmptyStringMatcher{matchNL: false}, right: nil}}, + // we don't support case insensitive matching for contains. + // This is because there's no strings.IndexOfFold function. + // We can revisit later if this is really popular by using strings.ToUpper. + {"^(.*)((?i)foo|foobar)(.*)$", nil}, + {"(api|rpc)_(v1|prom)_((?i)push|query)", nil}, + {"[a-z][a-z]", nil}, + {"[1^3]", nil}, + {".*foo.*bar.*", nil}, + {`\d*`, nil}, + {".", nil}, + {"/|/bar.*", &literalPrefixStringMatcher{prefix: "/", prefixCaseSensitive: true, right: orStringMatcher{emptyStringMatcher{}, &literalPrefixStringMatcher{prefix: "bar", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}}}, + // This one is not supported because `stringMatcherFromRegexp` is not reentrant for syntax.OpConcat. + // It would make the code too complex to handle it. + {"(.+)/(foo.*|bar$)", nil}, + // Case sensitive alternate with same literal prefix and .* suffix. + {"(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixStringMatcher{prefix: "xyz-016a-ixb-", prefixCaseSensitive: true, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "dp", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "op", prefixCaseSensitive: true, right: anyStringWithoutNewlineMatcher{}}}}}, + // Case insensitive alternate with same literal prefix and .* suffix. + {"(?i:(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*))", &literalPrefixStringMatcher{prefix: "XYZ-016A-IXB-", prefixCaseSensitive: false, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "DP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "OP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}}}}, + {"(?i)(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", &literalPrefixStringMatcher{prefix: "XYZ-016A-IXB-", prefixCaseSensitive: false, right: orStringMatcher{&literalPrefixStringMatcher{prefix: "DP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}, &literalPrefixStringMatcher{prefix: "OP", prefixCaseSensitive: false, right: anyStringWithoutNewlineMatcher{}}}}}, + // Concatenated variable length selectors are not supported. + {"foo.*.*", nil}, + {"foo.+.+", nil}, + {".*.*foo", nil}, + {".+.+foo", nil}, + {"aaa.?.?", nil}, + {"aaa.?.*", nil}, + // Regexps with ".?". + {"ext.?|xfs", orStringMatcher{&literalPrefixStringMatcher{prefix: "ext", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: false}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, + {"(?s)(ext.?|xfs)", orStringMatcher{&literalPrefixStringMatcher{prefix: "ext", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: true}}, &equalStringMatcher{s: "xfs", caseSensitive: true}}}, + {"foo.?", &literalPrefixStringMatcher{prefix: "foo", prefixCaseSensitive: true, right: &zeroOrOneCharacterStringMatcher{matchNL: false}}}, + {"f.?o", nil}, + } { + c := c + t.Run(c.pattern, func(t *testing.T) { + t.Parallel() + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + matches := stringMatcherFromRegexp(parsed) + require.Equal(t, c.exp, matches) + }) + } +} + +func TestStringMatcherFromRegexp_LiteralPrefix(t *testing.T) { + for _, c := range []struct { + pattern string + expectedLiteralPrefixMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Case sensitive. + { + pattern: "(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-016a-ixb-dp", "xyz-016a-ixb-dpXXX", "xyz-016a-ixb-op", "xyz-016a-ixb-opXXX"}, + expectedNotMatches: []string{"XYZ-016a-ixb-dp", "xyz-016a-ixb-d", "XYZ-016a-ixb-op", "xyz-016a-ixb-o", "xyz", "dp", "xyz-016a-ixb-dp\n"}, + }, + + // Case insensitive. + { + pattern: "(?i)(xyz-016a-ixb-dp.*|xyz-016a-ixb-op.*)", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-016a-ixb-dp", "XYZ-016a-ixb-dpXXX", "xyz-016a-ixb-op", "XYZ-016a-ixb-opXXX"}, + expectedNotMatches: []string{"xyz-016a-ixb-d", "xyz", "dp", "xyz-016a-ixb-dp\n"}, + }, + + // Nested literal prefixes, case sensitive. + { + pattern: "(xyz-(aaa-(111.*)|bbb-(222.*)))|(xyz-(aaa-(333.*)|bbb-(444.*)))", + expectedLiteralPrefixMatchers: 10, + expectedMatches: []string{"xyz-aaa-111", "xyz-aaa-111XXX", "xyz-aaa-333", "xyz-aaa-333XXX", "xyz-bbb-222", "xyz-bbb-222XXX", "xyz-bbb-444", "xyz-bbb-444XXX"}, + expectedNotMatches: []string{"XYZ-aaa-111", "xyz-aaa-11", "xyz-aaa-222", "xyz-bbb-111"}, + }, + + // Nested literal prefixes, case insensitive. + { + pattern: "(?i)(xyz-(aaa-(111.*)|bbb-(222.*)))|(xyz-(aaa-(333.*)|bbb-(444.*)))", + expectedLiteralPrefixMatchers: 10, + expectedMatches: []string{"xyz-aaa-111", "XYZ-aaa-111XXX", "xyz-aaa-333", "xyz-AAA-333XXX", "xyz-bbb-222", "xyz-BBB-222XXX", "XYZ-bbb-444", "xyz-bbb-444XXX"}, + expectedNotMatches: []string{"xyz-aaa-11", "xyz-aaa-222", "xyz-bbb-111"}, + }, + + // Mixed case sensitivity. + { + pattern: "(xyz-((?i)(aaa.*|bbb.*)))", + expectedLiteralPrefixMatchers: 3, + expectedMatches: []string{"xyz-aaa", "xyz-AAA", "xyz-aaaXXX", "xyz-AAAXXX", "xyz-bbb", "xyz-BBBXXX"}, + expectedNotMatches: []string{"XYZ-aaa", "xyz-aa", "yz-aaa", "aaa"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains literalPrefixStringMatcher. + numPrefixMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*literalPrefixStringMatcher); ok { + numPrefixMatchers++ + } + }) + + require.Equal(t, c.expectedLiteralPrefixMatchers, numPrefixMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func TestStringMatcherFromRegexp_LiteralSuffix(t *testing.T) { + for _, c := range []struct { + pattern string + expectedLiteralSuffixMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Case sensitive. + { + pattern: "(.*xyz-016a-ixb-dp|.*xyz-016a-ixb-op)", + expectedLiteralSuffixMatchers: 2, + expectedMatches: []string{"xyz-016a-ixb-dp", "XXXxyz-016a-ixb-dp", "xyz-016a-ixb-op", "XXXxyz-016a-ixb-op"}, + expectedNotMatches: []string{"XYZ-016a-ixb-dp", "yz-016a-ixb-dp", "XYZ-016a-ixb-op", "xyz-016a-ixb-o", "xyz", "dp", "\nxyz-016a-ixb-dp"}, + }, + + // Case insensitive. + { + pattern: "(?i)(.*xyz-016a-ixb-dp|.*xyz-016a-ixb-op)", + expectedLiteralSuffixMatchers: 2, + expectedMatches: []string{"xyz-016a-ixb-dp", "XYZ-016a-ixb-dp", "XXXxyz-016a-ixb-dp", "XyZ-016a-ixb-op", "XXXxyz-016a-ixb-op"}, + expectedNotMatches: []string{"yz-016a-ixb-dp", "xyz-016a-ixb-o", "xyz", "dp", "\nxyz-016a-ixb-dp"}, + }, + + // Nested literal suffixes, case sensitive. + { + pattern: "(.*aaa|.*bbb(.*ccc|.*ddd))", + expectedLiteralSuffixMatchers: 3, + expectedMatches: []string{"aaa", "XXXaaa", "bbbccc", "XXXbbbccc", "XXXbbbXXXccc", "bbbddd", "bbbddd", "XXXbbbddd", "XXXbbbXXXddd", "bbbXXXccc", "aaabbbccc", "aaabbbddd"}, + expectedNotMatches: []string{"AAA", "aa", "Xaa", "BBBCCC", "bb", "Xbb", "bbccc", "bbbcc", "bbbdd"}, + }, + + // Mixed case sensitivity. + { + pattern: "(.*aaa|.*bbb((?i)(.*ccc|.*ddd)))", + expectedLiteralSuffixMatchers: 3, + expectedMatches: []string{"aaa", "XXXaaa", "bbbccc", "bbbCCC", "bbbXXXCCC", "bbbddd", "bbbDDD", "bbbXXXddd", "bbbXXXDDD"}, + expectedNotMatches: []string{"AAA", "XXXAAA", "BBBccc", "BBBCCC", "aaaBBB"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains literalSuffixStringMatcher. + numSuffixMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*literalSuffixStringMatcher); ok { + numSuffixMatchers++ + } + }) + + require.Equal(t, c.expectedLiteralSuffixMatchers, numSuffixMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func TestStringMatcherFromRegexp_Quest(t *testing.T) { + for _, c := range []struct { + pattern string + expectedZeroOrOneMatchers int + expectedMatches []string + expectedNotMatches []string + }{ + // Not match newline. + { + pattern: "test.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "test!"}, + expectedNotMatches: []string{"test\n", "tes", "test!!"}, + }, + { + pattern: ".?test", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "!test"}, + expectedNotMatches: []string{"\ntest", "tes", "test!"}, + }, + { + pattern: "(aaa.?|bbb.?)", + expectedZeroOrOneMatchers: 2, + expectedMatches: []string{"aaa", "aaaX", "bbb", "bbbX"}, + expectedNotMatches: []string{"aa", "aaaXX", "aaa\n", "bb", "bbbXX", "bbb\n"}, + }, + { + pattern: ".*aaa.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"aaa", "Xaaa", "aaaX", "XXXaaa", "XXXaaaX"}, + expectedNotMatches: []string{"aa", "aaaXX", "XXXaaaXXX", "XXXaaa\n"}, + }, + + // Match newline. + { + pattern: "(?s)test.?", + expectedZeroOrOneMatchers: 1, + expectedMatches: []string{"test", "test!", "test\n"}, + expectedNotMatches: []string{"tes", "test!!", "test\n\n"}, + }, + + // Mixed flags (a part matches newline another doesn't). + { + pattern: "(aaa.?|((?s).?bbb.+))", + expectedZeroOrOneMatchers: 2, + expectedMatches: []string{"aaa", "aaaX", "bbbX", "XbbbX", "bbbXXX", "\nbbbX"}, + expectedNotMatches: []string{"aa", "aaa\n", "Xbbb", "\nbbb"}, + }, + } { + t.Run(c.pattern, func(t *testing.T) { + parsed, err := syntax.Parse(c.pattern, syntax.Perl) + require.NoError(t, err) + + matcher := stringMatcherFromRegexp(parsed) + require.NotNil(t, matcher) + + re := regexp.MustCompile("^" + c.pattern + "$") + + // Pre-condition check: ensure it contains zeroOrOneCharacterStringMatcher. + numZeroOrOneMatchers := 0 + visitStringMatcher(matcher, func(matcher StringMatcher) { + if _, ok := matcher.(*zeroOrOneCharacterStringMatcher); ok { + numZeroOrOneMatchers++ + } + }) + + require.Equal(t, c.expectedZeroOrOneMatchers, numZeroOrOneMatchers) + + for _, value := range c.expectedMatches { + require.Truef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Truef(t, re.MatchString(value), "Value: %s", value) + } + + for _, value := range c.expectedNotMatches { + require.Falsef(t, matcher.Matches(value), "Value: %s", value) + + // Ensure the golang regexp engine would return the same. + require.Falsef(t, re.MatchString(value), "Value: %s", value) + } + }) + } +} + +func randString(randGenerator *rand.Rand, length int) string { + b := make([]rune, length) + for i := range b { + b[i] = asciiRunes[randGenerator.Intn(len(asciiRunes))] + } + return string(b) +} + +func randStrings(randGenerator *rand.Rand, many, length int) []string { + out := make([]string, 0, many) + for i := 0; i < many; i++ { + out = append(out, randString(randGenerator, length)) + } + return out +} + +func TestOptimizeEqualStringMatchers(t *testing.T) { + tests := map[string]struct { + input StringMatcher + expectedValues []string + expectedCaseSensitive bool + }{ + "should skip optimization on orStringMatcher with containsStringMatcher": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &containsStringMatcher{substrings: []string{"a", "b", "c"}}, + }, + expectedValues: nil, + }, + "should run optimization on orStringMatcher with equalStringMatcher and same case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: []string{"FOO", "bar", "baz"}, + expectedCaseSensitive: true, + }, + "should skip optimization on orStringMatcher with equalStringMatcher but different case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + &equalStringMatcher{s: "bar", caseSensitive: false}, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should run optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, and same case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + orStringMatcher{ + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "xxx", caseSensitive: true}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: []string{"FOO", "bar", "xxx", "baz"}, + expectedCaseSensitive: true, + }, + "should skip optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, but different case sensitivity": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + orStringMatcher{ + // Case sensitivity is different within items at the same level. + &equalStringMatcher{s: "bar", caseSensitive: true}, + &equalStringMatcher{s: "xxx", caseSensitive: false}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should skip optimization on orStringMatcher with nested orStringMatcher and equalStringMatcher, but different case sensitivity in the nested one": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: true}, + // Case sensitivity is different between the parent and child. + orStringMatcher{ + &equalStringMatcher{s: "bar", caseSensitive: false}, + &equalStringMatcher{s: "xxx", caseSensitive: false}, + }, + &equalStringMatcher{s: "baz", caseSensitive: true}, + }, + expectedValues: nil, + }, + "should return unchanged values on few case insensitive matchers": { + input: orStringMatcher{ + &equalStringMatcher{s: "FOO", caseSensitive: false}, + orStringMatcher{ + &equalStringMatcher{s: "bAr", caseSensitive: false}, + }, + &equalStringMatcher{s: "baZ", caseSensitive: false}, + }, + expectedValues: []string{"FOO", "bAr", "baZ"}, + expectedCaseSensitive: false, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + actualMatcher := optimizeEqualStringMatchers(testData.input, 0) + + if testData.expectedValues == nil { + require.IsType(t, testData.input, actualMatcher) + } else { + require.IsType(t, &equalMultiStringSliceMatcher{}, actualMatcher) + require.Equal(t, testData.expectedValues, actualMatcher.(*equalMultiStringSliceMatcher).values) + require.Equal(t, testData.expectedCaseSensitive, actualMatcher.(*equalMultiStringSliceMatcher).caseSensitive) + } + }) + } +} + +func TestNewEqualMultiStringMatcher(t *testing.T) { + tests := map[string]struct { + values []string + caseSensitive bool + expectedValuesMap map[string]struct{} + expectedValuesList []string + }{ + "few case sensitive values": { + values: []string{"a", "B"}, + caseSensitive: true, + expectedValuesList: []string{"a", "B"}, + }, + "few case insensitive values": { + values: []string{"a", "B"}, + caseSensitive: false, + expectedValuesList: []string{"a", "B"}, + }, + "many case sensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: true, + expectedValuesMap: map[string]struct{}{"a": {}, "B": {}, "c": {}, "D": {}, "e": {}, "F": {}, "g": {}, "H": {}, "i": {}, "L": {}, "m": {}, "N": {}, "o": {}, "P": {}, "q": {}, "r": {}}, + }, + "many case insensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: false, + expectedValuesMap: map[string]struct{}{"a": {}, "b": {}, "c": {}, "d": {}, "e": {}, "f": {}, "g": {}, "h": {}, "i": {}, "l": {}, "m": {}, "n": {}, "o": {}, "p": {}, "q": {}, "r": {}}, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + for _, v := range testData.values { + matcher.add(v) + } + if testData.expectedValuesMap != nil { + require.IsType(t, &equalMultiStringMapMatcher{}, matcher) + require.Equal(t, testData.expectedValuesMap, matcher.(*equalMultiStringMapMatcher).values) + require.Equal(t, testData.caseSensitive, matcher.(*equalMultiStringMapMatcher).caseSensitive) + } + if testData.expectedValuesList != nil { + require.IsType(t, &equalMultiStringSliceMatcher{}, matcher) + require.Equal(t, testData.expectedValuesList, matcher.(*equalMultiStringSliceMatcher).values) + require.Equal(t, testData.caseSensitive, matcher.(*equalMultiStringSliceMatcher).caseSensitive) + } + }) + } +} + +func TestEqualMultiStringMatcher_Matches(t *testing.T) { + tests := map[string]struct { + values []string + caseSensitive bool + expectedMatches []string + expectedNotMatches []string + }{ + "few case sensitive values": { + values: []string{"a", "B"}, + caseSensitive: true, + expectedMatches: []string{"a", "B"}, + expectedNotMatches: []string{"A", "b"}, + }, + "few case insensitive values": { + values: []string{"a", "B"}, + caseSensitive: false, + expectedMatches: []string{"a", "A", "b", "B"}, + expectedNotMatches: []string{"c", "C"}, + }, + "many case sensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: true, + expectedMatches: []string{"a", "B"}, + expectedNotMatches: []string{"A", "b"}, + }, + "many case insensitive values": { + values: []string{"a", "B", "c", "D", "e", "F", "g", "H", "i", "L", "m", "N", "o", "P", "q", "r"}, + caseSensitive: false, + expectedMatches: []string{"a", "A", "b", "B"}, + expectedNotMatches: []string{"x", "X"}, + }, + } + + for testName, testData := range tests { + t.Run(testName, func(t *testing.T) { + matcher := newEqualMultiStringMatcher(testData.caseSensitive, len(testData.values)) + for _, v := range testData.values { + matcher.add(v) + } + + for _, v := range testData.expectedMatches { + require.True(t, matcher.Matches(v), "value: %s", v) + } + for _, v := range testData.expectedNotMatches { + require.False(t, matcher.Matches(v), "value: %s", v) + } + }) + } +} + +func TestFindEqualStringMatchers(t *testing.T) { + type match struct { + s string + caseSensitive bool + } + + // Utility to call findEqualStringMatchers() and collect all callback invocations. + findEqualStringMatchersAndCollectMatches := func(input StringMatcher) (matches []match, ok bool) { + ok = findEqualStringMatchers(input, func(matcher *equalStringMatcher) bool { + matches = append(matches, match{matcher.s, matcher.caseSensitive}) + return true + }) + return + } + + t.Run("empty matcher", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches(emptyStringMatcher{}) + require.False(t, actualOk) + require.Empty(t, actualMatches) + }) + + t.Run("concat of literal matchers (case sensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: true}, + &equalStringMatcher{s: "test-2", caseSensitive: true}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", true}, {"test-2", true}}, actualMatches) + }) + + t.Run("concat of literal matchers (case insensitive)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: false}, + &equalStringMatcher{s: "test-2", caseSensitive: false}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"test-2", false}}, actualMatches) + }) + + t.Run("concat of literal matchers (mixed case)", func(t *testing.T) { + actualMatches, actualOk := findEqualStringMatchersAndCollectMatches( + orStringMatcher{ + &equalStringMatcher{s: "test-1", caseSensitive: false}, + &equalStringMatcher{s: "test-2", caseSensitive: true}, + }, + ) + + require.True(t, actualOk) + require.Equal(t, []match{{"test-1", false}, {"test-2", true}}, actualMatches) + }) +} + +// This benchmark is used to find a good threshold to use to apply the optimization +// done by optimizeEqualStringMatchers(). +func BenchmarkOptimizeEqualStringMatchers(b *testing.B) { + randGenerator := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Generate variable lengths random texts to match against. + texts := append([]string{}, randStrings(randGenerator, 10, 10)...) + texts = append(texts, randStrings(randGenerator, 5, 30)...) + texts = append(texts, randStrings(randGenerator, 1, 100)...) + + for numAlternations := 2; numAlternations <= 256; numAlternations *= 2 { + for _, caseSensitive := range []bool{true, false} { + b.Run(fmt.Sprintf("alternations: %d case sensitive: %t", numAlternations, caseSensitive), func(b *testing.B) { + // Generate a regex with the expected number of alternations. + re := strings.Join(randStrings(randGenerator, numAlternations, 10), "|") + if !caseSensitive { + re = "(?i:(" + re + "))" + } + + parsed, err := syntax.Parse(re, syntax.Perl) + require.NoError(b, err) + + unoptimized := stringMatcherFromRegexpInternal(parsed) + require.IsType(b, orStringMatcher{}, unoptimized) + + optimized := optimizeEqualStringMatchers(unoptimized, 0) + if numAlternations < minEqualMultiStringMatcherMapThreshold { + require.IsType(b, &equalMultiStringSliceMatcher{}, optimized) + } else { + require.IsType(b, &equalMultiStringMapMatcher{}, optimized) + } + + b.Run("without optimizeEqualStringMatchers()", func(b *testing.B) { + for n := 0; n < b.N; n++ { + for _, t := range texts { + unoptimized.Matches(t) + } + } + }) + + b.Run("with optimizeEqualStringMatchers()", func(b *testing.B) { + for n := 0; n < b.N; n++ { + for _, t := range texts { + optimized.Matches(t) + } + } + }) + }) + } + } +} + +func TestZeroOrOneCharacterStringMatcher(t *testing.T) { + matcher := &zeroOrOneCharacterStringMatcher{matchNL: true} + require.True(t, matcher.Matches("")) + require.True(t, matcher.Matches("x")) + require.True(t, matcher.Matches("\n")) + require.False(t, matcher.Matches("xx")) + require.False(t, matcher.Matches("\n\n")) + + matcher = &zeroOrOneCharacterStringMatcher{matchNL: false} + require.True(t, matcher.Matches("")) + require.True(t, matcher.Matches("x")) + require.False(t, matcher.Matches("\n")) + require.False(t, matcher.Matches("xx")) + require.False(t, matcher.Matches("\n\n")) +} + +func TestLiteralPrefixStringMatcher(t *testing.T) { + m := &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: true, right: &emptyStringMatcher{}} + require.True(t, m.Matches("mar")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("ma")) + require.False(t, m.Matches("mAr")) + + m = &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: false, right: &emptyStringMatcher{}} + require.True(t, m.Matches("mar")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("ma")) + require.True(t, m.Matches("mAr")) + + m = &literalPrefixStringMatcher{prefix: "mar", prefixCaseSensitive: true, right: &equalStringMatcher{s: "co", caseSensitive: false}} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("marCO")) + require.False(t, m.Matches("MARco")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) +} + +func TestLiteralSuffixStringMatcher(t *testing.T) { + m := &literalSuffixStringMatcher{left: &emptyStringMatcher{}, suffix: "co", suffixCaseSensitive: true} + require.True(t, m.Matches("co")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("coo")) + require.False(t, m.Matches("Co")) + + m = &literalSuffixStringMatcher{left: &emptyStringMatcher{}, suffix: "co", suffixCaseSensitive: false} + require.True(t, m.Matches("co")) + require.False(t, m.Matches("marco")) + require.False(t, m.Matches("coo")) + require.True(t, m.Matches("Co")) + + m = &literalSuffixStringMatcher{left: &equalStringMatcher{s: "mar", caseSensitive: false}, suffix: "co", suffixCaseSensitive: true} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("MARco")) + require.False(t, m.Matches("marCO")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) + + m = &literalSuffixStringMatcher{left: &equalStringMatcher{s: "mar", caseSensitive: false}, suffix: "co", suffixCaseSensitive: false} + require.True(t, m.Matches("marco")) + require.True(t, m.Matches("MARco")) + require.True(t, m.Matches("marCO")) + require.False(t, m.Matches("mar")) + require.False(t, m.Matches("marcopracucci")) +} + +func TestHasPrefixCaseInsensitive(t *testing.T) { + require.True(t, hasPrefixCaseInsensitive("marco", "mar")) + require.True(t, hasPrefixCaseInsensitive("mArco", "mar")) + require.True(t, hasPrefixCaseInsensitive("marco", "MaR")) + require.True(t, hasPrefixCaseInsensitive("marco", "marco")) + require.True(t, hasPrefixCaseInsensitive("mArco", "marco")) + + require.False(t, hasPrefixCaseInsensitive("marco", "a")) + require.False(t, hasPrefixCaseInsensitive("marco", "abcdefghi")) +} + +func TestHasSuffixCaseInsensitive(t *testing.T) { + require.True(t, hasSuffixCaseInsensitive("marco", "rco")) + require.True(t, hasSuffixCaseInsensitive("marco", "RcO")) + require.True(t, hasSuffixCaseInsensitive("marco", "marco")) + require.False(t, hasSuffixCaseInsensitive("marco", "a")) + require.False(t, hasSuffixCaseInsensitive("marco", "abcdefghi")) +} + +func getTestNameFromRegexp(re string) string { + if len(re) > 32 { + return re[:32] + } + return re +} + +func generateRandomValues() []string { + // Init the random seed with a constant, so that it doesn't change between runs. + randGenerator := rand.New(rand.NewSource(1)) + + // Generate variable lengths random texts to match against. + texts := append([]string{}, randStrings(randGenerator, 10, 10)...) + texts = append(texts, randStrings(randGenerator, 5, 30)...) + texts = append(texts, randStrings(randGenerator, 1, 100)...) + texts = append(texts, "foo"+randString(randGenerator, 50)) + texts = append(texts, randString(randGenerator, 50)+"foo") + + return texts +} + +func visitStringMatcher(matcher StringMatcher, callback func(matcher StringMatcher)) { + callback(matcher) + + switch casted := matcher.(type) { + case *containsStringMatcher: + if casted.left != nil { + visitStringMatcher(casted.left, callback) + } + if casted.right != nil { + visitStringMatcher(casted.right, callback) + } + + case *literalPrefixStringMatcher: + visitStringMatcher(casted.right, callback) + + case *literalSuffixStringMatcher: + visitStringMatcher(casted.left, callback) + + case orStringMatcher: + for _, entry := range casted { + visitStringMatcher(entry, callback) + } + + // No nested matchers for the folling ones. + case emptyStringMatcher: + case *equalStringMatcher: + case *equalMultiStringSliceMatcher: + case *equalMultiStringMapMatcher: + case anyStringWithoutNewlineMatcher: + case *anyNonEmptyStringMatcher: + case trueMatcher: } } From f639d7794c456c4ec1b563d5bf1576bc2f6d832b Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 14:57:43 +0100 Subject: [PATCH 02/29] Fix TestParseExpressions Signed-off-by: Marco Pracucci --- promql/parser/parse_test.go | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/promql/parser/parse_test.go b/promql/parser/parse_test.go index 6c26445e38..7150891161 100644 --- a/promql/parser/parse_test.go +++ b/promql/parser/parse_test.go @@ -3568,7 +3568,31 @@ func TestParseExpressions(t *testing.T) { if !test.fail { require.NoError(t, err) - require.Equal(t, test.expected, expr, "error on input '%s'", test.input) + expected := test.expected + + // The FastRegexMatcher is not comparable with a deep equal, so only compare its String() version. + if actualVector, ok := expr.(*VectorSelector); ok { + require.IsType(t, &VectorSelector{}, test.expected, "error on input '%s'", test.input) + expectedVector := test.expected.(*VectorSelector) + + require.Len(t, actualVector.LabelMatchers, len(expectedVector.LabelMatchers), "error on input '%s'", test.input) + + for i := 0; i < len(actualVector.LabelMatchers); i++ { + expectedMatcher := expectedVector.LabelMatchers[i].String() + actualMatcher := actualVector.LabelMatchers[i].String() + + require.Equal(t, expectedMatcher, actualMatcher, "unexpected label matcher '%s' on input '%s'", actualMatcher, test.input) + } + + // Make a shallow copy of the expected expr (because the test cases are defined in a global variable) + // and then reset the LabelMatcher to not compared them with the following deep equal. + expectedCopy := *expectedVector + expectedCopy.LabelMatchers = nil + expected = &expectedCopy + actualVector.LabelMatchers = nil + } + + require.Equal(t, expected, expr, "error on input '%s'", test.input) } else { require.Error(t, err) require.Contains(t, err.Error(), test.errMsg, "unexpected error on input '%s', expected '%s', got '%s'", test.input, test.errMsg, err.Error()) From a1a45990a2eae1788d311f24166b2c780ee48b80 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Thu, 25 Jan 2024 14:59:39 +0100 Subject: [PATCH 03/29] Fix TestPostingsForMatcher Signed-off-by: Marco Pracucci --- tsdb/querier_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go index fcedc54621..ce59c0ee77 100644 --- a/tsdb/querier_test.go +++ b/tsdb/querier_test.go @@ -3304,7 +3304,7 @@ func TestPostingsForMatcher(t *testing.T) { { // Test case for double quoted regex matcher matcher: labels.MustNewMatcher(labels.MatchRegexp, "test", "^(?:a|b)$"), - hasError: true, + hasError: false, }, } From 515890ec5307a4e8448c50589ac3fc442e0e4671 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Fri, 26 Jan 2024 06:26:52 +0100 Subject: [PATCH 04/29] Use Matcher.SetMatches() Signed-off-by: Marco Pracucci --- tsdb/querier.go | 54 ++----------------------------------- tsdb/querier_test.go | 63 -------------------------------------------- 2 files changed, 2 insertions(+), 115 deletions(-) diff --git a/tsdb/querier.go b/tsdb/querier.go index a692c98f1a..4473323414 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "math" - "strings" "unicode/utf8" "github.com/oklog/ulid" @@ -186,55 +185,6 @@ func (q *blockChunkQuerier) Select(ctx context.Context, sortSeries bool, hints * return NewBlockChunkSeriesSet(q.blockID, q.index, q.chunks, q.tombstones, p, mint, maxt, disableTrimming) } -func findSetMatches(pattern string) []string { - // Return empty matches if the wrapper from Prometheus is missing. - if len(pattern) < 6 || pattern[:4] != "^(?:" || pattern[len(pattern)-2:] != ")$" { - return nil - } - escaped := false - sets := []*strings.Builder{{}} - init := 4 - end := len(pattern) - 2 - // If the regex is wrapped in a group we can remove the first and last parentheses - if pattern[init] == '(' && pattern[end-1] == ')' { - init++ - end-- - } - for i := init; i < end; i++ { - if escaped { - switch { - case isRegexMetaCharacter(pattern[i]): - sets[len(sets)-1].WriteByte(pattern[i]) - case pattern[i] == '\\': - sets[len(sets)-1].WriteByte('\\') - default: - return nil - } - escaped = false - } else { - switch { - case isRegexMetaCharacter(pattern[i]): - if pattern[i] == '|' { - sets = append(sets, &strings.Builder{}) - } else { - return nil - } - case pattern[i] == '\\': - escaped = true - default: - sets[len(sets)-1].WriteByte(pattern[i]) - } - } - } - matches := make([]string, 0, len(sets)) - for _, s := range sets { - if s.Len() > 0 { - matches = append(matches, s.String()) - } - } - return matches -} - // PostingsForMatchers assembles a single postings iterator against the index reader // based on the given matchers. The resulting postings are not ordered by series. func PostingsForMatchers(ctx context.Context, ix IndexReader, ms ...*labels.Matcher) (index.Postings, error) { @@ -376,7 +326,7 @@ func postingsForMatcher(ctx context.Context, ix IndexReader, m *labels.Matcher) // Fast-path for set matching. if m.Type == labels.MatchRegexp { - setMatches := findSetMatches(m.GetRegexString()) + setMatches := m.SetMatches() if len(setMatches) > 0 { return ix.Postings(ctx, m.Name, setMatches...) } @@ -407,7 +357,7 @@ func inversePostingsForMatcher(ctx context.Context, ix IndexReader, m *labels.Ma // Inverse of a MatchNotRegexp is MatchRegexp (double negation). // Fast-path for set matching. if m.Type == labels.MatchNotRegexp { - setMatches := findSetMatches(m.GetRegexString()) + setMatches := m.SetMatches() if len(setMatches) > 0 { return ix.Postings(ctx, m.Name, setMatches...) } diff --git a/tsdb/querier_test.go b/tsdb/querier_test.go index ce59c0ee77..ed7e4e3404 100644 --- a/tsdb/querier_test.go +++ b/tsdb/querier_test.go @@ -2637,69 +2637,6 @@ func BenchmarkSetMatcher(b *testing.B) { } } -// Refer to https://github.com/prometheus/prometheus/issues/2651. -func TestFindSetMatches(t *testing.T) { - cases := []struct { - pattern string - exp []string - }{ - // Single value, coming from a `bar=~"foo"` selector. - { - pattern: "^(?:foo)$", - exp: []string{ - "foo", - }, - }, - // Simple sets. - { - pattern: "^(?:foo|bar|baz)$", - exp: []string{ - "foo", - "bar", - "baz", - }, - }, - // Simple sets containing escaped characters. - { - pattern: "^(?:fo\\.o|bar\\?|\\^baz)$", - exp: []string{ - "fo.o", - "bar?", - "^baz", - }, - }, - // Simple sets containing special characters without escaping. - { - pattern: "^(?:fo.o|bar?|^baz)$", - exp: nil, - }, - // Missing wrapper. - { - pattern: "foo|bar|baz", - exp: nil, - }, - } - - for _, c := range cases { - matches := findSetMatches(c.pattern) - if len(c.exp) == 0 { - if len(matches) != 0 { - t.Errorf("Evaluating %s, unexpected result %v", c.pattern, matches) - } - } else { - if len(matches) != len(c.exp) { - t.Errorf("Evaluating %s, length of result not equal to exp", c.pattern) - } else { - for i := 0; i < len(c.exp); i++ { - if c.exp[i] != matches[i] { - t.Errorf("Evaluating %s, unexpected result %s", c.pattern, matches[i]) - } - } - } - } - } -} - func TestPostingsForMatchers(t *testing.T) { ctx := context.Background() From ec9cada56e78a3332e64beafd2874229c7503fb2 Mon Sep 17 00:00:00 2001 From: Marco Pracucci Date: Fri, 26 Jan 2024 06:35:02 +0100 Subject: [PATCH 05/29] Remove unused isRegexMetaCharacter() Signed-off-by: Marco Pracucci --- tsdb/querier.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tsdb/querier.go b/tsdb/querier.go index 4473323414..899674ef12 100644 --- a/tsdb/querier.go +++ b/tsdb/querier.go @@ -18,7 +18,6 @@ import ( "errors" "fmt" "math" - "unicode/utf8" "github.com/oklog/ulid" "golang.org/x/exp/slices" @@ -34,20 +33,6 @@ import ( "github.com/prometheus/prometheus/util/annotations" ) -// Bitmap used by func isRegexMetaCharacter to check whether a character needs to be escaped. -var regexMetaCharacterBytes [16]byte - -// isRegexMetaCharacter reports whether byte b needs to be escaped. -func isRegexMetaCharacter(b byte) bool { - return b < utf8.RuneSelf && regexMetaCharacterBytes[b%16]&(1<<(b/16)) != 0 -} - -func init() { - for _, b := range []byte(`.+*?()|[]{}^$`) { - regexMetaCharacterBytes[b%16] |= 1 << (b / 16) - } -} - type blockBaseQuerier struct { blockID ulid.ULID index IndexReader From b8d428b753981db46c7b896709bdc2d09d06dc03 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:07:57 +0000 Subject: [PATCH 06/29] Refactor: Azure Discovery: extract function to generate labelSet This should make it easier to test. Signed-off-by: Bryan Boreham --- discovery/azure/azure.go | 181 ++++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 90 deletions(-) diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 16628c7bfd..8936a33adb 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -374,96 +374,8 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { for _, vm := range machines { go func(vm virtualMachine) { defer wg.Done() - r, err := newAzureResourceFromID(vm.ID, d.logger) - if err != nil { - ch <- target{labelSet: nil, err: err} - return - } - - labels := model.LabelSet{ - azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID), - azureLabelTenantID: model.LabelValue(d.cfg.TenantID), - azureLabelMachineID: model.LabelValue(vm.ID), - azureLabelMachineName: model.LabelValue(vm.Name), - azureLabelMachineComputerName: model.LabelValue(vm.ComputerName), - azureLabelMachineOSType: model.LabelValue(vm.OsType), - azureLabelMachineLocation: model.LabelValue(vm.Location), - azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroupName), - azureLabelMachineSize: model.LabelValue(vm.Size), - } - - if vm.ScaleSet != "" { - labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet) - } - - for k, v := range vm.Tags { - name := strutil.SanitizeLabelName(k) - labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v) - } - - // Get the IP address information via separate call to the network provider. - for _, nicID := range vm.NetworkInterfaces { - var networkInterface *armnetwork.Interface - if v, ok := d.getFromCache(nicID); ok { - networkInterface = v - d.metrics.cacheHitCount.Add(1) - } else { - if vm.ScaleSet == "" { - networkInterface, err = client.getVMNetworkInterfaceByID(ctx, nicID) - } else { - networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) - } - - if err != nil { - if errors.Is(err, errorNotFound) { - level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) - } else { - ch <- target{labelSet: nil, err: err} - } - - // Get out of this routine because we cannot continue without a network interface. - return - } - - // Continue processing with the network interface - d.addToCache(nicID, networkInterface) - } - - if networkInterface.Properties == nil { - continue - } - - // Unfortunately Azure does not return information on whether a VM is deallocated. - // This information is available via another API call however the Go SDK does not - // yet support this. On deallocated machines, this value happens to be nil so it - // is a cheap and easy way to determine if a machine is allocated or not. - if networkInterface.Properties.Primary == nil { - level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name) - return - } - - if *networkInterface.Properties.Primary { - for _, ip := range networkInterface.Properties.IPConfigurations { - // IPAddress is a field defined in PublicIPAddressPropertiesFormat, - // therefore we need to validate that both are not nil. - if ip.Properties != nil && ip.Properties.PublicIPAddress != nil && ip.Properties.PublicIPAddress.Properties != nil && ip.Properties.PublicIPAddress.Properties.IPAddress != nil { - labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.Properties.PublicIPAddress.Properties.IPAddress) - } - if ip.Properties != nil && ip.Properties.PrivateIPAddress != nil { - labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.Properties.PrivateIPAddress) - address := net.JoinHostPort(*ip.Properties.PrivateIPAddress, fmt.Sprintf("%d", d.port)) - labels[model.AddressLabel] = model.LabelValue(address) - ch <- target{labelSet: labels, err: nil} - return - } - // If we made it here, we don't have a private IP which should be impossible. - // Return an empty target and error to ensure an all or nothing situation. - err = fmt.Errorf("unable to find a private IP for VM %s", vm.Name) - ch <- target{labelSet: nil, err: err} - return - } - } - } + labelSet, err := d.vmToLabelSet(ctx, client, vm) + ch <- target{labelSet: labelSet, err: err} }(vm) } @@ -484,6 +396,95 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { return []*targetgroup.Group{&tg}, nil } +func (d *Discovery) vmToLabelSet(ctx context.Context, client azureClient, vm virtualMachine) (model.LabelSet, error) { + r, err := newAzureResourceFromID(vm.ID, d.logger) + if err != nil { + return nil, err + } + + labels := model.LabelSet{ + azureLabelSubscriptionID: model.LabelValue(d.cfg.SubscriptionID), + azureLabelTenantID: model.LabelValue(d.cfg.TenantID), + azureLabelMachineID: model.LabelValue(vm.ID), + azureLabelMachineName: model.LabelValue(vm.Name), + azureLabelMachineComputerName: model.LabelValue(vm.ComputerName), + azureLabelMachineOSType: model.LabelValue(vm.OsType), + azureLabelMachineLocation: model.LabelValue(vm.Location), + azureLabelMachineResourceGroup: model.LabelValue(r.ResourceGroupName), + azureLabelMachineSize: model.LabelValue(vm.Size), + } + + if vm.ScaleSet != "" { + labels[azureLabelMachineScaleSet] = model.LabelValue(vm.ScaleSet) + } + + for k, v := range vm.Tags { + name := strutil.SanitizeLabelName(k) + labels[azureLabelMachineTag+model.LabelName(name)] = model.LabelValue(*v) + } + + // Get the IP address information via separate call to the network provider. + for _, nicID := range vm.NetworkInterfaces { + var networkInterface *armnetwork.Interface + if v, ok := d.getFromCache(nicID); ok { + networkInterface = v + d.metrics.cacheHitCount.Add(1) + } else { + if vm.ScaleSet == "" { + networkInterface, err = client.getVMNetworkInterfaceByID(ctx, nicID) + } else { + networkInterface, err = client.getVMScaleSetVMNetworkInterfaceByID(ctx, nicID, vm.ScaleSet, vm.InstanceID) + } + if err != nil { + if errors.Is(err, errorNotFound) { + level.Warn(d.logger).Log("msg", "Network interface does not exist", "name", nicID, "err", err) + } else { + return nil, err + } + // Get out of this routine because we cannot continue without a network interface. + return nil, nil + } + + // Continue processing with the network interface + d.addToCache(nicID, networkInterface) + } + + if networkInterface.Properties == nil { + continue + } + + // Unfortunately Azure does not return information on whether a VM is deallocated. + // This information is available via another API call however the Go SDK does not + // yet support this. On deallocated machines, this value happens to be nil so it + // is a cheap and easy way to determine if a machine is allocated or not. + if networkInterface.Properties.Primary == nil { + level.Debug(d.logger).Log("msg", "Skipping deallocated virtual machine", "machine", vm.Name) + return nil, nil + } + + if *networkInterface.Properties.Primary { + for _, ip := range networkInterface.Properties.IPConfigurations { + // IPAddress is a field defined in PublicIPAddressPropertiesFormat, + // therefore we need to validate that both are not nil. + if ip.Properties != nil && ip.Properties.PublicIPAddress != nil && ip.Properties.PublicIPAddress.Properties != nil && ip.Properties.PublicIPAddress.Properties.IPAddress != nil { + labels[azureLabelMachinePublicIP] = model.LabelValue(*ip.Properties.PublicIPAddress.Properties.IPAddress) + } + if ip.Properties != nil && ip.Properties.PrivateIPAddress != nil { + labels[azureLabelMachinePrivateIP] = model.LabelValue(*ip.Properties.PrivateIPAddress) + address := net.JoinHostPort(*ip.Properties.PrivateIPAddress, fmt.Sprintf("%d", d.port)) + labels[model.AddressLabel] = model.LabelValue(address) + return labels, nil + } + // If we made it here, we don't have a private IP which should be impossible. + // Return an empty target and error to ensure an all or nothing situation. + return nil, fmt.Errorf("unable to find a private IP for VM %s", vm.Name) + } + } + } + // TODO: Should we say something at this point? + return nil, nil +} + func (client *azureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) { var vms []virtualMachine if len(resourceGroup) == 0 { From 4e24e5b1d166842d6f2c604c81defd45223e4e37 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:41:26 +0000 Subject: [PATCH 07/29] Refactor: Azure Discovery: introduce an interface for the client So we can mock it. Signed-off-by: Bryan Boreham --- discovery/azure/azure.go | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/discovery/azure/azure.go b/discovery/azure/azure.go index 8936a33adb..6bde835e5d 100644 --- a/discovery/azure/azure.go +++ b/discovery/azure/azure.go @@ -212,6 +212,14 @@ func NewDiscovery(cfg *SDConfig, logger log.Logger, metrics discovery.Discoverer return d, nil } +type client interface { + getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) + getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) + getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) + getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) + getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) +} + // azureClient represents multiple Azure Resource Manager providers. type azureClient struct { nic *armnetwork.InterfacesClient @@ -221,14 +229,17 @@ type azureClient struct { logger log.Logger } +var _ client = &azureClient{} + // createAzureClient is a helper function for creating an Azure compute client to ARM. -func createAzureClient(cfg SDConfig) (azureClient, error) { +func createAzureClient(cfg SDConfig, logger log.Logger) (client, error) { cloudConfiguration, err := CloudConfigurationFromName(cfg.Environment) if err != nil { - return azureClient{}, err + return &azureClient{}, err } var c azureClient + c.logger = logger telemetry := policy.TelemetryOptions{ ApplicationID: userAgent, @@ -239,12 +250,12 @@ func createAzureClient(cfg SDConfig) (azureClient, error) { Telemetry: telemetry, }) if err != nil { - return azureClient{}, err + return &azureClient{}, err } client, err := config_util.NewClientFromConfig(cfg.HTTPClientConfig, "azure_sd") if err != nil { - return azureClient{}, err + return &azureClient{}, err } options := &arm.ClientOptions{ ClientOptions: policy.ClientOptions{ @@ -256,25 +267,25 @@ func createAzureClient(cfg SDConfig) (azureClient, error) { c.vm, err = armcompute.NewVirtualMachinesClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.nic, err = armnetwork.NewInterfacesClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.vmss, err = armcompute.NewVirtualMachineScaleSetsClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } c.vmssvm, err = armcompute.NewVirtualMachineScaleSetVMsClient(cfg.SubscriptionID, credential, options) if err != nil { - return azureClient{}, err + return &azureClient{}, err } - return c, nil + return &c, nil } func newCredential(cfg SDConfig, policyClientOptions policy.ClientOptions) (azcore.TokenCredential, error) { @@ -330,12 +341,11 @@ func newAzureResourceFromID(id string, logger log.Logger) (*arm.ResourceID, erro func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { defer level.Debug(d.logger).Log("msg", "Azure discovery completed") - client, err := createAzureClient(*d.cfg) + client, err := createAzureClient(*d.cfg, d.logger) if err != nil { d.metrics.failuresCount.Inc() return nil, fmt.Errorf("could not create Azure client: %w", err) } - client.logger = d.logger machines, err := client.getVMs(ctx, d.cfg.ResourceGroup) if err != nil { @@ -396,7 +406,7 @@ func (d *Discovery) refresh(ctx context.Context) ([]*targetgroup.Group, error) { return []*targetgroup.Group{&tg}, nil } -func (d *Discovery) vmToLabelSet(ctx context.Context, client azureClient, vm virtualMachine) (model.LabelSet, error) { +func (d *Discovery) vmToLabelSet(ctx context.Context, client client, vm virtualMachine) (model.LabelSet, error) { r, err := newAzureResourceFromID(vm.ID, d.logger) if err != nil { return nil, err From 5f2c0c5283c745652e109c6174b5d539eaf9430a Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 14:59:31 +0000 Subject: [PATCH 08/29] Azure Discovery tests: mock the azure client interface Signed-off-by: Bryan Boreham --- discovery/azure/azure_test.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go index 1e437c75f2..0daaab86be 100644 --- a/discovery/azure/azure_test.go +++ b/discovery/azure/azure_test.go @@ -14,10 +14,13 @@ package azure import ( + "context" + "fmt" "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) @@ -381,3 +384,35 @@ func TestNewAzureResourceFromID(t *testing.T) { require.Equal(t, tc.expected.ResourceGroupName, actual.ResourceGroupName) } } + +type mockAzureClient struct { + networkInterface *armnetwork.Interface +} + +var _ client = &mockAzureClient{} + +func (*mockAzureClient) getVMs(ctx context.Context, resourceGroup string) ([]virtualMachine, error) { + return nil, nil +} + +func (*mockAzureClient) getScaleSets(ctx context.Context, resourceGroup string) ([]armcompute.VirtualMachineScaleSet, error) { + return nil, nil +} + +func (*mockAzureClient) getScaleSetVMs(ctx context.Context, scaleSet armcompute.VirtualMachineScaleSet) ([]virtualMachine, error) { + return nil, nil +} + +func (m *mockAzureClient) getVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID string) (*armnetwork.Interface, error) { + if networkInterfaceID == "" { + return nil, fmt.Errorf("parameter networkInterfaceID cannot be empty") + } + return m.networkInterface, nil +} + +func (m *mockAzureClient) getVMScaleSetVMNetworkInterfaceByID(ctx context.Context, networkInterfaceID, scaleSetName, instanceID string) (*armnetwork.Interface, error) { + if scaleSetName == "" { + return nil, fmt.Errorf("parameter virtualMachineScaleSetName cannot be empty") + } + return m.networkInterface, nil +} From ab9c544ec7371c0f0b146e082781f539d3f52ab4 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Fri, 8 Mar 2024 15:12:41 +0000 Subject: [PATCH 09/29] Azure Discovery tests: Add test for VMToLabelSet Test fails due to bug in code on main. Signed-off-by: Bryan Boreham --- discovery/azure/azure_test.go | 92 ++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/discovery/azure/azure_test.go b/discovery/azure/azure_test.go index 0daaab86be..32dab66c8c 100644 --- a/discovery/azure/azure_test.go +++ b/discovery/azure/azure_test.go @@ -21,12 +21,17 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v4" + cache "github.com/Code-Hex/go-generics-cache" + "github.com/Code-Hex/go-generics-cache/policy/lru" + "github.com/go-kit/log" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestMain(m *testing.M) { - goleak.VerifyTestMain(m) + goleak.VerifyTestMain(m, + goleak.IgnoreTopFunction("github.com/Code-Hex/go-generics-cache.(*janitor).run.func1"), + ) } func TestMapFromVMWithEmptyTags(t *testing.T) { @@ -82,6 +87,91 @@ func TestMapFromVMWithEmptyTags(t *testing.T) { require.Equal(t, expectedVM, actualVM) } +func TestVMToLabelSet(t *testing.T) { + id := "/subscriptions/00000000-0000-0000-0000-000000000000/test" + name := "name" + size := "size" + vmSize := armcompute.VirtualMachineSizeTypes(size) + osType := armcompute.OperatingSystemTypesLinux + vmType := "type" + location := "westeurope" + computerName := "computer_name" + networkID := "/subscriptions/00000000-0000-0000-0000-000000000000/network1" + ipAddress := "10.20.30.40" + primary := true + networkProfile := armcompute.NetworkProfile{ + NetworkInterfaces: []*armcompute.NetworkInterfaceReference{ + { + ID: &networkID, + Properties: &armcompute.NetworkInterfaceReferenceProperties{Primary: &primary}, + }, + }, + } + properties := &armcompute.VirtualMachineProperties{ + OSProfile: &armcompute.OSProfile{ + ComputerName: &computerName, + }, + StorageProfile: &armcompute.StorageProfile{ + OSDisk: &armcompute.OSDisk{ + OSType: &osType, + }, + }, + NetworkProfile: &networkProfile, + HardwareProfile: &armcompute.HardwareProfile{ + VMSize: &vmSize, + }, + } + + testVM := armcompute.VirtualMachine{ + ID: &id, + Name: &name, + Type: &vmType, + Location: &location, + Tags: nil, + Properties: properties, + } + + expectedVM := virtualMachine{ + ID: id, + Name: name, + ComputerName: computerName, + Type: vmType, + Location: location, + OsType: "Linux", + Tags: map[string]*string{}, + NetworkInterfaces: []string{networkID}, + Size: size, + } + + actualVM := mapFromVM(testVM) + + require.Equal(t, expectedVM, actualVM) + + cfg := DefaultSDConfig + d := &Discovery{ + cfg: &cfg, + logger: log.NewNopLogger(), + cache: cache.New(cache.AsLRU[string, *armnetwork.Interface](lru.WithCapacity(5))), + } + network := armnetwork.Interface{ + Name: &networkID, + Properties: &armnetwork.InterfacePropertiesFormat{ + Primary: &primary, + IPConfigurations: []*armnetwork.InterfaceIPConfiguration{ + {Properties: &armnetwork.InterfaceIPConfigurationPropertiesFormat{ + PrivateIPAddress: &ipAddress, + }}, + }, + }, + } + client := &mockAzureClient{ + networkInterface: &network, + } + labelSet, err := d.vmToLabelSet(context.Background(), client, actualVM) + require.NoError(t, err) + require.Len(t, labelSet, 11) +} + func TestMapFromVMWithEmptyOSType(t *testing.T) { id := "test" name := "name" From cef1025ea80a1a901cf0e2bbe7b589563dd16a8e Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Fri, 15 Mar 2024 10:11:04 +0100 Subject: [PATCH 10/29] tsdb/wlog.Checkpoint: Fix counting of histogram samples Signed-off-by: Arve Knudsen --- tsdb/wlog/checkpoint.go | 4 ++-- tsdb/wlog/checkpoint_test.go | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go index 4ad1bb2365..5491d9718e 100644 --- a/tsdb/wlog/checkpoint.go +++ b/tsdb/wlog/checkpoint.go @@ -224,8 +224,8 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head if len(repl) > 0 { buf = enc.HistogramSamples(repl, buf) } - stats.TotalSamples += len(samples) - stats.DroppedSamples += len(samples) - len(repl) + stats.TotalSamples += len(histogramSamples) + stats.DroppedSamples += len(histogramSamples) - len(repl) case record.Tombstones: tstones, err = dec.Tombstones(rec, tstones) diff --git a/tsdb/wlog/checkpoint_test.go b/tsdb/wlog/checkpoint_test.go index 142a5a9d49..0d22171768 100644 --- a/tsdb/wlog/checkpoint_test.go +++ b/tsdb/wlog/checkpoint_test.go @@ -220,12 +220,14 @@ func TestCheckpoint(t *testing.T) { } require.NoError(t, w.Close()) - _, err = Checkpoint(log.NewNopLogger(), w, 100, 106, func(x chunks.HeadSeriesRef) bool { + stats, err := Checkpoint(log.NewNopLogger(), w, 100, 106, func(x chunks.HeadSeriesRef) bool { return x%2 == 0 }, last/2) require.NoError(t, err) require.NoError(t, w.Truncate(107)) require.NoError(t, DeleteCheckpoints(w.Dir(), 106)) + require.Equal(t, histogramsInWAL+samplesInWAL, stats.TotalSamples) + require.Greater(t, stats.DroppedSamples, 0) // Only the new checkpoint should be left. files, err := os.ReadDir(dir) From 9c7a7340630b61d296bb770e6d450b09be759878 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 19 Mar 2024 09:10:21 +0100 Subject: [PATCH 11/29] tsdb.BeyondTimeRetention: Fix comment and test at retention duration Signed-off-by: Arve Knudsen --- tsdb/db.go | 2 +- tsdb/db_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tsdb/db.go b/tsdb/db.go index e49c5811d2..f83c7c4958 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -1608,7 +1608,7 @@ func BeyondTimeRetention(db *DB, blocks []*Block) (deletable map[ulid.ULID]struc deletable = make(map[ulid.ULID]struct{}) for i, block := range blocks { - // The difference between the first block and this block is larger than + // The difference between the first block and this block is greater than or equal to // the retention period so any blocks after that are added as deletable. if i > 0 && blocks[0].Meta().MaxTime-block.Meta().MaxTime >= db.opts.RetentionDuration { for _, b := range blocks[i:] { diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 498e26c588..c7ea564d0e 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -689,10 +689,10 @@ func TestDB_BeyondTimeRetention(t *testing.T) { require.NoError(t, db.Close()) }() - // We have 4 blocks, 3 of which are beyond the retention duration. + // We have 4 blocks, 3 of which are beyond or at the retention duration. metas := []BlockMeta{ {MinTime: 300, MaxTime: 500}, - {MinTime: 200, MaxTime: 300}, + {MinTime: 200, MaxTime: 400}, {MinTime: 100, MaxTime: 200}, {MinTime: 0, MaxTime: 100}, } From af694dc29549aee7862893b9916df1560b293f66 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Wed, 20 Mar 2024 09:07:16 +0100 Subject: [PATCH 12/29] Merge TestDB_BeyondTimeRetention into TestTimeRetention Signed-off-by: Arve Knudsen --- tsdb/db_test.go | 40 ++++++++-------------------------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index c7ea564d0e..50d5141bd5 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -681,34 +681,6 @@ func TestDB_Snapshot(t *testing.T) { require.Equal(t, 1000.0, sum) } -func TestDB_BeyondTimeRetention(t *testing.T) { - opts := DefaultOptions() - opts.RetentionDuration = 100 - db := openTestDB(t, opts, nil) - defer func() { - require.NoError(t, db.Close()) - }() - - // We have 4 blocks, 3 of which are beyond or at the retention duration. - metas := []BlockMeta{ - {MinTime: 300, MaxTime: 500}, - {MinTime: 200, MaxTime: 400}, - {MinTime: 100, MaxTime: 200}, - {MinTime: 0, MaxTime: 100}, - } - - for _, m := range metas { - createBlock(t, db.Dir(), genSeries(1, 1, m.MinTime, m.MaxTime)) - } - - // Reloading should truncate the 3 blocks which are >= the retention period. - require.NoError(t, db.reloadBlocks()) - blocks := db.Blocks() - require.Len(t, blocks, 1) - require.Equal(t, metas[0].MinTime, blocks[0].Meta().MinTime) - require.Equal(t, metas[0].MaxTime, blocks[0].Meta().MaxTime) -} - // TestDB_Snapshot_ChunksOutsideOfCompactedRange ensures that a snapshot removes chunks samples // that are outside the set block time range. // See https://github.com/prometheus/prometheus/issues/5105 @@ -1497,7 +1469,8 @@ func TestTimeRetention(t *testing.T) { }() blocks := []*BlockMeta{ - {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 500, MaxTime: 1000}, // Coinciding exactly with the retention duration. {MinTime: 1000, MaxTime: 1500}, {MinTime: 1500, MaxTime: 2000}, // Newest Block } @@ -1509,14 +1482,17 @@ func TestTimeRetention(t *testing.T) { require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. require.Equal(t, len(blocks), len(db.Blocks())) // Ensure all blocks are registered. - db.opts.RetentionDuration = blocks[2].MaxTime - blocks[1].MinTime + // By setting retention duration as follows, we verify that also blocks[1], + // coinciding exactly with the boundary, is deleted. + db.opts.RetentionDuration = blocks[3].MaxTime - blocks[1].MaxTime + // Reloading should truncate the blocks which are >= the retention duration. require.NoError(t, db.reloadBlocks()) - expBlocks := blocks[1:] + expBlocks := blocks[2:] actBlocks := db.Blocks() require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") - require.Equal(t, len(expBlocks), len(actBlocks)) + require.Len(t, actBlocks, len(expBlocks)) require.Equal(t, expBlocks[0].MaxTime, actBlocks[0].meta.MaxTime) require.Equal(t, expBlocks[len(expBlocks)-1].MaxTime, actBlocks[len(actBlocks)-1].meta.MaxTime) } From 07332f7427ed9a1d6a04c124ae545eb3989134d6 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Wed, 20 Mar 2024 14:58:09 +0100 Subject: [PATCH 13/29] TestTimeRetention: Split into two sub-tests Signed-off-by: Arve Knudsen --- tsdb/db_test.go | 86 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 29 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 50d5141bd5..ce5efd0e43 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1463,38 +1463,66 @@ func (*mockCompactorFailing) CompactOOO(string, *OOOCompactionHead) (result []ul } func TestTimeRetention(t *testing.T) { - db := openTestDB(t, nil, []int64{1000}) - defer func() { - require.NoError(t, db.Close()) - }() - - blocks := []*BlockMeta{ - {MinTime: 500, MaxTime: 900}, // Oldest block - {MinTime: 500, MaxTime: 1000}, // Coinciding exactly with the retention duration. - {MinTime: 1000, MaxTime: 1500}, - {MinTime: 1500, MaxTime: 2000}, // Newest Block + testCases := []struct { + name string + blocks []*BlockMeta + expBlocks []*BlockMeta + retentionDuration int64 + }{ + { + name: "Block max time delta greater than retention duration", + blocks: []*BlockMeta{ + {MinTime: 500, MaxTime: 900}, // Oldest block, beyond retention + {MinTime: 1000, MaxTime: 1500}, + {MinTime: 1500, MaxTime: 2000}, // Newest block + }, + expBlocks: []*BlockMeta{ + {MinTime: 1000, MaxTime: 1500}, + {MinTime: 1500, MaxTime: 2000}, + }, + retentionDuration: 1000, + }, + { + name: "Block max time delta equal to retention duration", + blocks: []*BlockMeta{ + {MinTime: 500, MaxTime: 900}, // Oldest block + {MinTime: 1000, MaxTime: 1500}, // Coinciding exactly with the retention duration. + {MinTime: 1500, MaxTime: 2000}, // Newest block + }, + expBlocks: []*BlockMeta{ + {MinTime: 1500, MaxTime: 2000}, + }, + retentionDuration: 500, + }, } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + db := openTestDB(t, nil, []int64{1000}) + defer func() { + require.NoError(t, db.Close()) + }() - for _, m := range blocks { - createBlock(t, db.Dir(), genSeries(10, 10, m.MinTime, m.MaxTime)) + for _, m := range tc.blocks { + createBlock(t, db.Dir(), genSeries(10, 10, m.MinTime, m.MaxTime)) + } + + require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. + require.Len(t, db.Blocks(), len(tc.blocks)) // Ensure all blocks are registered. + + db.opts.RetentionDuration = tc.retentionDuration + // Reloading should truncate the blocks which are >= the retention duration vs the first block. + require.NoError(t, db.reloadBlocks()) + + actBlocks := db.Blocks() + + require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") + require.Len(t, actBlocks, len(tc.expBlocks)) + for i, eb := range tc.expBlocks { + require.Equal(t, eb.MinTime, actBlocks[i].meta.MinTime) + require.Equal(t, eb.MaxTime, actBlocks[i].meta.MaxTime) + } + }) } - - require.NoError(t, db.reloadBlocks()) // Reload the db to register the new blocks. - require.Equal(t, len(blocks), len(db.Blocks())) // Ensure all blocks are registered. - - // By setting retention duration as follows, we verify that also blocks[1], - // coinciding exactly with the boundary, is deleted. - db.opts.RetentionDuration = blocks[3].MaxTime - blocks[1].MaxTime - // Reloading should truncate the blocks which are >= the retention duration. - require.NoError(t, db.reloadBlocks()) - - expBlocks := blocks[2:] - actBlocks := db.Blocks() - - require.Equal(t, 1, int(prom_testutil.ToFloat64(db.metrics.timeRetentionCount)), "metric retention count mismatch") - require.Len(t, actBlocks, len(expBlocks)) - require.Equal(t, expBlocks[0].MaxTime, actBlocks[0].meta.MaxTime) - require.Equal(t, expBlocks[len(expBlocks)-1].MaxTime, actBlocks[len(actBlocks)-1].meta.MaxTime) } func TestRetentionDurationMetric(t *testing.T) { From 2a2e2ed28bc9f1f9205e874ed1be705e765ecb9b Mon Sep 17 00:00:00 2001 From: machine424 Date: Wed, 20 Mar 2024 14:09:21 +0100 Subject: [PATCH 14/29] chore(tsdb): set the wbl to nil as well in DBReadOnly.loadDataAsQueryable Signed-off-by: machine424 --- tsdb/db.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsdb/db.go b/tsdb/db.go index e49c5811d2..28cecde4f4 100644 --- a/tsdb/db.go +++ b/tsdb/db.go @@ -529,9 +529,10 @@ func (db *DBReadOnly) loadDataAsQueryable(maxt int64) (storage.SampleAndChunkQue if err := head.Init(maxBlockTime); err != nil { return nil, fmt.Errorf("read WAL: %w", err) } - // Set the wal to nil to disable all wal operations. + // Set the wal and the wbl to nil to disable related operations. // This is mainly to avoid blocking when closing the head. head.wal = nil + head.wbl = nil } db.closers = append(db.closers, head) From f6834c347ad64d2768b60b60f411597a467b4b64 Mon Sep 17 00:00:00 2001 From: tdakkota Date: Wed, 20 Mar 2024 22:01:05 +0300 Subject: [PATCH 15/29] promql: validate `label_join` destination label Signed-off-by: tdakkota --- promql/functions.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/promql/functions.go b/promql/functions.go index da66af2f02..d5840374e7 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1386,6 +1386,9 @@ func (ev *evaluator) evalLabelJoin(args parser.Expressions) (parser.Value, annot } srcLabels[i-3] = src } + if !model.LabelName(dst).IsValid() { + panic(fmt.Errorf("invalid destination label name in label_join(): %s", dst)) + } val, ws := ev.eval(args[0]) matrix := val.(Matrix) From 7ec4a11472648e994b1ebe4dfe80497d43a05455 Mon Sep 17 00:00:00 2001 From: David Leadbeater Date: Thu, 21 Mar 2024 20:23:40 +1100 Subject: [PATCH 16/29] promtool: Avoid using testify for user rule tests Using testify outside of unit tests results in panics rather than a useful error for the user. Fixes #13703 Signed-off-by: David Leadbeater --- cmd/promtool/unittest.go | 11 ++++++++--- promql/test.go | 29 +++++++++++++++++------------ promql/test_test.go | 2 +- util/teststorage/storage.go | 23 +++++++++++++++++++---- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/cmd/promtool/unittest.go b/cmd/promtool/unittest.go index 4777b88098..6d6683a934 100644 --- a/cmd/promtool/unittest.go +++ b/cmd/promtool/unittest.go @@ -175,13 +175,18 @@ type testGroup struct { } // test performs the unit tests. -func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promql.LazyLoaderOpts, diffFlag bool, ruleFiles ...string) []error { +func (tg *testGroup) test(evalInterval time.Duration, groupOrderMap map[string]int, queryOpts promql.LazyLoaderOpts, diffFlag bool, ruleFiles ...string) (outErr []error) { // Setup testing suite. - suite, err := promql.NewLazyLoader(nil, tg.seriesLoadingString(), queryOpts) + suite, err := promql.NewLazyLoader(tg.seriesLoadingString(), queryOpts) if err != nil { return []error{err} } - defer suite.Close() + defer func() { + err := suite.Close() + if err != nil { + outErr = append(outErr, err) + } + }() suite.SubqueryInterval = evalInterval // Load the rule files. diff --git a/promql/test.go b/promql/test.go index 589b1e5b6b..c45cfd8a51 100644 --- a/promql/test.go +++ b/promql/test.go @@ -704,8 +704,6 @@ func parseNumber(s string) (float64, error) { // LazyLoader lazily loads samples into storage. // This is specifically implemented for unit testing of rules. type LazyLoader struct { - testutil.T - loadCmd *loadCmd storage storage.Storage @@ -727,13 +725,15 @@ type LazyLoaderOpts struct { } // NewLazyLoader returns an initialized empty LazyLoader. -func NewLazyLoader(t testutil.T, input string, opts LazyLoaderOpts) (*LazyLoader, error) { +func NewLazyLoader(input string, opts LazyLoaderOpts) (*LazyLoader, error) { ll := &LazyLoader{ - T: t, opts: opts, } err := ll.parse(input) - ll.clear() + if err != nil { + return nil, err + } + err = ll.clear() return ll, err } @@ -761,15 +761,20 @@ func (ll *LazyLoader) parse(input string) error { } // clear the current test storage of all inserted samples. -func (ll *LazyLoader) clear() { +func (ll *LazyLoader) clear() error { if ll.storage != nil { - err := ll.storage.Close() - require.NoError(ll.T, err, "Unexpected error while closing test storage.") + if err := ll.storage.Close(); err != nil { + return fmt.Errorf("closing test storage: %w", err) + } } if ll.cancelCtx != nil { ll.cancelCtx() } - ll.storage = teststorage.New(ll) + var err error + ll.storage, err = teststorage.NewWithError() + if err != nil { + return err + } opts := EngineOpts{ Logger: nil, @@ -783,6 +788,7 @@ func (ll *LazyLoader) clear() { ll.queryEngine = NewEngine(opts) ll.context, ll.cancelCtx = context.WithCancel(context.Background()) + return nil } // appendTill appends the defined time series to the storage till the given timestamp (in milliseconds). @@ -836,8 +842,7 @@ func (ll *LazyLoader) Storage() storage.Storage { } // Close closes resources associated with the LazyLoader. -func (ll *LazyLoader) Close() { +func (ll *LazyLoader) Close() error { ll.cancelCtx() - err := ll.storage.Close() - require.NoError(ll.T, err, "Unexpected error while closing test storage.") + return ll.storage.Close() } diff --git a/promql/test_test.go b/promql/test_test.go index ee2a0e264b..316c177d77 100644 --- a/promql/test_test.go +++ b/promql/test_test.go @@ -110,7 +110,7 @@ func TestLazyLoader_WithSamplesTill(t *testing.T) { } for _, c := range cases { - suite, err := NewLazyLoader(t, c.loadString, LazyLoaderOpts{}) + suite, err := NewLazyLoader(c.loadString, LazyLoaderOpts{}) require.NoError(t, err) defer suite.Close() diff --git a/util/teststorage/storage.go b/util/teststorage/storage.go index 5d95437e99..7d1f9dda24 100644 --- a/util/teststorage/storage.go +++ b/util/teststorage/storage.go @@ -14,6 +14,7 @@ package teststorage import ( + "fmt" "os" "time" @@ -30,8 +31,18 @@ import ( // New returns a new TestStorage for testing purposes // that removes all associated files on closing. func New(t testutil.T) *TestStorage { + stor, err := NewWithError() + require.NoError(t, err) + return stor +} + +// NewWithError returns a new TestStorage for user facing tests, which reports +// errors directly. +func NewWithError() (*TestStorage, error) { dir, err := os.MkdirTemp("", "test_storage") - require.NoError(t, err, "unexpected error while opening test directory") + if err != nil { + return nil, fmt.Errorf("opening test directory: %w", err) + } // Tests just load data for a series sequentially. Thus we // need a long appendable window. @@ -41,13 +52,17 @@ func New(t testutil.T) *TestStorage { opts.RetentionDuration = 0 opts.EnableNativeHistograms = true db, err := tsdb.Open(dir, nil, nil, opts, tsdb.NewDBStats()) - require.NoError(t, err, "unexpected error while opening test storage") + if err != nil { + return nil, fmt.Errorf("opening test storage: %w", err) + } reg := prometheus.NewRegistry() eMetrics := tsdb.NewExemplarMetrics(reg) es, err := tsdb.NewCircularExemplarStorage(10, eMetrics) - require.NoError(t, err, "unexpected error while opening test exemplar storage") - return &TestStorage{DB: db, exemplarStorage: es, dir: dir} + if err != nil { + return nil, fmt.Errorf("opening test exemplar storage: %w", err) + } + return &TestStorage{DB: db, exemplarStorage: es, dir: dir}, nil } type TestStorage struct { From 9d32754bc0456f3026be8cb8ad7009e776b3d1a6 Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 17/29] add unit tests with all negative values for histogram_stddev and var Signed-off-by: Jeanette Tan --- promql/engine_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/promql/engine_test.go b/promql/engine_test.go index cc5d0ee780..3f5f2dc132 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3498,6 +3498,38 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, stdVar: 1544.8582535368798, // actual variance: 1738.4082 }, + { + name: "-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3", + h: &histogram.Histogram{ + Count: 10, + ZeroCount: 0, + Sum: -112946, + Schema: 0, + NegativeSpans: []histogram.Span{ + {Offset: 2, Length: 3}, + {Offset: 1, Length: 2}, + {Offset: 2, Length: 1}, + {Offset: 3, Length: 1}, + {Offset: 2, Length: 1}, + }, + NegativeBuckets: []int64{1, 0, 0, 0, 0, 2, -2, 0}, + }, + stdVar: 1240930974.5260057, // actual variance: 882690990 + }, + { + name: "-10 x10", + h: &histogram.Histogram{ + Count: 10, + ZeroCount: 0, + Sum: -100, + Schema: 0, + NegativeSpans: []histogram.Span{ + {Offset: 4, Length: 1}, + }, + NegativeBuckets: []int64{10}, + }, + stdVar: 454.2741699796952, // actual variance: 0 + }, { name: "-50, -8, 0, 3, 8, 9, 100, NaN", h: &histogram.Histogram{ From fab629855027c63e3cc746fa91ce8628c390efd6 Mon Sep 17 00:00:00 2001 From: deterclosed Date: Sat, 23 Mar 2024 13:39:27 +0800 Subject: [PATCH 18/29] chore: remove repetitive word Signed-off-by: deterclosed --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57055ef38c..7687826ba4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -90,7 +90,7 @@ can modify the `./promql/parser/generated_parser.y.go` manually. ```golang // As of writing this was somewhere around line 600. var ( - yyDebug = 0 // This can be be a number 0 -> 5. + yyDebug = 0 // This can be a number 0 -> 5. yyErrorVerbose = false // This can be set to true. ) From 44dcf02c697eba456bfeb28cd6587ee5b834d46f Mon Sep 17 00:00:00 2001 From: Artur Melanchyk Date: Sat, 23 Mar 2024 19:44:30 +0100 Subject: [PATCH 19/29] TSDB: make total lock-free by using atomic Signed-off-by: Artur Melanchyk --- cmd/promtool/tsdb.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/promtool/tsdb.go b/cmd/promtool/tsdb.go index 73258754e2..b786c92976 100644 --- a/cmd/promtool/tsdb.go +++ b/cmd/promtool/tsdb.go @@ -33,6 +33,7 @@ import ( "github.com/alecthomas/units" "github.com/go-kit/log" + "go.uber.org/atomic" "github.com/prometheus/prometheus/model/labels" "github.com/prometheus/prometheus/promql/parser" @@ -149,8 +150,7 @@ func benchmarkWrite(outPath, samplesFile string, numMetrics, numScrapes int) err } func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (uint64, error) { - var mu sync.Mutex - var total uint64 + var total atomic.Uint64 for i := 0; i < scrapeCount; i += 100 { var wg sync.WaitGroup @@ -165,22 +165,21 @@ func (b *writeBenchmark) ingestScrapes(lbls []labels.Labels, scrapeCount int) (u wg.Add(1) go func() { + defer wg.Done() + n, err := b.ingestScrapesShard(batch, 100, int64(timeDelta*i)) if err != nil { // exitWithError(err) fmt.Println(" err", err) } - mu.Lock() - total += n - mu.Unlock() - wg.Done() + total.Add(n) }() } wg.Wait() } fmt.Println("ingestion completed") - return total, nil + return total.Load(), nil } func (b *writeBenchmark) ingestScrapesShard(lbls []labels.Labels, scrapeCount int, baset int64) (uint64, error) { From ff15b17400dc579bdb7b9d9ebcea6465453a64b2 Mon Sep 17 00:00:00 2001 From: sellskin Date: Mon, 25 Mar 2024 12:18:33 +0800 Subject: [PATCH 20/29] remove code that will not be executed Signed-off-by: sellskin --- discovery/file/file_test.go | 1 - discovery/legacymanager/manager_test.go | 1 - discovery/manager_test.go | 1 - 3 files changed, 3 deletions(-) diff --git a/discovery/file/file_test.go b/discovery/file/file_test.go index 521a3c0f16..179ac5cd1c 100644 --- a/discovery/file/file_test.go +++ b/discovery/file/file_test.go @@ -208,7 +208,6 @@ func (t *testRunner) requireUpdate(ref time.Time, expected []*targetgroup.Group) select { case <-timeout: t.Fatalf("Expected update but got none") - return case <-time.After(defaultWait / 10): if ref.Equal(t.lastReceive()) { // No update received. diff --git a/discovery/legacymanager/manager_test.go b/discovery/legacymanager/manager_test.go index 6fbecabc2a..1ed699645d 100644 --- a/discovery/legacymanager/manager_test.go +++ b/discovery/legacymanager/manager_test.go @@ -733,7 +733,6 @@ func verifyPresence(t *testing.T, tSets map[poolKey]map[string]*targetgroup.Grou t.Helper() if _, ok := tSets[poolKey]; !ok { t.Fatalf("'%s' should be present in Pool keys: %v", poolKey, tSets) - return } match := false diff --git a/discovery/manager_test.go b/discovery/manager_test.go index 52159d94f6..8f2345911e 100644 --- a/discovery/manager_test.go +++ b/discovery/manager_test.go @@ -733,7 +733,6 @@ func verifySyncedPresence(t *testing.T, tGroups map[string][]*targetgroup.Group, t.Helper() if _, ok := tGroups[key]; !ok { t.Fatalf("'%s' should be present in Group map keys: %v", key, tGroups) - return } match := false var mergedTargets string From 48786ad4e8b644c31a0ce21f5275bb2dcba4a85f Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Mon, 25 Mar 2024 12:20:18 +0000 Subject: [PATCH 21/29] Use slices insteda of exp/slices Signed-off-by: Bryan Boreham --- go.mod | 2 +- model/labels/regexp.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 499104570d..5ea1124fa7 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,6 @@ require ( go.uber.org/automaxprocs v1.5.3 go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/net v0.22.0 golang.org/x/oauth2 v0.18.0 golang.org/x/sync v0.6.0 @@ -186,6 +185,7 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/proto/otlp v1.1.0 // indirect golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/mod v0.16.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/model/labels/regexp.go b/model/labels/regexp.go index 5e470afa3b..f35dc76f60 100644 --- a/model/labels/regexp.go +++ b/model/labels/regexp.go @@ -14,11 +14,11 @@ package labels import ( + "slices" "strings" "github.com/grafana/regexp" "github.com/grafana/regexp/syntax" - "golang.org/x/exp/slices" ) const ( From ceca6c4716e275d39bd28798bd6ac0ec998938e7 Mon Sep 17 00:00:00 2001 From: Ben Ye Date: Tue, 26 Mar 2024 04:16:27 -0700 Subject: [PATCH 22/29] [ENHANCEMENT] TSDB: Log more statistics during startup (#13838) * log chunk snapshot and mmap chunks replay duration together with total replay duration Signed-off-by: Ben Ye --- tsdb/head.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tsdb/head.go b/tsdb/head.go index 7e6fda9c7e..820fba9e2c 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -620,6 +620,7 @@ func (h *Head) Init(minValidTime int64) error { refSeries := make(map[chunks.HeadSeriesRef]*memSeries) snapshotLoaded := false + var chunkSnapshotLoadDuration time.Duration if h.opts.EnableMemorySnapshotOnShutdown { level.Info(h.logger).Log("msg", "Chunk snapshot is enabled, replaying from the snapshot") // If there are any WAL files, there should be at least one WAL file with an index that is current or newer @@ -650,7 +651,8 @@ func (h *Head) Init(minValidTime int64) error { snapIdx, snapOffset, refSeries, err = h.loadChunkSnapshot() if err == nil { snapshotLoaded = true - level.Info(h.logger).Log("msg", "Chunk snapshot loading time", "duration", time.Since(start).String()) + chunkSnapshotLoadDuration = time.Since(start) + level.Info(h.logger).Log("msg", "Chunk snapshot loading time", "duration", chunkSnapshotLoadDuration.String()) } if err != nil { snapIdx, snapOffset = -1, 0 @@ -672,6 +674,8 @@ func (h *Head) Init(minValidTime int64) error { oooMmappedChunks map[chunks.HeadSeriesRef][]*mmappedChunk lastMmapRef chunks.ChunkDiskMapperRef err error + + mmapChunkReplayDuration time.Duration ) if snapshotLoaded || h.wal != nil { // If snapshot was not loaded and if there is no WAL, then m-map chunks will be discarded @@ -695,7 +699,8 @@ func (h *Head) Init(minValidTime int64) error { return err } } - level.Info(h.logger).Log("msg", "On-disk memory mappable chunks replay completed", "duration", time.Since(mmapChunkReplayStart).String()) + mmapChunkReplayDuration = time.Since(mmapChunkReplayStart) + level.Info(h.logger).Log("msg", "On-disk memory mappable chunks replay completed", "duration", mmapChunkReplayDuration.String()) } if h.wal == nil { @@ -817,6 +822,8 @@ func (h *Head) Init(minValidTime int64) error { "checkpoint_replay_duration", checkpointReplayDuration.String(), "wal_replay_duration", walReplayDuration.String(), "wbl_replay_duration", wblReplayDuration.String(), + "chunk_snapshot_load_duration", chunkSnapshotLoadDuration.String(), + "mmap_chunk_replay_duration", mmapChunkReplayDuration.String(), "total_replay_duration", totalReplayDuration.String(), ) From 481f14e1c0eabd57c5570b1b3f87aa9d8d3b9a81 Mon Sep 17 00:00:00 2001 From: Nick Pillitteri <56quarters@users.noreply.github.com> Date: Tue, 26 Mar 2024 07:17:38 -0400 Subject: [PATCH 23/29] TSDB: Don't rely on integer overflow in head compaction check (#13755) * TSDB: Don't compact the head block when empty Don't compact the Head block if there have not yet been any samples appended. Previously, the logic for determining if the head should be compacted relied on the default values for min and max time and integer overflow when they were checked in `Head.compactable()`. The check in `Head.compactable()` effectively did `math.MinInt64 - math.MaxInt64` which overflowed and wrapped to `1`. Since `1` is less than `1.5` times the chunk range, compaction did not happen. This was the correct behavior but relying on overflow wrapping is surprising. This change add a method for checking if the min and max time for the head is unset and uses it to short-circuit compaction in that case. It also replaces several explicit checks for the default value to determine if the head has not yet had any samples added. Signed-off-by: Nick Pillitteri --- tsdb/db_test.go | 5 +++++ tsdb/head.go | 21 +++++++++++++++------ tsdb/head_append.go | 4 ++-- tsdb/head_test.go | 13 +++++++++++++ 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/tsdb/db_test.go b/tsdb/db_test.go index 498e26c588..e98f60299e 100644 --- a/tsdb/db_test.go +++ b/tsdb/db_test.go @@ -1994,6 +1994,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { // Should be set to init values if no WAL or blocks exist so far. require.Equal(t, int64(math.MaxInt64), db.head.MinTime()) require.Equal(t, int64(math.MinInt64), db.head.MaxTime()) + require.False(t, db.head.initialized()) // First added sample initializes the writable range. ctx := context.Background() @@ -2003,6 +2004,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(1000), db.head.MinTime()) require.Equal(t, int64(1000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("wal-only", func(t *testing.T) { dir := t.TempDir() @@ -2031,6 +2033,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(5000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block", func(t *testing.T) { dir := t.TempDir() @@ -2043,6 +2046,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(2000), db.head.MinTime()) require.Equal(t, int64(2000), db.head.MaxTime()) + require.True(t, db.head.initialized()) }) t.Run("existing-block-and-wal", func(t *testing.T) { dir := t.TempDir() @@ -2075,6 +2079,7 @@ func TestInitializeHeadTimestamp(t *testing.T) { require.Equal(t, int64(6000), db.head.MinTime()) require.Equal(t, int64(15000), db.head.MaxTime()) + require.True(t, db.head.initialized()) // Check that old series has been GCed. require.Equal(t, 1.0, prom_testutil.ToFloat64(db.head.metrics.series)) }) diff --git a/tsdb/head.go b/tsdb/head.go index 820fba9e2c..8b3d9787ca 100644 --- a/tsdb/head.go +++ b/tsdb/head.go @@ -1081,11 +1081,11 @@ func (h *Head) SetMinValidTime(minValidTime int64) { // Truncate removes old data before mint from the head and WAL. func (h *Head) Truncate(mint int64) (err error) { - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() if err := h.truncateMemory(mint); err != nil { return err } - if initialize { + if !initialized { return nil } return h.truncateWAL(mint) @@ -1107,9 +1107,9 @@ func (h *Head) truncateMemory(mint int64) (err error) { } }() - initialize := h.MinTime() == math.MaxInt64 + initialized := h.initialized() - if h.MinTime() >= mint && !initialize { + if h.MinTime() >= mint && initialized { return nil } @@ -1120,7 +1120,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { defer h.memTruncationInProcess.Store(false) // We wait for pending queries to end that overlap with this truncation. - if !initialize { + if initialized { h.WaitForPendingReadersInTimeRange(h.MinTime(), mint) } @@ -1134,7 +1134,7 @@ func (h *Head) truncateMemory(mint int64) (err error) { // This was an initial call to Truncate after loading blocks on startup. // We haven't read back the WAL yet, so do not attempt to truncate it. - if initialize { + if !initialized { return nil } @@ -1622,10 +1622,19 @@ func (h *Head) MaxOOOTime() int64 { return h.maxOOOTime.Load() } +// initialized returns true if the head has a MinTime set, false otherwise. +func (h *Head) initialized() bool { + return h.MinTime() != math.MaxInt64 +} + // compactable returns whether the head has a compactable range. // The head has a compactable range when the head time range is 1.5 times the chunk range. // The 0.5 acts as a buffer of the appendable window. func (h *Head) compactable() bool { + if !h.initialized() { + return false + } + return h.MaxTime()-h.MinTime() > h.chunkRange.Load()/2*3 } diff --git a/tsdb/head_append.go b/tsdb/head_append.go index 6342a19d46..58d24ceb94 100644 --- a/tsdb/head_append.go +++ b/tsdb/head_append.go @@ -138,7 +138,7 @@ func (h *Head) Appender(_ context.Context) storage.Appender { // The head cache might not have a starting point yet. The init appender // picks up the first appended timestamp as the base. - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return &initAppender{ head: h, } @@ -191,7 +191,7 @@ func (h *Head) appendableMinValidTime() int64 { // AppendableMinValidTime returns the minimum valid time for samples to be appended to the Head. // Returns false if Head hasn't been initialized yet and the minimum time isn't known yet. func (h *Head) AppendableMinValidTime() (int64, bool) { - if h.MinTime() == math.MaxInt64 { + if !h.initialized() { return 0, false } diff --git a/tsdb/head_test.go b/tsdb/head_test.go index 41c2e062f2..750c8a11e5 100644 --- a/tsdb/head_test.go +++ b/tsdb/head_test.go @@ -5819,3 +5819,16 @@ func TestHeadAppender_AppendCTZeroSample(t *testing.T) { require.Equal(t, chunkenc.ValNone, it.Next()) } } + +func TestHeadCompactableDoesNotCompactEmptyHead(t *testing.T) { + // Use a chunk range of 1 here so that if we attempted to determine if the head + // was compactable using default values for min and max times, `Head.compactable()` + // would return true which is incorrect. This test verifies that we short-circuit + // the check when the head has not yet had any samples added. + head, _ := newTestHead(t, 1, wlog.CompressionNone, false) + defer func() { + require.NoError(t, head.Close()) + }() + + require.False(t, head.compactable()) +} From 5cc97a1820773869bf898b8c2533a4d3bf5d9aba Mon Sep 17 00:00:00 2001 From: Charles Korn Date: Tue, 26 Mar 2024 22:22:22 +1100 Subject: [PATCH 24/29] [tests]: extend test scripting language to support range queries (#13825) * Extract method to make it easier to test. Signed-off-by: Charles Korn * Remove superfluous interface definition. Signed-off-by: Charles Korn * Add test cases for existing instant query functionality. Signed-off-by: Charles Korn * Add support for testing range queries Signed-off-by: Charles Korn * Expand test coverage for instant queries and clarify error when a float is returned but a histogram is expected (or vice versa) Signed-off-by: Charles Korn * Improve error message formatting Signed-off-by: Charles Korn * Add test case for instant query command with invalid timestamp Signed-off-by: Charles Korn * Fix linting warning. Signed-off-by: Charles Korn * Remove superfluous print statement and expected result Signed-off-by: Charles Korn * Fix linting warning. Signed-off-by: Charles Korn * Add note about ordered range eval commands. Signed-off-by: Charles Korn * Check that matrix results are always sorted by labels. Signed-off-by: Charles Korn --------- Signed-off-by: Charles Korn --- promql/test.go | 431 +++++++++++++++++++++++++++++++++----------- promql/test_test.go | 351 ++++++++++++++++++++++++++++++++++++ 2 files changed, 681 insertions(+), 101 deletions(-) diff --git a/promql/test.go b/promql/test.go index c45cfd8a51..296b3d3cad 100644 --- a/promql/test.go +++ b/promql/test.go @@ -46,6 +46,7 @@ var ( patSpace = regexp.MustCompile("[\t ]+") patLoad = regexp.MustCompile(`^load\s+(.+?)$`) patEvalInstant = regexp.MustCompile(`^eval(?:_(fail|ordered))?\s+instant\s+(?:at\s+(.+?))?\s+(.+)$`) + patEvalRange = regexp.MustCompile(`^eval(?:_(fail))?\s+range\s+from\s+(.+)\s+to\s+(.+)\s+step\s+(.+?)\s+(.+)$`) ) const ( @@ -72,7 +73,7 @@ func LoadedStorage(t testutil.T, input string) *teststorage.TestStorage { } // RunBuiltinTests runs an acceptance test suite against the provided engine. -func RunBuiltinTests(t *testing.T, engine engineQuerier) { +func RunBuiltinTests(t *testing.T, engine QueryEngine) { t.Cleanup(func() { parser.EnableExperimentalFunctions = false }) parser.EnableExperimentalFunctions = true @@ -89,11 +90,19 @@ func RunBuiltinTests(t *testing.T, engine engineQuerier) { } // RunTest parses and runs the test against the provided engine. -func RunTest(t testutil.T, input string, engine engineQuerier) { - test, err := newTest(t, input) - require.NoError(t, err) +func RunTest(t testutil.T, input string, engine QueryEngine) { + require.NoError(t, runTest(t, input, engine)) +} +func runTest(t testutil.T, input string, engine QueryEngine) error { + test, err := newTest(t, input) + + // Why do this before checking err? newTest() can create the test storage and then return an error, + // and we want to make sure to clean that up to avoid leaking goroutines. defer func() { + if test == nil { + return + } if test.storage != nil { test.storage.Close() } @@ -102,11 +111,19 @@ func RunTest(t testutil.T, input string, engine engineQuerier) { } }() - for _, cmd := range test.cmds { - // TODO(fabxc): aggregate command errors, yield diffs for result - // comparison errors. - require.NoError(t, test.exec(cmd, engine)) + if err != nil { + return err } + + for _, cmd := range test.cmds { + if err := test.exec(cmd, engine); err != nil { + // TODO(fabxc): aggregate command errors, yield diffs for result + // comparison errors. + return err + } + } + + return nil } // test is a sequence of read and write commands that are run @@ -137,11 +154,6 @@ func newTest(t testutil.T, input string) (*test, error) { //go:embed testdata var testsFs embed.FS -type engineQuerier interface { - NewRangeQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, start, end time.Time, interval time.Duration) (Query, error) - NewInstantQuery(ctx context.Context, q storage.Queryable, opts QueryOpts, qs string, ts time.Time) (Query, error) -} - func raise(line int, format string, v ...interface{}) error { return &parser.ParseErr{ LineOffset: line, @@ -188,15 +200,26 @@ func parseSeries(defLine string, line int) (labels.Labels, []parser.SequenceValu } func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { - if !patEvalInstant.MatchString(lines[i]) { - return i, nil, raise(i, "invalid evaluation command. (eval[_fail|_ordered] instant [at ] ") + instantParts := patEvalInstant.FindStringSubmatch(lines[i]) + rangeParts := patEvalRange.FindStringSubmatch(lines[i]) + + if instantParts == nil && rangeParts == nil { + return i, nil, raise(i, "invalid evaluation command. Must be either 'eval[_fail|_ordered] instant [at ] ' or 'eval[_fail] range from to step '") } - parts := patEvalInstant.FindStringSubmatch(lines[i]) - var ( - mod = parts[1] - at = parts[2] - expr = parts[3] - ) + + isInstant := instantParts != nil + + var mod string + var expr string + + if isInstant { + mod = instantParts[1] + expr = instantParts[3] + } else { + mod = rangeParts[1] + expr = rangeParts[5] + } + _, err := parser.ParseExpr(expr) if err != nil { parser.EnrichParseError(err, func(parseErr *parser.ParseErr) { @@ -209,15 +232,54 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { return i, nil, err } - offset, err := model.ParseDuration(at) - if err != nil { - return i, nil, raise(i, "invalid step definition %q: %s", parts[1], err) - } - ts := testStartTime.Add(time.Duration(offset)) + formatErr := func(format string, args ...any) error { + combinedArgs := []any{expr, i + 1} + + combinedArgs = append(combinedArgs, args...) + return fmt.Errorf("error in eval %s (line %v): "+format, combinedArgs...) + } + + var cmd *evalCmd + + if isInstant { + at := instantParts[2] + offset, err := model.ParseDuration(at) + if err != nil { + return i, nil, formatErr("invalid timestamp definition %q: %s", at, err) + } + ts := testStartTime.Add(time.Duration(offset)) + cmd = newInstantEvalCmd(expr, ts, i+1) + } else { + from := rangeParts[2] + to := rangeParts[3] + step := rangeParts[4] + + parsedFrom, err := model.ParseDuration(from) + if err != nil { + return i, nil, formatErr("invalid start timestamp definition %q: %s", from, err) + } + + parsedTo, err := model.ParseDuration(to) + if err != nil { + return i, nil, formatErr("invalid end timestamp definition %q: %s", to, err) + } + + if parsedTo < parsedFrom { + return i, nil, formatErr("invalid test definition, end timestamp (%s) is before start timestamp (%s)", to, from) + } + + parsedStep, err := model.ParseDuration(step) + if err != nil { + return i, nil, formatErr("invalid step definition %q: %s", step, err) + } + + cmd = newRangeEvalCmd(expr, testStartTime.Add(time.Duration(parsedFrom)), testStartTime.Add(time.Duration(parsedTo)), time.Duration(parsedStep), i+1) + } - cmd := newEvalCmd(expr, ts, i+1) switch mod { case "ordered": + // Ordered results are not supported for range queries, but the regex for range query commands does not allow + // asserting an ordered result, so we don't need to do any error checking here. cmd.ordered = true case "fail": cmd.fail = true @@ -240,8 +302,8 @@ func (t *test) parseEval(lines []string, i int) (int, *evalCmd, error) { } // Currently, we are not expecting any matrices. - if len(vals) > 1 { - return i, nil, raise(i, "expecting multiple values in instant evaluation not allowed") + if len(vals) > 1 && isInstant { + return i, nil, formatErr("expecting multiple values in instant evaluation not allowed") } cmd.expectMetric(j, metric, vals...) } @@ -375,8 +437,11 @@ func appendSample(a storage.Appender, s Sample, m labels.Labels) error { type evalCmd struct { expr string start time.Time + end time.Time + step time.Duration line int + isRange bool // if false, instant query fail, ordered bool metrics map[uint64]labels.Labels @@ -392,7 +457,7 @@ func (e entry) String() string { return fmt.Sprintf("%d: %s", e.pos, e.vals) } -func newEvalCmd(expr string, start time.Time, line int) *evalCmd { +func newInstantEvalCmd(expr string, start time.Time, line int) *evalCmd { return &evalCmd{ expr: expr, start: start, @@ -403,6 +468,20 @@ func newEvalCmd(expr string, start time.Time, line int) *evalCmd { } } +func newRangeEvalCmd(expr string, start, end time.Time, step time.Duration, line int) *evalCmd { + return &evalCmd{ + expr: expr, + start: start, + end: end, + step: step, + line: line, + isRange: true, + + metrics: map[uint64]labels.Labels{}, + expected: map[uint64]entry{}, + } +} + func (ev *evalCmd) String() string { return "eval" } @@ -425,7 +504,77 @@ func (ev *evalCmd) expectMetric(pos int, m labels.Labels, vals ...parser.Sequenc func (ev *evalCmd) compareResult(result parser.Value) error { switch val := result.(type) { case Matrix: - return errors.New("received range result on instant evaluation") + if ev.ordered { + return fmt.Errorf("expected ordered result, but query returned a matrix") + } + + if err := assertMatrixSorted(val); err != nil { + return err + } + + seen := map[uint64]bool{} + for _, s := range val { + hash := s.Metric.Hash() + if _, ok := ev.metrics[hash]; !ok { + return fmt.Errorf("unexpected metric %s in result", s.Metric) + } + seen[hash] = true + exp := ev.expected[hash] + + var expectedFloats []FPoint + var expectedHistograms []HPoint + + for i, e := range exp.vals { + ts := ev.start.Add(time.Duration(i) * ev.step) + + if ts.After(ev.end) { + return fmt.Errorf("expected %v points for %s, but query time range cannot return this many points", len(exp.vals), ev.metrics[hash]) + } + + t := ts.UnixNano() / int64(time.Millisecond/time.Nanosecond) + + if e.Histogram != nil { + expectedHistograms = append(expectedHistograms, HPoint{T: t, H: e.Histogram}) + } else if !e.Omitted { + expectedFloats = append(expectedFloats, FPoint{T: t, F: e.Value}) + } + } + + if len(expectedFloats) != len(s.Floats) || len(expectedHistograms) != len(s.Histograms) { + return fmt.Errorf("expected %v float points and %v histogram points for %s, but got %s", len(expectedFloats), len(expectedHistograms), ev.metrics[hash], formatSeriesResult(s)) + } + + for i, expected := range expectedFloats { + actual := s.Floats[i] + + if expected.T != actual.T { + return fmt.Errorf("expected float value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) + } + + if !almostEqual(actual.F, expected.F, defaultEpsilon) { + return fmt.Errorf("expected float value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.F, actual.F, formatSeriesResult(s)) + } + } + + for i, expected := range expectedHistograms { + actual := s.Histograms[i] + + if expected.T != actual.T { + return fmt.Errorf("expected histogram value at index %v for %s to have timestamp %v, but it had timestamp %v (result has %s)", i, ev.metrics[hash], expected.T, actual.T, formatSeriesResult(s)) + } + + if !actual.H.Equals(expected.H) { + return fmt.Errorf("expected histogram value at index %v (t=%v) for %s to be %v, but got %v (result has %s)", i, actual.T, ev.metrics[hash], expected.H, actual.H, formatSeriesResult(s)) + } + } + + } + + for hash := range ev.expected { + if !seen[hash] { + return fmt.Errorf("expected metric %s not found", ev.metrics[hash]) + } + } case Vector: seen := map[uint64]bool{} @@ -440,7 +589,13 @@ func (ev *evalCmd) compareResult(result parser.Value) error { } exp0 := exp.vals[0] expH := exp0.Histogram - if (expH == nil) != (v.H == nil) || (expH != nil && !expH.Equals(v.H)) { + if expH == nil && v.H != nil { + return fmt.Errorf("expected float value %v for %s but got histogram %s", exp0, v.Metric, HistogramTestExpression(v.H)) + } + if expH != nil && v.H == nil { + return fmt.Errorf("expected histogram %s for %s but got float value %v", HistogramTestExpression(expH), v.Metric, v.F) + } + if expH != nil && !expH.Equals(v.H) { return fmt.Errorf("expected %v for %s but got %s", HistogramTestExpression(expH), v.Metric, HistogramTestExpression(v.H)) } if !almostEqual(exp0.Value, v.F, defaultEpsilon) { @@ -477,6 +632,21 @@ func (ev *evalCmd) compareResult(result parser.Value) error { return nil } +func formatSeriesResult(s Series) string { + floatPlural := "s" + histogramPlural := "s" + + if len(s.Floats) == 1 { + floatPlural = "" + } + + if len(s.Histograms) == 1 { + histogramPlural = "" + } + + return fmt.Sprintf("%v float point%s %v and %v histogram point%s %v", len(s.Floats), floatPlural, s.Floats, len(s.Histograms), histogramPlural, s.Histograms) +} + // HistogramTestExpression returns TestExpression() for the given histogram or "" if the histogram is nil. func HistogramTestExpression(h *histogram.FloatHistogram) string { if h != nil { @@ -561,7 +731,7 @@ func atModifierTestCases(exprStr string, evalTime time.Time) ([]atModifierTestCa } // exec processes a single step of the test. -func (t *test) exec(tc testCommand, engine engineQuerier) error { +func (t *test) exec(tc testCommand, engine QueryEngine) error { switch cmd := tc.(type) { case *clearCmd: t.clear() @@ -578,74 +748,7 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error { } case *evalCmd: - queries, err := atModifierTestCases(cmd.expr, cmd.start) - if err != nil { - return err - } - queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...) - for _, iq := range queries { - q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime) - if err != nil { - return err - } - defer q.Close() - res := q.Exec(t.context) - if res.Err != nil { - if cmd.fail { - continue - } - return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err) - } - if res.Err == nil && cmd.fail { - return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line) - } - err = cmd.compareResult(res.Value) - if err != nil { - return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err) - } - - // Check query returns same result in range mode, - // by checking against the middle step. - q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute) - if err != nil { - return err - } - rangeRes := q.Exec(t.context) - if rangeRes.Err != nil { - return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err) - } - defer q.Close() - if cmd.ordered { - // Ordering isn't defined for range queries. - continue - } - mat := rangeRes.Value.(Matrix) - vec := make(Vector, 0, len(mat)) - for _, series := range mat { - // We expect either Floats or Histograms. - for _, point := range series.Floats { - if point.T == timeMilliseconds(iq.evalTime) { - vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F}) - break - } - } - for _, point := range series.Histograms { - if point.T == timeMilliseconds(iq.evalTime) { - vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H}) - break - } - } - } - if _, ok := res.Value.(Scalar); ok { - err = cmd.compareResult(Scalar{V: vec[0].F}) - } else { - err = cmd.compareResult(vec) - } - if err != nil { - return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err) - } - - } + return t.execEval(cmd, engine) default: panic("promql.Test.exec: unknown test command type") @@ -653,6 +756,132 @@ func (t *test) exec(tc testCommand, engine engineQuerier) error { return nil } +func (t *test) execEval(cmd *evalCmd, engine QueryEngine) error { + if cmd.isRange { + return t.execRangeEval(cmd, engine) + } + + return t.execInstantEval(cmd, engine) +} + +func (t *test) execRangeEval(cmd *evalCmd, engine QueryEngine) error { + q, err := engine.NewRangeQuery(t.context, t.storage, nil, cmd.expr, cmd.start, cmd.end, cmd.step) + if err != nil { + return err + } + res := q.Exec(t.context) + if res.Err != nil { + if cmd.fail { + return nil + } + + return fmt.Errorf("error evaluating query %q (line %d): %w", cmd.expr, cmd.line, res.Err) + } + if res.Err == nil && cmd.fail { + return fmt.Errorf("expected error evaluating query %q (line %d) but got none", cmd.expr, cmd.line) + } + defer q.Close() + + if err := cmd.compareResult(res.Value); err != nil { + return fmt.Errorf("error in %s %s (line %d): %w", cmd, cmd.expr, cmd.line, err) + } + + return nil +} + +func (t *test) execInstantEval(cmd *evalCmd, engine QueryEngine) error { + queries, err := atModifierTestCases(cmd.expr, cmd.start) + if err != nil { + return err + } + queries = append([]atModifierTestCase{{expr: cmd.expr, evalTime: cmd.start}}, queries...) + for _, iq := range queries { + q, err := engine.NewInstantQuery(t.context, t.storage, nil, iq.expr, iq.evalTime) + if err != nil { + return err + } + defer q.Close() + res := q.Exec(t.context) + if res.Err != nil { + if cmd.fail { + continue + } + return fmt.Errorf("error evaluating query %q (line %d): %w", iq.expr, cmd.line, res.Err) + } + if res.Err == nil && cmd.fail { + return fmt.Errorf("expected error evaluating query %q (line %d) but got none", iq.expr, cmd.line) + } + err = cmd.compareResult(res.Value) + if err != nil { + return fmt.Errorf("error in %s %s (line %d): %w", cmd, iq.expr, cmd.line, err) + } + + // Check query returns same result in range mode, + // by checking against the middle step. + q, err = engine.NewRangeQuery(t.context, t.storage, nil, iq.expr, iq.evalTime.Add(-time.Minute), iq.evalTime.Add(time.Minute), time.Minute) + if err != nil { + return err + } + rangeRes := q.Exec(t.context) + if rangeRes.Err != nil { + return fmt.Errorf("error evaluating query %q (line %d) in range mode: %w", iq.expr, cmd.line, rangeRes.Err) + } + defer q.Close() + if cmd.ordered { + // Range queries are always sorted by labels, so skip this test case that expects results in a particular order. + continue + } + mat := rangeRes.Value.(Matrix) + if err := assertMatrixSorted(mat); err != nil { + return err + } + + vec := make(Vector, 0, len(mat)) + for _, series := range mat { + // We expect either Floats or Histograms. + for _, point := range series.Floats { + if point.T == timeMilliseconds(iq.evalTime) { + vec = append(vec, Sample{Metric: series.Metric, T: point.T, F: point.F}) + break + } + } + for _, point := range series.Histograms { + if point.T == timeMilliseconds(iq.evalTime) { + vec = append(vec, Sample{Metric: series.Metric, T: point.T, H: point.H}) + break + } + } + } + if _, ok := res.Value.(Scalar); ok { + err = cmd.compareResult(Scalar{V: vec[0].F}) + } else { + err = cmd.compareResult(vec) + } + if err != nil { + return fmt.Errorf("error in %s %s (line %d) range mode: %w", cmd, iq.expr, cmd.line, err) + } + } + + return nil +} + +func assertMatrixSorted(m Matrix) error { + if len(m) <= 1 { + return nil + } + + for i, s := range m[:len(m)-1] { + nextIndex := i + 1 + nextMetric := m[nextIndex].Metric + + if labels.Compare(s.Metric, nextMetric) > 0 { + return fmt.Errorf("matrix results should always be sorted by labels, but matrix is not sorted: series at index %v with labels %s sorts before series at index %v with labels %s", nextIndex, nextMetric, i, s.Metric) + } + } + + return nil +} + // clear the current test storage of all inserted samples. func (t *test) clear() { if t.storage != nil { diff --git a/promql/test_test.go b/promql/test_test.go index 316c177d77..0130a789d7 100644 --- a/promql/test_test.go +++ b/promql/test_test.go @@ -156,3 +156,354 @@ func TestLazyLoader_WithSamplesTill(t *testing.T) { } } } + +func TestRunTest(t *testing.T) { + testData := ` +load 5m + http_requests{job="api-server", instance="0", group="production"} 0+10x10 + http_requests{job="api-server", instance="1", group="production"} 0+20x10 + http_requests{job="api-server", instance="0", group="canary"} 0+30x10 + http_requests{job="api-server", instance="1", group="canary"} 0+40x10 +` + + testCases := map[string]struct { + input string + expectedError string + }{ + "instant query with expected float result": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 70 +`, + }, + "instant query with unexpected float result": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 80 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 80 for {group="canary"} but got 70`, + }, + "instant query with expected histogram result": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} + +eval instant at 0 testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} +`, + }, + "instant query with unexpected histogram result": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} + +eval instant at 0 testmetric + testmetric {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} +`, + expectedError: `error in eval testmetric (line 5): expected {{schema:-1 count:1 sum:6 offset:1 buckets:[1]}} for {__name__="testmetric"} but got {{schema:-1 count:1 sum:4 offset:1 buckets:[1]}}`, + }, + "instant query with float value returned when histogram expected": { + input: ` +load 5m + testmetric 2 + +eval instant at 0 testmetric + testmetric {{}} +`, + expectedError: `error in eval testmetric (line 5): expected histogram {{}} for {__name__="testmetric"} but got float value 2`, + }, + "instant query with histogram returned when float expected": { + input: ` +load 5m + testmetric {{}} + +eval instant at 0 testmetric + testmetric 2 +`, + expectedError: `error in eval testmetric (line 5): expected float value 2.000000 for {__name__="testmetric"} but got histogram {{}}`, + }, + "instant query, but result has an unexpected series": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + }, + "instant query, but result is missing a series": { + input: testData + ` +eval instant at 5m sum by (group) (http_requests) + {group="production"} 30 + {group="canary"} 70 + {group="test"} 100 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} with 3: [100.000000] not found`, + }, + "instant query expected to fail, and query fails": { + input: ` +load 5m + testmetric1{src="a",dst="b"} 0 + testmetric2{src="a",dst="b"} 1 + +eval_fail instant at 0m ceil({__name__=~'testmetric1|testmetric2'}) +`, + }, + "instant query expected to fail, but query succeeds": { + input: `eval_fail instant at 0s vector(0)`, + expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`, + }, + "instant query with results expected to match provided order, and result is in expected order": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="1", job="api-server"} 400 +`, + }, + "instant query with results expected to match provided order, but result is out of order": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="0", job="api-server"} 300 +`, + expectedError: `error in eval sort(http_requests) (line 8): expected metric {__name__="http_requests", group="canary", instance="0", job="api-server"} with [300.000000] at position 4 but was at 3`, + }, + "instant query with results expected to match provided order, but result has an unexpected series": { + input: testData + ` +eval_ordered instant at 50m sort(http_requests) + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="canary", instance="0", job="api-server"} 300 +`, + expectedError: `error in eval sort(http_requests) (line 8): unexpected metric {__name__="http_requests", group="canary", instance="1", job="api-server"} in result`, + }, + "instant query with invalid timestamp": { + input: `eval instant at abc123 vector(0)`, + expectedError: `error in eval vector(0) (line 1): invalid timestamp definition "abc123": not a valid duration string: "abc123"`, + }, + "range query with expected result": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 70 140 +`, + }, + "range query with unexpected float value": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 80 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected float value at index 1 (t=300000) for {group="canary"} to be 80, but got 70 (result has 3 float points [0 @[0] 70 @[300000] 140 @[600000]] and 0 histogram points [])`, + }, + "range query with expected histogram values": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} + +eval range from 0 to 10m step 5m testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} +`, + }, + "range query with unexpected histogram value": { + input: ` +load 5m + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:5 count:1 buckets:[1] offset:1}} {{schema:-1 sum:6 count:1 buckets:[1] offset:1}} + +eval range from 0 to 10m step 5m testmetric + testmetric {{schema:-1 sum:4 count:1 buckets:[1] offset:1}} {{schema:-1 sum:7 count:1 buckets:[1] offset:1}} {{schema:-1 sum:8 count:1 buckets:[1] offset:1}} +`, + expectedError: `error in eval testmetric (line 5): expected histogram value at index 1 (t=300000) for {__name__="testmetric"} to be {count:1, sum:7, (1,4]:1}, but got {count:1, sum:5, (1,4]:1} (result has 0 float points [] and 3 histogram points [{count:1, sum:4, (1,4]:1} @[0] {count:1, sum:5, (1,4]:1} @[300000] {count:1, sum:6, (1,4]:1} @[600000]])`, + }, + "range query with too many points for query time range": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 90 + {group="canary"} 0 70 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 4 points for {group="production"}, but query time range cannot return this many points`, + }, + "range query with missing point in result": { + input: ` +load 5m + testmetric 5 + +eval range from 0 to 6m step 6m testmetric + testmetric 5 10 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 1 float point [5 @[0]] and 0 histogram points []`, + }, + "range query with extra point in result": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 + {group="canary"} 0 70 140 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected 2 float points and 0 histogram points for {group="production"}, but got 3 float points [0 @[0] 30 @[300000] 60 @[600000]] and 0 histogram points []`, + }, + "range query, but result has an unexpected series": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): unexpected metric {group="canary"} in result`, + }, + "range query, but result is missing a series": { + input: testData + ` +eval range from 0 to 10m step 5m sum by (group) (http_requests) + {group="production"} 0 30 60 + {group="canary"} 0 70 140 + {group="test"} 0 100 200 +`, + expectedError: `error in eval sum by (group) (http_requests) (line 8): expected metric {group="test"} not found`, + }, + "range query expected to fail, and query fails": { + input: ` +load 5m + testmetric1{src="a",dst="b"} 0 + testmetric2{src="a",dst="b"} 1 + +eval_fail range from 0 to 10m step 5m ceil({__name__=~'testmetric1|testmetric2'}) +`, + }, + "range query expected to fail, but query succeeds": { + input: `eval_fail range from 0 to 10m step 5m vector(0)`, + expectedError: `expected error evaluating query "vector(0)" (line 1) but got none`, + }, + "range query with from and to timestamps in wrong order": { + input: `eval range from 10m to 9m step 5m vector(0)`, + expectedError: `error in eval vector(0) (line 1): invalid test definition, end timestamp (9m) is before start timestamp (10m)`, + }, + "range query with sparse output": { + input: ` +load 6m + testmetric 1 _ 3 + +eval range from 0 to 18m step 6m testmetric + testmetric 1 _ 3 +`, + }, + "range query with float value returned when no value expected": { + input: ` +load 6m + testmetric 1 2 3 + +eval range from 0 to 18m step 6m testmetric + testmetric 1 _ 3 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 3 float points [1 @[0] 2 @[360000] 3 @[720000]] and 0 histogram points []`, + }, + "range query with float value returned when histogram expected": { + input: ` +load 5m + testmetric 2 3 + +eval range from 0 to 5m step 5m testmetric + testmetric {{}} {{}} +`, + expectedError: `error in eval testmetric (line 5): expected 0 float points and 2 histogram points for {__name__="testmetric"}, but got 2 float points [2 @[0] 3 @[300000]] and 0 histogram points []`, + }, + "range query with histogram returned when float expected": { + input: ` +load 5m + testmetric {{}} {{}} + +eval range from 0 to 5m step 5m testmetric + testmetric 2 3 +`, + expectedError: `error in eval testmetric (line 5): expected 2 float points and 0 histogram points for {__name__="testmetric"}, but got 0 float points [] and 2 histogram points [{count:0, sum:0} @[0] {count:0, sum:0} @[300000]]`, + }, + "range query with expected mixed results": { + input: ` +load 6m + testmetric{group="a"} {{}} _ _ + testmetric{group="b"} _ _ 3 + +eval range from 0 to 12m step 6m sum(testmetric) + {} {{}} _ 3 +`, + }, + "range query with mixed results and incorrect values": { + input: ` +load 5m + testmetric 3 {{}} + +eval range from 0 to 5m step 5m testmetric + testmetric {{}} 3 +`, + expectedError: `error in eval testmetric (line 5): expected float value at index 0 for {__name__="testmetric"} to have timestamp 300000, but it had timestamp 0 (result has 1 float point [3 @[0]] and 1 histogram point [{count:0, sum:0} @[300000]])`, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + err := runTest(t, testCase.input, newTestEngine()) + + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, testCase.expectedError) + } + }) + } +} + +func TestAssertMatrixSorted(t *testing.T) { + testCases := map[string]struct { + matrix Matrix + expectedError string + }{ + "empty matrix": { + matrix: Matrix{}, + }, + "matrix with one series": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + }, + }, + "matrix with two series, series in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + }, + }, + "matrix with two series, series in reverse order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_2")}, + Series{Metric: labels.FromStrings("the_label", "value_1")}, + }, + expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 1 with labels {the_label="value_1"} sorts before series at index 0 with labels {the_label="value_2"}`, + }, + "matrix with three series, series in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + Series{Metric: labels.FromStrings("the_label", "value_3")}, + }, + }, + "matrix with three series, series not in sorted order": { + matrix: Matrix{ + Series{Metric: labels.FromStrings("the_label", "value_1")}, + Series{Metric: labels.FromStrings("the_label", "value_3")}, + Series{Metric: labels.FromStrings("the_label", "value_2")}, + }, + expectedError: `matrix results should always be sorted by labels, but matrix is not sorted: series at index 2 with labels {the_label="value_2"} sorts before series at index 1 with labels {the_label="value_3"}`, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + err := assertMatrixSorted(testCase.matrix) + + if testCase.expectedError == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, testCase.expectedError) + } + }) + } +} From 35aab01de02528714f75f8cdd197b633cbadf031 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Tue, 26 Mar 2024 15:16:45 +0100 Subject: [PATCH 25/29] tsdb/wlog.Checkpoint: Handle also float histograms Signed-off-by: Arve Knudsen --- tsdb/wlog/checkpoint.go | 43 ++++++++++++++++++++++++++---------- tsdb/wlog/checkpoint_test.go | 38 ++++++++++++++++++++++++++++--- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/tsdb/wlog/checkpoint.go b/tsdb/wlog/checkpoint.go index 5491d9718e..a16cd5fc74 100644 --- a/tsdb/wlog/checkpoint.go +++ b/tsdb/wlog/checkpoint.go @@ -149,22 +149,23 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head r := NewReader(sgmReader) var ( - series []record.RefSeries - samples []record.RefSample - histogramSamples []record.RefHistogramSample - tstones []tombstones.Stone - exemplars []record.RefExemplar - metadata []record.RefMetadata - st = labels.NewSymbolTable() // Needed for decoding; labels do not outlive this function. - dec = record.NewDecoder(st) - enc record.Encoder - buf []byte - recs [][]byte + series []record.RefSeries + samples []record.RefSample + histogramSamples []record.RefHistogramSample + floatHistogramSamples []record.RefFloatHistogramSample + tstones []tombstones.Stone + exemplars []record.RefExemplar + metadata []record.RefMetadata + st = labels.NewSymbolTable() // Needed for decoding; labels do not outlive this function. + dec = record.NewDecoder(st) + enc record.Encoder + buf []byte + recs [][]byte latestMetadataMap = make(map[chunks.HeadSeriesRef]record.RefMetadata) ) for r.Next() { - series, samples, histogramSamples, tstones, exemplars, metadata = series[:0], samples[:0], histogramSamples[:0], tstones[:0], exemplars[:0], metadata[:0] + series, samples, histogramSamples, floatHistogramSamples, tstones, exemplars, metadata = series[:0], samples[:0], histogramSamples[:0], floatHistogramSamples[:0], tstones[:0], exemplars[:0], metadata[:0] // We don't reset the buffer since we batch up multiple records // before writing them to the checkpoint. @@ -227,6 +228,24 @@ func Checkpoint(logger log.Logger, w *WL, from, to int, keep func(id chunks.Head stats.TotalSamples += len(histogramSamples) stats.DroppedSamples += len(histogramSamples) - len(repl) + case record.FloatHistogramSamples: + floatHistogramSamples, err = dec.FloatHistogramSamples(rec, floatHistogramSamples) + if err != nil { + return nil, fmt.Errorf("decode float histogram samples: %w", err) + } + // Drop irrelevant floatHistogramSamples in place. + repl := floatHistogramSamples[:0] + for _, fh := range floatHistogramSamples { + if fh.T >= mint { + repl = append(repl, fh) + } + } + if len(repl) > 0 { + buf = enc.FloatHistogramSamples(repl, buf) + } + stats.TotalSamples += len(floatHistogramSamples) + stats.DroppedSamples += len(floatHistogramSamples) - len(repl) + case record.Tombstones: tstones, err = dec.Tombstones(rec, tstones) if err != nil { diff --git a/tsdb/wlog/checkpoint_test.go b/tsdb/wlog/checkpoint_test.go index 0d22171768..279f7c4356 100644 --- a/tsdb/wlog/checkpoint_test.go +++ b/tsdb/wlog/checkpoint_test.go @@ -125,6 +125,20 @@ func TestCheckpoint(t *testing.T) { PositiveBuckets: []int64{int64(i + 1), 1, -1, 0}, } } + makeFloatHistogram := func(i int) *histogram.FloatHistogram { + return &histogram.FloatHistogram{ + Count: 5 + float64(i*4), + ZeroCount: 2 + float64(i), + ZeroThreshold: 0.001, + Sum: 18.4 * float64(i+1), + Schema: 1, + PositiveSpans: []histogram.Span{ + {Offset: 0, Length: 2}, + {Offset: 1, Length: 2}, + }, + PositiveBuckets: []float64{float64(i + 1), 1, -1, 0}, + } + } for _, compress := range []CompressionType{CompressionNone, CompressionSnappy, CompressionZstd} { t.Run(fmt.Sprintf("compress=%s", compress), func(t *testing.T) { @@ -154,7 +168,7 @@ func TestCheckpoint(t *testing.T) { w, err = NewSize(nil, nil, dir, 64*1024, compress) require.NoError(t, err) - samplesInWAL, histogramsInWAL := 0, 0 + samplesInWAL, histogramsInWAL, floatHistogramsInWAL := 0, 0, 0 var last int64 for i := 0; ; i++ { _, n, err := Segments(w.Dir()) @@ -200,6 +214,15 @@ func TestCheckpoint(t *testing.T) { }, nil) require.NoError(t, w.Log(b)) histogramsInWAL += 4 + fh := makeFloatHistogram(i) + b = enc.FloatHistogramSamples([]record.RefFloatHistogramSample{ + {Ref: 0, T: last, FH: fh}, + {Ref: 1, T: last + 10000, FH: fh}, + {Ref: 2, T: last + 20000, FH: fh}, + {Ref: 3, T: last + 30000, FH: fh}, + }, nil) + require.NoError(t, w.Log(b)) + floatHistogramsInWAL += 4 b = enc.Exemplars([]record.RefExemplar{ {Ref: 1, T: last, V: float64(i), Labels: labels.FromStrings("trace_id", fmt.Sprintf("trace-%d", i))}, @@ -226,7 +249,7 @@ func TestCheckpoint(t *testing.T) { require.NoError(t, err) require.NoError(t, w.Truncate(107)) require.NoError(t, DeleteCheckpoints(w.Dir(), 106)) - require.Equal(t, histogramsInWAL+samplesInWAL, stats.TotalSamples) + require.Equal(t, histogramsInWAL+floatHistogramsInWAL+samplesInWAL, stats.TotalSamples) require.Greater(t, stats.DroppedSamples, 0) // Only the new checkpoint should be left. @@ -244,7 +267,7 @@ func TestCheckpoint(t *testing.T) { var metadata []record.RefMetadata r := NewReader(sr) - samplesInCheckpoint, histogramsInCheckpoint := 0, 0 + samplesInCheckpoint, histogramsInCheckpoint, floatHistogramsInCheckpoint := 0, 0, 0 for r.Next() { rec := r.Record() @@ -266,6 +289,13 @@ func TestCheckpoint(t *testing.T) { require.GreaterOrEqual(t, h.T, last/2, "histogram with wrong timestamp") } histogramsInCheckpoint += len(histograms) + case record.FloatHistogramSamples: + floatHistograms, err := dec.FloatHistogramSamples(rec, nil) + require.NoError(t, err) + for _, h := range floatHistograms { + require.GreaterOrEqual(t, h.T, last/2, "float histogram with wrong timestamp") + } + floatHistogramsInCheckpoint += len(floatHistograms) case record.Exemplars: exemplars, err := dec.Exemplars(rec, nil) require.NoError(t, err) @@ -283,6 +313,8 @@ func TestCheckpoint(t *testing.T) { require.Less(t, float64(samplesInCheckpoint)/float64(samplesInWAL), 0.8) require.Greater(t, float64(histogramsInCheckpoint)/float64(histogramsInWAL), 0.5) require.Less(t, float64(histogramsInCheckpoint)/float64(histogramsInWAL), 0.8) + require.Greater(t, float64(floatHistogramsInCheckpoint)/float64(floatHistogramsInWAL), 0.5) + require.Less(t, float64(floatHistogramsInCheckpoint)/float64(floatHistogramsInWAL), 0.8) expectedRefSeries := []record.RefSeries{ {Ref: 0, Labels: labels.FromStrings("a", "b", "c", "0")}, From 435f330d0b139111c1c6e9a3ab5854c7077e4056 Mon Sep 17 00:00:00 2001 From: Domantas Date: Wed, 27 Mar 2024 12:35:17 +0200 Subject: [PATCH 26/29] [BUGFIX] labels: don't modify original labels in DropMetricName (#13845) Restrict the capacity of first argument to `append()` to force an allocation. This is for the slice implementation only. Signed-off-by: Domantas Jadenkus --- model/labels/labels.go | 4 +++- model/labels/labels_test.go | 6 +++++- promql/engine_test.go | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/model/labels/labels.go b/model/labels/labels.go index e998248269..01514abf38 100644 --- a/model/labels/labels.go +++ b/model/labels/labels.go @@ -349,7 +349,9 @@ func (ls Labels) DropMetricName() Labels { if i == 0 { // Make common case fast with no allocations. return ls[1:] } - return append(ls[:i], ls[i+1:]...) + // Avoid modifying original Labels - use [:i:i] so that left slice would not + // have any spare capacity and append would have to allocate a new slice for the result. + return append(ls[:i:i], ls[i+1:]...) } } return ls diff --git a/model/labels/labels_test.go b/model/labels/labels_test.go index cedeb95a6c..90ae41ccea 100644 --- a/model/labels/labels_test.go +++ b/model/labels/labels_test.go @@ -457,7 +457,11 @@ func TestLabels_Get(t *testing.T) { func TestLabels_DropMetricName(t *testing.T) { require.True(t, Equal(FromStrings("aaa", "111", "bbb", "222"), FromStrings("aaa", "111", "bbb", "222").DropMetricName())) require.True(t, Equal(FromStrings("aaa", "111"), FromStrings(MetricName, "myname", "aaa", "111").DropMetricName())) - require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222").DropMetricName())) + + original := FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222") + check := FromStrings("__aaa__", "111", MetricName, "myname", "bbb", "222") + require.True(t, Equal(FromStrings("__aaa__", "111", "bbb", "222"), check.DropMetricName())) + require.True(t, Equal(original, check)) } // BenchmarkLabels_Get was written to check whether a binary search can improve the performance vs the linear search implementation diff --git a/promql/engine_test.go b/promql/engine_test.go index cc5d0ee780..3f6727d849 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3212,6 +3212,24 @@ func TestRangeQuery(t *testing.T) { End: time.Unix(120, 0), Interval: 1 * time.Minute, }, + { + Name: "drop-metric-name", + Load: `load 30s + requests{job="1", __address__="bar"} 100`, + Query: `requests * 2`, + Result: Matrix{ + Series{ + Floats: []FPoint{{F: 200, T: 0}, {F: 200, T: 60000}, {F: 200, T: 120000}}, + Metric: labels.FromStrings( + "__address__", "bar", + "job", "1", + ), + }, + }, + Start: time.Unix(0, 0), + End: time.Unix(120, 0), + Interval: 1 * time.Minute, + }, } for _, c := range cases { t.Run(c.Name, func(t *testing.T) { From 64dfd8a158fe8de72d8019c54ea4242956e4d652 Mon Sep 17 00:00:00 2001 From: Ziqi Zhao Date: Wed, 27 Mar 2024 23:32:37 +0800 Subject: [PATCH 27/29] fix the bug of setting native histogram min bucket factor (#13846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix the bug of setting native histogram min bucket factor Signed-off-by: Ziqi Zhao * Add unit test for checking that min_bucket_factor is correctly applied Signed-off-by: György Krajcsovits --------- Signed-off-by: Ziqi Zhao Signed-off-by: György Krajcsovits Co-authored-by: György Krajcsovits --- scrape/scrape.go | 2 + scrape/scrape_test.go | 135 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/scrape/scrape.go b/scrape/scrape.go index 064b0b67bc..734c248130 100644 --- a/scrape/scrape.go +++ b/scrape/scrape.go @@ -427,6 +427,7 @@ func (sp *scrapePool) sync(targets []*Target) { bodySizeLimit = int64(sp.config.BodySizeLimit) sampleLimit = int(sp.config.SampleLimit) bucketLimit = int(sp.config.NativeHistogramBucketLimit) + maxSchema = pickSchema(sp.config.NativeHistogramMinBucketFactor) labelLimits = &labelLimits{ labelLimit: int(sp.config.LabelLimit), labelNameLengthLimit: int(sp.config.LabelNameLengthLimit), @@ -464,6 +465,7 @@ func (sp *scrapePool) sync(targets []*Target) { scraper: s, sampleLimit: sampleLimit, bucketLimit: bucketLimit, + maxSchema: maxSchema, labelLimits: labelLimits, honorLabels: honorLabels, honorTimestamps: honorTimestamps, diff --git a/scrape/scrape_test.go b/scrape/scrape_test.go index 0dfca538d8..20b21936b9 100644 --- a/scrape/scrape_test.go +++ b/scrape/scrape_test.go @@ -41,6 +41,7 @@ import ( "github.com/stretchr/testify/require" "github.com/prometheus/prometheus/config" + "github.com/prometheus/prometheus/discovery" "github.com/prometheus/prometheus/discovery/targetgroup" "github.com/prometheus/prometheus/model/exemplar" "github.com/prometheus/prometheus/model/histogram" @@ -3631,3 +3632,137 @@ func TestScrapeLoopSeriesAddedDuplicates(t *testing.T) { value := metric.GetCounter().GetValue() require.Equal(t, 4.0, value) } + +// This tests running a full scrape loop and checking that the scrape option +// `native_histogram_min_bucket_factor` is used correctly. +func TestNativeHistogramMaxSchemaSet(t *testing.T) { + testcases := map[string]struct { + minBucketFactor string + expectedSchema int32 + }{ + "min factor not specified": { + minBucketFactor: "", + expectedSchema: 3, // Factor 1.09. + }, + "min factor 1": { + minBucketFactor: "native_histogram_min_bucket_factor: 1", + expectedSchema: 3, // Factor 1.09. + }, + "min factor 2": { + minBucketFactor: "native_histogram_min_bucket_factor: 2", + expectedSchema: 0, // Factor 2.00. + }, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + testNativeHistogramMaxSchemaSet(t, tc.minBucketFactor, tc.expectedSchema) + }) + } +} + +func testNativeHistogramMaxSchemaSet(t *testing.T, minBucketFactor string, expectedSchema int32) { + // Create a ProtoBuf message to serve as a Prometheus metric. + nativeHistogram := prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: "testing", + Name: "example_native_histogram", + Help: "This is used for testing", + NativeHistogramBucketFactor: 1.1, + NativeHistogramMaxBucketNumber: 100, + }, + ) + registry := prometheus.NewRegistry() + registry.Register(nativeHistogram) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(1.0) + nativeHistogram.Observe(10.0) // in different bucket since > 1*1.1. + nativeHistogram.Observe(10.0) + + gathered, err := registry.Gather() + require.NoError(t, err) + require.NotEmpty(t, gathered) + + histogramMetricFamily := gathered[0] + buffer := protoMarshalDelimited(t, histogramMetricFamily) + + // Create a HTTP server to serve /metrics via ProtoBuf + metricsServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", `application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited`) + w.Write(buffer) + })) + defer metricsServer.Close() + + // Create a scrape loop with the HTTP server as the target. + configStr := fmt.Sprintf(` +global: + scrape_interval: 1s + scrape_timeout: 1s +scrape_configs: + - job_name: test + %s + static_configs: + - targets: [%s] +`, minBucketFactor, strings.ReplaceAll(metricsServer.URL, "http://", "")) + + s := teststorage.New(t) + defer s.Close() + s.DB.EnableNativeHistograms() + reg := prometheus.NewRegistry() + + mng, err := NewManager(nil, nil, s, reg) + require.NoError(t, err) + cfg, err := config.Load(configStr, false, log.NewNopLogger()) + require.NoError(t, err) + mng.ApplyConfig(cfg) + tsets := make(chan map[string][]*targetgroup.Group) + go func() { + err = mng.Run(tsets) + require.NoError(t, err) + }() + defer mng.Stop() + + // Get the static targets and apply them to the scrape manager. + require.Len(t, cfg.ScrapeConfigs, 1) + scrapeCfg := cfg.ScrapeConfigs[0] + require.Len(t, scrapeCfg.ServiceDiscoveryConfigs, 1) + staticDiscovery, ok := scrapeCfg.ServiceDiscoveryConfigs[0].(discovery.StaticConfig) + require.True(t, ok) + require.Len(t, staticDiscovery, 1) + tsets <- map[string][]*targetgroup.Group{"test": staticDiscovery} + + // Wait for the scrape loop to scrape the target. + require.Eventually(t, func() bool { + q, err := s.Querier(0, math.MaxInt64) + require.NoError(t, err) + seriesS := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing_example_native_histogram")) + countSeries := 0 + for seriesS.Next() { + countSeries++ + } + return countSeries > 0 + }, 15*time.Second, 100*time.Millisecond) + + // Check that native histogram schema is as expected. + q, err := s.Querier(0, math.MaxInt64) + require.NoError(t, err) + seriesS := q.Select(context.Background(), false, nil, labels.MustNewMatcher(labels.MatchEqual, "__name__", "testing_example_native_histogram")) + histogramSamples := []*histogram.Histogram{} + for seriesS.Next() { + series := seriesS.At() + it := series.Iterator(nil) + for vt := it.Next(); vt != chunkenc.ValNone; vt = it.Next() { + if vt != chunkenc.ValHistogram { + // don't care about other samples + continue + } + _, h := it.AtHistogram(nil) + histogramSamples = append(histogramSamples, h) + } + } + require.NoError(t, seriesS.Err()) + require.NotEmpty(t, histogramSamples) + for _, h := range histogramSamples { + require.Equal(t, expectedSchema, h.Schema) + } +} From 22d0f4f114b9143c66d7805ed92c5f692e74c066 Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 28/29] improve handling of negative bounds in histogram std dev/var Signed-off-by: Jeanette Tan --- promql/engine_test.go | 6 +++--- promql/functions.go | 6 ++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/promql/engine_test.go b/promql/engine_test.go index 3f5f2dc132..9d43efdb90 100644 --- a/promql/engine_test.go +++ b/promql/engine_test.go @@ -3496,7 +3496,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{1, 0}, }, - stdVar: 1544.8582535368798, // actual variance: 1738.4082 + stdVar: 1844.4651144196398, // actual variance: 1738.4082 }, { name: "-100000, -10000, -1000, -888, -888, -100, -50, -9, -8, -3", @@ -3514,7 +3514,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{1, 0, 0, 0, 0, 2, -2, 0}, }, - stdVar: 1240930974.5260057, // actual variance: 882690990 + stdVar: 759352122.1939945, // actual variance: 882690990 }, { name: "-10 x10", @@ -3528,7 +3528,7 @@ func TestNativeHistogram_HistogramStdDevVar(t *testing.T) { }, NegativeBuckets: []int64{10}, }, - stdVar: 454.2741699796952, // actual variance: 0 + stdVar: 1.725830020304794, // actual variance: 0 }, { name: "-50, -8, 0, 3, 8, 9, 100, NaN", diff --git a/promql/functions.go b/promql/functions.go index da66af2f02..e0b811a09d 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1116,6 +1116,9 @@ func funcHistogramStdDev(vals []parser.Value, args parser.Expressions, enh *Eval val = 0 } else { val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } } delta := val - mean variance, cVariance = kahanSumInc(bucket.Count*delta*delta, variance, cVariance) @@ -1149,6 +1152,9 @@ func funcHistogramStdVar(vals []parser.Value, args parser.Expressions, enh *Eval val = 0 } else { val = math.Sqrt(bucket.Upper * bucket.Lower) + if bucket.Upper < 0 { + val = -val + } } delta := val - mean variance, cVariance = kahanSumInc(bucket.Count*delta*delta, variance, cVariance) From 4f2df329bd8d49f69eff060da04edf91756a36fb Mon Sep 17 00:00:00 2001 From: Jeanette Tan Date: Fri, 22 Mar 2024 03:42:50 +0800 Subject: [PATCH 29/29] improve handling of empty buckets with infinite bounds in histogram std dev/var Signed-off-by: Jeanette Tan --- promql/functions.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/promql/functions.go b/promql/functions.go index e0b811a09d..6853307d0e 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -1111,6 +1111,9 @@ func funcHistogramStdDev(vals []parser.Value, args parser.Expressions, enh *Eval it := sample.H.AllBucketIterator() for it.Next() { bucket := it.At() + if bucket.Count == 0 { + continue + } var val float64 if bucket.Lower <= 0 && 0 <= bucket.Upper { val = 0 @@ -1147,6 +1150,9 @@ func funcHistogramStdVar(vals []parser.Value, args parser.Expressions, enh *Eval it := sample.H.AllBucketIterator() for it.Next() { bucket := it.At() + if bucket.Count == 0 { + continue + } var val float64 if bucket.Lower <= 0 && 0 <= bucket.Upper { val = 0