github.com/thanos-io/thanos@v0.32.5/pkg/store/cache/memcached_test.go (about)

     1  // Copyright (c) The Thanos Authors.
     2  // Licensed under the Apache License 2.0.
     3  
     4  package storecache
     5  
     6  import (
     7  	"context"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/go-kit/log"
    12  	"github.com/oklog/ulid"
    13  	"github.com/pkg/errors"
    14  	prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
    15  	"github.com/prometheus/prometheus/model/labels"
    16  	"github.com/prometheus/prometheus/storage"
    17  
    18  	"github.com/efficientgo/core/testutil"
    19  )
    20  
    21  func TestMemcachedIndexCache_FetchMultiPostings(t *testing.T) {
    22  	t.Parallel()
    23  
    24  	// Init some data to conveniently define test cases later one.
    25  	block1 := ulid.MustNew(1, nil)
    26  	block2 := ulid.MustNew(2, nil)
    27  	label1 := labels.Label{Name: "instance", Value: "a"}
    28  	label2 := labels.Label{Name: "instance", Value: "b"}
    29  	value1 := []byte{1}
    30  	value2 := []byte{2}
    31  	value3 := []byte{3}
    32  
    33  	tests := map[string]struct {
    34  		setup          []mockedPostings
    35  		mockedErr      error
    36  		fetchBlockID   ulid.ULID
    37  		fetchLabels    []labels.Label
    38  		expectedHits   map[labels.Label][]byte
    39  		expectedMisses []labels.Label
    40  	}{
    41  		"should return no hits on empty cache": {
    42  			setup:          []mockedPostings{},
    43  			fetchBlockID:   block1,
    44  			fetchLabels:    []labels.Label{label1, label2},
    45  			expectedHits:   nil,
    46  			expectedMisses: []labels.Label{label1, label2},
    47  		},
    48  		"should return no misses on 100% hit ratio": {
    49  			setup: []mockedPostings{
    50  				{block: block1, label: label1, value: value1},
    51  				{block: block1, label: label2, value: value2},
    52  				{block: block2, label: label1, value: value3},
    53  			},
    54  			fetchBlockID: block1,
    55  			fetchLabels:  []labels.Label{label1, label2},
    56  			expectedHits: map[labels.Label][]byte{
    57  				label1: value1,
    58  				label2: value2,
    59  			},
    60  			expectedMisses: nil,
    61  		},
    62  		"should return hits and misses on partial hits": {
    63  			setup: []mockedPostings{
    64  				{block: block1, label: label1, value: value1},
    65  				{block: block2, label: label1, value: value3},
    66  			},
    67  			fetchBlockID:   block1,
    68  			fetchLabels:    []labels.Label{label1, label2},
    69  			expectedHits:   map[labels.Label][]byte{label1: value1},
    70  			expectedMisses: []labels.Label{label2},
    71  		},
    72  		"should return no hits on memcached error": {
    73  			setup: []mockedPostings{
    74  				{block: block1, label: label1, value: value1},
    75  				{block: block1, label: label2, value: value2},
    76  				{block: block2, label: label1, value: value3},
    77  			},
    78  			mockedErr:      errors.New("mocked error"),
    79  			fetchBlockID:   block1,
    80  			fetchLabels:    []labels.Label{label1, label2},
    81  			expectedHits:   nil,
    82  			expectedMisses: []labels.Label{label1, label2},
    83  		},
    84  	}
    85  
    86  	for testName, testData := range tests {
    87  		t.Run(testName, func(t *testing.T) {
    88  			memcached := newMockedMemcachedClient(testData.mockedErr)
    89  			c, err := NewRemoteIndexCache(log.NewNopLogger(), memcached, nil, nil)
    90  			testutil.Ok(t, err)
    91  
    92  			// Store the postings expected before running the test.
    93  			ctx := context.Background()
    94  			for _, p := range testData.setup {
    95  				c.StorePostings(p.block, p.label, p.value)
    96  			}
    97  
    98  			// Fetch postings from cached and assert on it.
    99  			hits, misses := c.FetchMultiPostings(ctx, testData.fetchBlockID, testData.fetchLabels)
   100  			testutil.Equals(t, testData.expectedHits, hits)
   101  			testutil.Equals(t, testData.expectedMisses, misses)
   102  
   103  			// Assert on metrics.
   104  			testutil.Equals(t, float64(len(testData.fetchLabels)), prom_testutil.ToFloat64(c.postingRequests))
   105  			testutil.Equals(t, float64(len(testData.expectedHits)), prom_testutil.ToFloat64(c.postingHits))
   106  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.seriesRequests))
   107  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.seriesHits))
   108  		})
   109  	}
   110  }
   111  
   112  func TestMemcachedIndexCache_FetchExpandedPostings(t *testing.T) {
   113  	t.Parallel()
   114  
   115  	// Init some data to conveniently define test cases later one.
   116  	block1 := ulid.MustNew(1, nil)
   117  	block2 := ulid.MustNew(2, nil)
   118  	matcher1 := labels.MustNewMatcher(labels.MatchEqual, "cluster", "us")
   119  	matcher2 := labels.MustNewMatcher(labels.MatchEqual, "job", "thanos")
   120  	matcher3 := labels.MustNewMatcher(labels.MatchRegexp, "__name__", "up")
   121  	value1 := []byte{1}
   122  	value2 := []byte{2}
   123  
   124  	tests := map[string]struct {
   125  		setup         []mockedExpandedPostings
   126  		mockedErr     error
   127  		fetchBlockID  ulid.ULID
   128  		fetchMatchers []*labels.Matcher
   129  		expectedHit   bool
   130  		expectedValue []byte
   131  	}{
   132  		"should return no hits on empty cache": {
   133  			setup:         []mockedExpandedPostings{},
   134  			fetchBlockID:  block1,
   135  			fetchMatchers: []*labels.Matcher{matcher1, matcher2},
   136  			expectedHit:   false,
   137  		},
   138  		"should return no misses on 100% hit ratio": {
   139  			setup: []mockedExpandedPostings{
   140  				{block: block1, matchers: []*labels.Matcher{matcher1}, value: value1},
   141  			},
   142  			fetchBlockID:  block1,
   143  			fetchMatchers: []*labels.Matcher{matcher1},
   144  			expectedHit:   true,
   145  			expectedValue: value1,
   146  		},
   147  		"Cache miss when matchers key doesn't match": {
   148  			setup: []mockedExpandedPostings{
   149  				{block: block1, matchers: []*labels.Matcher{matcher1}, value: value1},
   150  				{block: block2, matchers: []*labels.Matcher{matcher2}, value: value2},
   151  			},
   152  			fetchBlockID:  block1,
   153  			fetchMatchers: []*labels.Matcher{matcher1, matcher2},
   154  			expectedHit:   false,
   155  		},
   156  		"should return no hits on memcached error": {
   157  			setup: []mockedExpandedPostings{
   158  				{block: block1, matchers: []*labels.Matcher{matcher3}, value: value1},
   159  			},
   160  			mockedErr:     errors.New("mocked error"),
   161  			fetchBlockID:  block1,
   162  			fetchMatchers: []*labels.Matcher{matcher3},
   163  			expectedHit:   false,
   164  		},
   165  	}
   166  
   167  	for testName, testData := range tests {
   168  		t.Run(testName, func(t *testing.T) {
   169  			memcached := newMockedMemcachedClient(testData.mockedErr)
   170  			c, err := NewRemoteIndexCache(log.NewNopLogger(), memcached, nil, nil)
   171  			testutil.Ok(t, err)
   172  
   173  			// Store the postings expected before running the test.
   174  			ctx := context.Background()
   175  			for _, p := range testData.setup {
   176  				c.StoreExpandedPostings(p.block, p.matchers, p.value)
   177  			}
   178  
   179  			// Fetch postings from cached and assert on it.
   180  			val, hit := c.FetchExpandedPostings(ctx, testData.fetchBlockID, testData.fetchMatchers)
   181  			testutil.Equals(t, testData.expectedHit, hit)
   182  			if hit {
   183  				testutil.Equals(t, testData.expectedValue, val)
   184  			}
   185  
   186  			// Assert on metrics.
   187  			testutil.Equals(t, 1.0, prom_testutil.ToFloat64(c.expandedPostingRequests))
   188  			if testData.expectedHit {
   189  				testutil.Equals(t, 1.0, prom_testutil.ToFloat64(c.expandedPostingHits))
   190  			}
   191  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.postingRequests))
   192  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.postingHits))
   193  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.seriesRequests))
   194  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.seriesHits))
   195  		})
   196  	}
   197  }
   198  
   199  func TestMemcachedIndexCache_FetchMultiSeries(t *testing.T) {
   200  	t.Parallel()
   201  
   202  	// Init some data to conveniently define test cases later one.
   203  	block1 := ulid.MustNew(1, nil)
   204  	block2 := ulid.MustNew(2, nil)
   205  	value1 := []byte{1}
   206  	value2 := []byte{2}
   207  	value3 := []byte{3}
   208  
   209  	tests := map[string]struct {
   210  		setup          []mockedSeries
   211  		mockedErr      error
   212  		fetchBlockID   ulid.ULID
   213  		fetchIds       []storage.SeriesRef
   214  		expectedHits   map[storage.SeriesRef][]byte
   215  		expectedMisses []storage.SeriesRef
   216  	}{
   217  		"should return no hits on empty cache": {
   218  			setup:          []mockedSeries{},
   219  			fetchBlockID:   block1,
   220  			fetchIds:       []storage.SeriesRef{1, 2},
   221  			expectedHits:   nil,
   222  			expectedMisses: []storage.SeriesRef{1, 2},
   223  		},
   224  		"should return no misses on 100% hit ratio": {
   225  			setup: []mockedSeries{
   226  				{block: block1, id: 1, value: value1},
   227  				{block: block1, id: 2, value: value2},
   228  				{block: block2, id: 1, value: value3},
   229  			},
   230  			fetchBlockID: block1,
   231  			fetchIds:     []storage.SeriesRef{1, 2},
   232  			expectedHits: map[storage.SeriesRef][]byte{
   233  				1: value1,
   234  				2: value2,
   235  			},
   236  			expectedMisses: nil,
   237  		},
   238  		"should return hits and misses on partial hits": {
   239  			setup: []mockedSeries{
   240  				{block: block1, id: 1, value: value1},
   241  				{block: block2, id: 1, value: value3},
   242  			},
   243  			fetchBlockID:   block1,
   244  			fetchIds:       []storage.SeriesRef{1, 2},
   245  			expectedHits:   map[storage.SeriesRef][]byte{1: value1},
   246  			expectedMisses: []storage.SeriesRef{2},
   247  		},
   248  		"should return no hits on memcached error": {
   249  			setup: []mockedSeries{
   250  				{block: block1, id: 1, value: value1},
   251  				{block: block1, id: 2, value: value2},
   252  				{block: block2, id: 1, value: value3},
   253  			},
   254  			mockedErr:      errors.New("mocked error"),
   255  			fetchBlockID:   block1,
   256  			fetchIds:       []storage.SeriesRef{1, 2},
   257  			expectedHits:   nil,
   258  			expectedMisses: []storage.SeriesRef{1, 2},
   259  		},
   260  	}
   261  
   262  	for testName, testData := range tests {
   263  		t.Run(testName, func(t *testing.T) {
   264  			memcached := newMockedMemcachedClient(testData.mockedErr)
   265  			c, err := NewRemoteIndexCache(log.NewNopLogger(), memcached, nil, nil)
   266  			testutil.Ok(t, err)
   267  
   268  			// Store the series expected before running the test.
   269  			ctx := context.Background()
   270  			for _, p := range testData.setup {
   271  				c.StoreSeries(p.block, p.id, p.value)
   272  			}
   273  
   274  			// Fetch series from cached and assert on it.
   275  			hits, misses := c.FetchMultiSeries(ctx, testData.fetchBlockID, testData.fetchIds)
   276  			testutil.Equals(t, testData.expectedHits, hits)
   277  			testutil.Equals(t, testData.expectedMisses, misses)
   278  
   279  			// Assert on metrics.
   280  			testutil.Equals(t, float64(len(testData.fetchIds)), prom_testutil.ToFloat64(c.seriesRequests))
   281  			testutil.Equals(t, float64(len(testData.expectedHits)), prom_testutil.ToFloat64(c.seriesHits))
   282  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.postingRequests))
   283  			testutil.Equals(t, 0.0, prom_testutil.ToFloat64(c.postingHits))
   284  		})
   285  	}
   286  }
   287  
   288  type mockedPostings struct {
   289  	block ulid.ULID
   290  	label labels.Label
   291  	value []byte
   292  }
   293  
   294  type mockedExpandedPostings struct {
   295  	block    ulid.ULID
   296  	matchers []*labels.Matcher
   297  	value    []byte
   298  }
   299  
   300  type mockedSeries struct {
   301  	block ulid.ULID
   302  	id    storage.SeriesRef
   303  	value []byte
   304  }
   305  
   306  type mockedMemcachedClient struct {
   307  	cache             map[string][]byte
   308  	mockedGetMultiErr error
   309  }
   310  
   311  func newMockedMemcachedClient(mockedGetMultiErr error) *mockedMemcachedClient {
   312  	return &mockedMemcachedClient{
   313  		cache:             map[string][]byte{},
   314  		mockedGetMultiErr: mockedGetMultiErr,
   315  	}
   316  }
   317  
   318  func (c *mockedMemcachedClient) GetMulti(ctx context.Context, keys []string) map[string][]byte {
   319  	if c.mockedGetMultiErr != nil {
   320  		return nil
   321  	}
   322  
   323  	hits := map[string][]byte{}
   324  
   325  	for _, key := range keys {
   326  		if value, ok := c.cache[key]; ok {
   327  			hits[key] = value
   328  		}
   329  	}
   330  
   331  	return hits
   332  }
   333  
   334  func (c *mockedMemcachedClient) SetAsync(key string, value []byte, ttl time.Duration) error {
   335  	c.cache[key] = value
   336  
   337  	return nil
   338  }
   339  
   340  func (c *mockedMemcachedClient) Stop() {
   341  	// Nothing to do.
   342  }