github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/index/postings_list_cache_test.go (about) 1 // Copyright (c) 2019 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package index 22 23 import ( 24 "fmt" 25 "sort" 26 "strconv" 27 "sync" 28 "testing" 29 30 "github.com/m3db/m3/src/m3ninx/postings" 31 "github.com/m3db/m3/src/m3ninx/postings/roaring" 32 "github.com/m3db/m3/src/x/instrument" 33 34 "github.com/pborman/uuid" 35 "github.com/stretchr/testify/require" 36 ) 37 38 const ( 39 numTestPlEntries = 1000 40 ) 41 42 var ( 43 // Filled in by init(). 44 testPlEntries []testEntry 45 testPostingListCacheOptions = PostingsListCacheOptions{ 46 InstrumentOptions: instrument.NewOptions(), 47 } 48 ) 49 50 func init() { 51 // Generate test data. 52 for i := 0; i < numTestPlEntries; i++ { 53 var ( 54 segmentUUID = uuid.Parse( 55 fmt.Sprintf("00000000-0000-0000-0000-000000000%03d", i)) 56 57 field = fmt.Sprintf("field_%d", i) 58 pattern = fmt.Sprintf("pattern_%d", i) 59 pl = roaring.NewPostingsList() 60 ) 61 pl.Insert(postings.ID(i)) 62 63 patternType := PatternTypeRegexp 64 switch i % 3 { 65 case 0: 66 patternType = PatternTypeTerm 67 case 1: 68 patternType = PatternTypeField 69 pattern = "" // field queries don't have patterns 70 } 71 72 testPlEntries = append(testPlEntries, testEntry{ 73 segmentUUID: segmentUUID, 74 key: newKey(field, pattern, patternType), 75 postingsList: pl, 76 }) 77 } 78 } 79 80 type testEntry struct { 81 segmentUUID uuid.UUID 82 key PostingsListCacheKey 83 postingsList postings.List 84 } 85 86 func TestSimpleLRUBehavior(t *testing.T) { 87 size := 3 88 plCache, err := NewPostingsListCache(size, testPostingListCacheOptions) 89 require.NoError(t, err) 90 91 var ( 92 e0 = testPlEntries[0] 93 e1 = testPlEntries[1] 94 e2 = testPlEntries[2] 95 e3 = testPlEntries[3] 96 e4 = testPlEntries[4] 97 e5 = testPlEntries[5] 98 ) 99 putEntry(t, plCache, 0) 100 putEntry(t, plCache, 1) 101 putEntry(t, plCache, 2) 102 requireExpectedOrder(t, plCache, []testEntry{e0, e1, e2}) 103 104 putEntry(t, plCache, 3) 105 requireExpectedOrder(t, plCache, []testEntry{e1, e2, e3}) 106 107 putEntry(t, plCache, 4) 108 putEntry(t, plCache, 4) 109 putEntry(t, plCache, 5) 110 putEntry(t, plCache, 5) 111 putEntry(t, plCache, 0) 112 putEntry(t, plCache, 0) 113 requireExpectedOrder(t, plCache, []testEntry{e4, e5, e0}) 114 115 // Miss, no expected change. 116 getEntry(t, plCache, 100) 117 requireExpectedOrder(t, plCache, []testEntry{e4, e5, e0}) 118 119 // Hit. 120 getEntry(t, plCache, 4) 121 requireExpectedOrder(t, plCache, []testEntry{e5, e0, e4}) 122 123 // Multiple hits. 124 getEntry(t, plCache, 4) 125 getEntry(t, plCache, 0) 126 getEntry(t, plCache, 5) 127 getEntry(t, plCache, 5) 128 requireExpectedOrder(t, plCache, []testEntry{e4, e0, e5}) 129 } 130 131 func TestPurgeSegment(t *testing.T) { 132 size := len(testPlEntries) 133 plCache, err := NewPostingsListCache(size, testPostingListCacheOptions) 134 require.NoError(t, err) 135 136 // Write many entries with the same segment UUID. 137 for i := 0; i < 100; i++ { 138 if testPlEntries[i].key.PatternType == PatternTypeRegexp { 139 plCache.PutRegexp( 140 testPlEntries[0].segmentUUID, 141 testPlEntries[i].key.Field, 142 testPlEntries[i].key.Pattern, 143 testPlEntries[i].postingsList, 144 ) 145 } else { 146 plCache.PutTerm( 147 testPlEntries[0].segmentUUID, 148 testPlEntries[i].key.Field, 149 testPlEntries[i].key.Pattern, 150 testPlEntries[i].postingsList, 151 ) 152 } 153 } 154 155 // Write the remaining entries. 156 for i := 100; i < len(testPlEntries); i++ { 157 putEntry(t, plCache, i) 158 } 159 160 // Purge all entries related to the segment. 161 plCache.PurgeSegment(testPlEntries[0].segmentUUID) 162 163 // All entries related to the purged segment should be gone. 164 require.Equal(t, size-100, plCache.lru.Len()) 165 for i := 0; i < 100; i++ { 166 if testPlEntries[i].key.PatternType == PatternTypeRegexp { 167 _, ok := plCache.GetRegexp( 168 testPlEntries[0].segmentUUID, 169 testPlEntries[i].key.Field, 170 testPlEntries[i].key.Pattern, 171 ) 172 require.False(t, ok) 173 } else { 174 _, ok := plCache.GetTerm( 175 testPlEntries[0].segmentUUID, 176 testPlEntries[i].key.Field, 177 testPlEntries[i].key.Pattern, 178 ) 179 require.False(t, ok) 180 } 181 } 182 183 // Remaining entries should still be present. 184 for i := 100; i < len(testPlEntries); i++ { 185 getEntry(t, plCache, i) 186 } 187 } 188 189 func TestEverthingInsertedCanBeRetrieved(t *testing.T) { 190 plCache, err := NewPostingsListCache(len(testPlEntries), testPostingListCacheOptions) 191 require.NoError(t, err) 192 193 for i := range testPlEntries { 194 putEntry(t, plCache, i) 195 } 196 197 for i, entry := range testPlEntries { 198 cached, ok := getEntry(t, plCache, i) 199 require.True(t, ok) 200 require.True(t, cached.Equal(entry.postingsList)) 201 } 202 } 203 204 func TestConcurrencyWithEviction(t *testing.T) { 205 testConcurrency(t, len(testPlEntries)/10, true, false) 206 } 207 208 func TestConcurrencyVerifyResultsNoEviction(t *testing.T) { 209 testConcurrency(t, len(testPlEntries), false, true) 210 } 211 212 func testConcurrency(t *testing.T, size int, purge bool, verify bool) { 213 plCache, err := NewPostingsListCache(size, testPostingListCacheOptions) 214 require.NoError(t, err) 215 216 wg := sync.WaitGroup{} 217 // Spin up writers. 218 for i := range testPlEntries { 219 wg.Add(1) 220 go func(i int) { 221 for j := 0; j < 100; j++ { 222 putEntry(t, plCache, i) 223 } 224 wg.Done() 225 }(i) 226 } 227 228 // Spin up readers. 229 for i := range testPlEntries { 230 wg.Add(1) 231 go func(i int) { 232 for j := 0; j < 100; j++ { 233 getEntry(t, plCache, j) 234 } 235 wg.Done() 236 }(i) 237 } 238 239 stopPurge := make(chan struct{}) 240 if purge { 241 go func() { 242 for { 243 select { 244 case <-stopPurge: 245 default: 246 for _, entry := range testPlEntries { 247 plCache.PurgeSegment(entry.segmentUUID) 248 } 249 } 250 } 251 }() 252 } 253 254 wg.Wait() 255 close(stopPurge) 256 257 if !verify { 258 return 259 } 260 261 for i, entry := range testPlEntries { 262 cached, ok := getEntry(t, plCache, i) 263 if !ok { 264 // Debug. 265 printSortedKeys(t, plCache) 266 } 267 require.True(t, ok) 268 require.True(t, cached.Equal(entry.postingsList)) 269 } 270 } 271 272 func putEntry(t *testing.T, cache *PostingsListCache, i int) { 273 // Do each put twice to test the logic that avoids storing 274 // multiple entries for the same value. 275 switch testPlEntries[i].key.PatternType { 276 case PatternTypeRegexp: 277 cache.PutRegexp( 278 testPlEntries[i].segmentUUID, 279 testPlEntries[i].key.Field, 280 testPlEntries[i].key.Pattern, 281 testPlEntries[i].postingsList, 282 ) 283 cache.PutRegexp( 284 testPlEntries[i].segmentUUID, 285 testPlEntries[i].key.Field, 286 testPlEntries[i].key.Pattern, 287 testPlEntries[i].postingsList, 288 ) 289 case PatternTypeTerm: 290 cache.PutTerm( 291 testPlEntries[i].segmentUUID, 292 testPlEntries[i].key.Field, 293 testPlEntries[i].key.Pattern, 294 testPlEntries[i].postingsList, 295 ) 296 cache.PutTerm( 297 testPlEntries[i].segmentUUID, 298 testPlEntries[i].key.Field, 299 testPlEntries[i].key.Pattern, 300 testPlEntries[i].postingsList, 301 ) 302 case PatternTypeField: 303 cache.PutField( 304 testPlEntries[i].segmentUUID, 305 testPlEntries[i].key.Field, 306 testPlEntries[i].postingsList, 307 ) 308 cache.PutField( 309 testPlEntries[i].segmentUUID, 310 testPlEntries[i].key.Field, 311 testPlEntries[i].postingsList, 312 ) 313 default: 314 require.FailNow(t, "unknown pattern type", testPlEntries[i].key.PatternType) 315 } 316 } 317 318 func getEntry(t *testing.T, cache *PostingsListCache, i int) (postings.List, bool) { 319 switch testPlEntries[i].key.PatternType { 320 case PatternTypeRegexp: 321 return cache.GetRegexp( 322 testPlEntries[i].segmentUUID, 323 testPlEntries[i].key.Field, 324 testPlEntries[i].key.Pattern, 325 ) 326 case PatternTypeTerm: 327 return cache.GetTerm( 328 testPlEntries[i].segmentUUID, 329 testPlEntries[i].key.Field, 330 testPlEntries[i].key.Pattern, 331 ) 332 case PatternTypeField: 333 return cache.GetField( 334 testPlEntries[i].segmentUUID, 335 testPlEntries[i].key.Field, 336 ) 337 default: 338 require.FailNow(t, "unknown pattern type", testPlEntries[i].key.PatternType) 339 } 340 return nil, false 341 } 342 343 func requireExpectedOrder(t *testing.T, plCache *PostingsListCache, expectedOrder []testEntry) { 344 for i, key := range plCache.lru.keys() { 345 require.Equal(t, expectedOrder[i].key, key) 346 } 347 } 348 349 func printSortedKeys(t *testing.T, cache *PostingsListCache) { 350 keys := cache.lru.keys() 351 sort.Slice(keys, func(i, j int) bool { 352 iIdx, err := strconv.ParseInt(keys[i].Field, 10, 64) 353 if err != nil { 354 t.Fatalf("unable to parse: %s into int", keys[i].Field) 355 } 356 357 jIdx, err := strconv.ParseInt(keys[j].Field, 10, 64) 358 if err != nil { 359 t.Fatalf("unable to parse: %s into int", keys[i].Field) 360 } 361 362 return iIdx < jIdx 363 }) 364 365 for _, key := range keys { 366 fmt.Println("key: ", key) 367 } 368 }