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 }