Skip to content

Commit

Permalink
Caching only regex matchers
Browse files Browse the repository at this point in the history
Signed-off-by: alanprot <[email protected]>
  • Loading branch information
alanprot committed Jan 7, 2025
1 parent dc929f5 commit 7126b9a
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 78 deletions.
61 changes: 56 additions & 5 deletions pkg/store/cache/matchers_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
package storecache

import (
"strings"

lru "github.com/hashicorp/golang-lru/v2"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
Expand All @@ -17,22 +19,37 @@ const DefaultCacheSize = 200

type NewItemFunc func() (*labels.Matcher, error)

type ConversionLabelMatcher interface {
GetValue() string
GetName() string
MatcherType() (labels.MatchType, error)
}

type MatchersCache interface {
// GetOrSet retrieves a matcher from cache or creates and stores it if not present.
// If the matcher is not in cache, it uses the provided newItem function to create it.
GetOrSet(key string, newItem NewItemFunc) (*labels.Matcher, error)
GetOrSet(m ConversionLabelMatcher, newItem NewItemFunc) (*labels.Matcher, error)
}

// Ensure implementations satisfy the interface.
var (
_ MatchersCache = (*LruMatchersCache)(nil)
NoopMatchersCache MatchersCache = &noopMatcherCache{}

defaultIsCacheableFunc = func(m ConversionLabelMatcher) bool {
t, err := m.MatcherType()
if err != nil {
return false
}

return t == labels.MatchRegexp || t == labels.MatchNotRegexp
}
)

type noopMatcherCache struct{}

// GetOrSet implements MatchersCache by always creating a new matcher without caching.
func (n *noopMatcherCache) GetOrSet(_ string, newItem NewItemFunc) (*labels.Matcher, error) {
func (n *noopMatcherCache) GetOrSet(_ ConversionLabelMatcher, newItem NewItemFunc) (*labels.Matcher, error) {
return newItem()
}

Expand All @@ -43,6 +60,8 @@ type LruMatchersCache struct {
metrics *matcherCacheMetrics
size int
sf singleflight.Group

isCacheable func(matcher ConversionLabelMatcher) bool
}

type MatcherCacheOption func(*LruMatchersCache)
Expand All @@ -59,9 +78,17 @@ func WithSize(size int) MatcherCacheOption {
}
}

// WithIsCacheable sets the function that determines if the item should be cached or not

Check failure on line 81 in pkg/store/cache/matchers_cache.go

View workflow job for this annotation

GitHub Actions / Linters (Static Analysis) for Go

Comment should end in a period (godot)
func WithIsCacheable(f func(matcher ConversionLabelMatcher) bool) MatcherCacheOption {
return func(c *LruMatchersCache) {
c.isCacheable = f
}
}

func NewMatchersCache(opts ...MatcherCacheOption) (*LruMatchersCache, error) {
cache := &LruMatchersCache{
size: DefaultCacheSize,
size: DefaultCacheSize,
isCacheable: defaultIsCacheableFunc,
}

for _, opt := range opts {
Expand All @@ -79,8 +106,18 @@ func NewMatchersCache(opts ...MatcherCacheOption) (*LruMatchersCache, error) {
return cache, nil
}

func (c *LruMatchersCache) GetOrSet(key string, newItem NewItemFunc) (*labels.Matcher, error) {
func (c *LruMatchersCache) GetOrSet(m ConversionLabelMatcher, newItem NewItemFunc) (*labels.Matcher, error) {
if !c.isCacheable(m) {
return newItem()
}

c.metrics.requestsTotal.Inc()
key, err := cacheKey(m)

if err != nil {
return nil, err
}

v, err, _ := c.sf.Do(key, func() (interface{}, error) {
if item, ok := c.cache.Get(key); ok {
c.metrics.hitsTotal.Inc()
Expand Down Expand Up @@ -146,11 +183,25 @@ func newMatcherCacheMetrics(reg prometheus.Registerer) *matcherCacheMetrics {
func MatchersToPromMatchersCached(cache MatchersCache, ms ...storepb.LabelMatcher) ([]*labels.Matcher, error) {
res := make([]*labels.Matcher, 0, len(ms))
for i := range ms {
pm, err := cache.GetOrSet(ms[i].String(), func() (*labels.Matcher, error) { return storepb.MatcherToPromMatcher(ms[i]) })
pm, err := cache.GetOrSet(&ms[i], func() (*labels.Matcher, error) { return storepb.MatcherToPromMatcher(ms[i]) })
if err != nil {
return nil, err
}
res = append(res, pm)
}
return res, nil
}

func cacheKey(m ConversionLabelMatcher) (string, error) {
sb := strings.Builder{}
t, err := m.MatcherType()
if err != nil {
return "", err
}
typeStr := t.String()
sb.Grow(len(m.GetValue()) + len(m.GetName()) + len(typeStr))
sb.WriteString(m.GetName())
sb.WriteString(typeStr)
sb.WriteString(m.GetValue())
return sb.String(), nil
}
165 changes: 92 additions & 73 deletions pkg/store/cache/matchers_cache_test.go
Original file line number Diff line number Diff line change
@@ -1,95 +1,114 @@
// Copyright (c) The Thanos Authors.
// Licensed under the Apache License 2.0.

package storecache_test
package storecache

import (
"testing"

"github.com/efficientgo/core/testutil"
"github.com/prometheus/prometheus/model/labels"

storecache "github.com/thanos-io/thanos/pkg/store/cache"
"github.com/thanos-io/thanos/pkg/store/storepb"
)

func TestMatchersCache(t *testing.T) {
cache, err := storecache.NewMatchersCache(storecache.WithSize(2))
testutil.Ok(t, err)

matcher := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_EQ,
Name: "key",
Value: "val",
}

matcher2 := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_RE,
Name: "key2",
Value: "val2|val3",
}

matcher3 := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_EQ,
Name: "key3",
Value: "val3",
testCases := map[string]struct {
isCacheable func(matcher ConversionLabelMatcher) bool
}{
"default": {
isCacheable: defaultIsCacheableFunc,
},
"cache all items": {
isCacheable: func(matcher ConversionLabelMatcher) bool {
return true
},
},
}

var cacheHit bool
newItem := func(matcher *storepb.LabelMatcher) func() (*labels.Matcher, error) {
return func() (*labels.Matcher, error) {
cacheHit = false
return storepb.MatcherToPromMatcher(*matcher)
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
cache, err := NewMatchersCache(
WithSize(2),
WithIsCacheable(tc.isCacheable),
)
testutil.Ok(t, err)

matcher := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_EQ,
Name: "key",
Value: "val",
}

matcher2 := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_RE,
Name: "key2",
Value: "val2|val3",
}

matcher3 := &storepb.LabelMatcher{
Type: storepb.LabelMatcher_EQ,
Name: "key3",
Value: "val3",
}

var cacheHit bool
newItem := func(matcher *storepb.LabelMatcher) func() (*labels.Matcher, error) {
return func() (*labels.Matcher, error) {
cacheHit = false
return storepb.MatcherToPromMatcher(*matcher)
}
}
expected := labels.MustNewMatcher(labels.MatchEqual, "key", "val")
expected2 := labels.MustNewMatcher(labels.MatchRegexp, "key2", "val2|val3")
expected3 := labels.MustNewMatcher(labels.MatchEqual, "key3", "val3")

item, err := cache.GetOrSet(matcher, newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher, newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, tc.isCacheable(matcher), cacheHit)
testutil.Equals(t, expected.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher2, newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected2.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher2, newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, tc.isCacheable(matcher2), cacheHit)
testutil.Equals(t, expected2.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher, newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, tc.isCacheable(matcher), cacheHit)
testutil.Equals(t, expected, item)

cacheHit = true
item, err = cache.GetOrSet(matcher3, newItem(matcher3))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected3, item)

cacheHit = true
item, err = cache.GetOrSet(matcher2, newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, tc.isCacheable(matcher2) && cache.cache.Len() < 2, cacheHit)
testutil.Equals(t, expected2.String(), item.String())
})
}
expected := labels.MustNewMatcher(labels.MatchEqual, "key", "val")
expected2 := labels.MustNewMatcher(labels.MatchRegexp, "key2", "val2|val3")
expected3 := labels.MustNewMatcher(labels.MatchEqual, "key3", "val3")

item, err := cache.GetOrSet(matcher.String(), newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher.String(), newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, true, cacheHit)
testutil.Equals(t, expected.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher2.String(), newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected2.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher2.String(), newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, true, cacheHit)
testutil.Equals(t, expected2.String(), item.String())

cacheHit = true
item, err = cache.GetOrSet(matcher.String(), newItem(matcher))
testutil.Ok(t, err)
testutil.Equals(t, true, cacheHit)
testutil.Equals(t, expected, item)

cacheHit = true
item, err = cache.GetOrSet(matcher3.String(), newItem(matcher3))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected3, item)

cacheHit = true
item, err = cache.GetOrSet(matcher2.String(), newItem(matcher2))
testutil.Ok(t, err)
testutil.Equals(t, false, cacheHit)
testutil.Equals(t, expected2.String(), item.String())
}

func BenchmarkMatchersCache(b *testing.B) {
cache, err := storecache.NewMatchersCache(storecache.WithSize(100))
cache, err := NewMatchersCache(WithSize(100))
if err != nil {
b.Fatalf("failed to create cache: %v", err)
}
Expand All @@ -106,7 +125,7 @@ func BenchmarkMatchersCache(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
matcher := matchers[i%len(matchers)]
_, err := cache.GetOrSet(matcher.String(), func() (*labels.Matcher, error) {
_, err := cache.GetOrSet(matcher, func() (*labels.Matcher, error) {
return storepb.MatcherToPromMatcher(*matcher)
})
if err != nil {
Expand Down
18 changes: 18 additions & 0 deletions pkg/store/storepb/custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,3 +552,21 @@ func (c *SeriesStatsCounter) Count(series *Series) {
func (m *SeriesRequest) ToPromQL() string {
return m.QueryHints.toPromQL(m.Matchers)
}

func (m *LabelMatcher) MatcherType() (labels.MatchType, error) {
var t labels.MatchType
switch m.Type {
case LabelMatcher_EQ:
t = labels.MatchEqual
case LabelMatcher_NEQ:
t = labels.MatchNotEqual
case LabelMatcher_RE:
t = labels.MatchRegexp
case LabelMatcher_NRE:
t = labels.MatchNotRegexp
default:
return 0, errors.Errorf("unrecognized label matcher type %d", m.Type)
}

return t, nil
}

0 comments on commit 7126b9a

Please sign in to comment.