github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/indexshipper/downloads/table_test.go (about) 1 package downloads 2 3 import ( 4 "context" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 "sort" 9 "testing" 10 "time" 11 12 "github.com/pkg/errors" 13 "github.com/stretchr/testify/require" 14 15 "github.com/grafana/loki/pkg/storage/stores/indexshipper/index" 16 "github.com/grafana/loki/pkg/storage/stores/indexshipper/storage" 17 util_log "github.com/grafana/loki/pkg/util/log" 18 ) 19 20 const ( 21 userID = "user-id" 22 tableName = "test" 23 ) 24 25 // storageClientWithFakeObjectsInList adds a fake object in the list call response which 26 // helps with testing the case where objects gets deleted in the middle of a Sync/Download operation due to compaction. 27 type storageClientWithFakeObjectsInList struct { 28 storage.Client 29 } 30 31 func newStorageClientWithFakeObjectsInList(storageClient storage.Client) storage.Client { 32 return storageClientWithFakeObjectsInList{storageClient} 33 } 34 35 func (o storageClientWithFakeObjectsInList) ListFiles(ctx context.Context, tableName string, bypassCache bool) ([]storage.IndexFile, []string, error) { 36 files, userIDs, err := o.Client.ListFiles(ctx, tableName, true) 37 if err != nil { 38 return nil, nil, err 39 } 40 41 files = append(files, storage.IndexFile{ 42 Name: "fake-object", 43 ModifiedAt: time.Now(), 44 }) 45 46 return files, userIDs, nil 47 } 48 49 func (o storageClientWithFakeObjectsInList) ListUserFiles(ctx context.Context, tableName, userID string, _ bool) ([]storage.IndexFile, error) { 50 files, err := o.Client.ListUserFiles(ctx, tableName, userID, true) 51 if err != nil { 52 return nil, err 53 } 54 55 files = append(files, storage.IndexFile{ 56 Name: "fake-object", 57 ModifiedAt: time.Now(), 58 }) 59 60 return files, nil 61 } 62 63 func buildTestTable(t *testing.T, path string) (*table, stopFunc) { 64 storageClient := buildTestStorageClient(t, path) 65 cachePath := filepath.Join(path, cacheDirName) 66 67 table := NewTable(tableName, cachePath, storageClient, func(path string) (index.Index, error) { 68 return openMockIndexFile(t, path), nil 69 }, newMetrics(nil)).(*table) 70 _, usersWithIndex, err := table.storageClient.ListFiles(context.Background(), tableName, false) 71 require.NoError(t, err) 72 require.NoError(t, table.EnsureQueryReadiness(context.Background(), usersWithIndex)) 73 74 return table, table.Close 75 } 76 77 type mockIndexSet struct { 78 IndexSet 79 indexes []index.Index 80 failQueries bool 81 lastUsedAt time.Time 82 } 83 84 func (m *mockIndexSet) ForEach(ctx context.Context, callback index.ForEachIndexCallback) error { 85 for _, idx := range m.indexes { 86 if err := callback(false, idx); err != nil { 87 return err 88 } 89 } 90 91 return nil 92 } 93 94 func (m *mockIndexSet) Err() error { 95 var err error 96 if m.failQueries { 97 err = errors.New("fail queries") 98 } 99 return err 100 } 101 102 func (m *mockIndexSet) DropAllDBs() error { 103 return nil 104 } 105 106 func (m *mockIndexSet) LastUsedAt() time.Time { 107 return m.lastUsedAt 108 } 109 110 func (m *mockIndexSet) UpdateLastUsedAt() { 111 m.lastUsedAt = time.Now() 112 } 113 114 func TestTable_ForEach(t *testing.T) { 115 usersToSetup := []string{"user1", "user2"} 116 for name, tc := range map[string]struct { 117 withError bool 118 withUserID string 119 }{ 120 "without error": { 121 withUserID: usersToSetup[0], 122 }, 123 "with error": { 124 withError: true, 125 withUserID: usersToSetup[0], 126 }, 127 "query with user2": { 128 withUserID: usersToSetup[1], 129 }, 130 } { 131 t.Run(name, func(t *testing.T) { 132 table := table{ 133 indexSets: map[string]IndexSet{}, 134 logger: util_log.Logger, 135 } 136 137 table.indexSets[""] = &mockIndexSet{} 138 for _, userID := range usersToSetup { 139 var testIndexes []index.Index 140 for _, indexPath := range setupIndexesAtPath(t, userID, t.TempDir(), 0, 5) { 141 testIndexes = append(testIndexes, openMockIndexFile(t, indexPath)) 142 } 143 table.indexSets[userID] = &mockIndexSet{ 144 failQueries: tc.withError, 145 indexes: testIndexes, 146 } 147 } 148 149 var indexesFound []index.Index 150 151 err := table.ForEach(context.Background(), tc.withUserID, func(_ bool, idx index.Index) error { 152 indexesFound = append(indexesFound, idx) 153 return nil 154 }) 155 if tc.withError { 156 require.Error(t, err) 157 require.Len(t, table.indexSets, len(usersToSetup)) 158 ensureIndexSetExistsInTable(t, &table, "") 159 for _, userID := range usersToSetup { 160 if userID != tc.withUserID { 161 ensureIndexSetExistsInTable(t, &table, userID) 162 } 163 } 164 } else { 165 require.NoError(t, err) 166 require.Len(t, table.indexSets, len(usersToSetup)+1) 167 require.Equal(t, table.indexSets[tc.withUserID].(*mockIndexSet).indexes, indexesFound) 168 } 169 }) 170 } 171 } 172 173 func TestTable_DropUnusedIndex(t *testing.T) { 174 ttl := 24 * time.Hour 175 now := time.Now() 176 notExpiredIndexUserID := "not-expired-user-based-index" 177 expiredIndexUserID := "expired-user-based-index" 178 179 // initialize some indexSets with indexSet for expiredIndexUserID being expired 180 indexSets := map[string]IndexSet{ 181 "": &mockIndexSet{lastUsedAt: time.Now()}, 182 notExpiredIndexUserID: &mockIndexSet{lastUsedAt: time.Now().Add(-time.Hour)}, 183 expiredIndexUserID: &mockIndexSet{lastUsedAt: now.Add(-25 * time.Hour)}, 184 } 185 186 table := table{ 187 indexSets: indexSets, 188 logger: util_log.Logger, 189 } 190 191 // ensure that we only find expiredIndexUserID to be dropped 192 require.Equal(t, []string{expiredIndexUserID}, table.findExpiredIndexSets(ttl, now)) 193 194 // dropping unused indexSets should drop only index set for expiredIndexUserID 195 allIndexSetsDropped, err := table.DropUnusedIndex(ttl, now) 196 require.NoError(t, err) 197 require.False(t, allIndexSetsDropped) 198 199 // verify that we only dropped index set for expiredIndexUserID 200 require.Len(t, table.indexSets, 2) 201 ensureIndexSetExistsInTable(t, &table, "") 202 ensureIndexSetExistsInTable(t, &table, notExpiredIndexUserID) 203 204 // change the lastUsedAt for common index set to expire it 205 indexSets[""].(*mockIndexSet).lastUsedAt = now.Add(-25 * time.Hour) 206 207 // common index set should not get dropped since we still have notExpiredIndexUserID which is not expired 208 require.Equal(t, []string(nil), table.findExpiredIndexSets(ttl, now)) 209 allIndexSetsDropped, err = table.DropUnusedIndex(ttl, now) 210 require.NoError(t, err) 211 require.False(t, allIndexSetsDropped) 212 213 // none of the index set should be dropped 214 require.Len(t, table.indexSets, 2) 215 ensureIndexSetExistsInTable(t, &table, "") 216 ensureIndexSetExistsInTable(t, &table, notExpiredIndexUserID) 217 218 // change the lastUsedAt for all indexSets so that all of them get dropped 219 for _, indexSets := range table.indexSets { 220 indexSets.(*mockIndexSet).lastUsedAt = now.Add(-25 * time.Hour) 221 } 222 223 // ensure that we get userID of common index set at the end 224 require.Equal(t, []string{notExpiredIndexUserID, ""}, table.findExpiredIndexSets(ttl, now)) 225 226 allIndexSetsDropped, err = table.DropUnusedIndex(ttl, now) 227 require.NoError(t, err) 228 require.True(t, allIndexSetsDropped) 229 } 230 231 func TestTable_EnsureQueryReadiness(t *testing.T) { 232 tempDir := t.TempDir() 233 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 234 235 // setup table in storage with 1 common db and 2 users with a db each 236 tablePath := filepath.Join(objectStoragePath, tableName) 237 setupIndexesAtPath(t, "", tablePath, 0, 5) 238 usersToSetup := []string{"user1", "user2"} 239 for _, userID := range usersToSetup { 240 setupIndexesAtPath(t, userID, tablePath, 0, 5) 241 } 242 243 storageClient := buildTestStorageClient(t, tempDir) 244 245 for _, tc := range []struct { 246 name string 247 usersToDoQueryReadinessFor []string 248 }{ 249 { 250 name: "only common index to be query ready", 251 }, 252 { 253 name: "one of the users to be query ready", 254 usersToDoQueryReadinessFor: []string{"user-1"}, 255 }, 256 } { 257 t.Run(tc.name, func(t *testing.T) { 258 cachePath := t.TempDir() 259 table := NewTable(tableName, cachePath, storageClient, func(path string) (index.Index, error) { 260 return openMockIndexFile(t, path), nil 261 }, newMetrics(nil)).(*table) 262 defer func() { 263 table.Close() 264 }() 265 266 // EnsureQueryReadiness should update the last used at time of common index set 267 require.NoError(t, table.EnsureQueryReadiness(context.Background(), tc.usersToDoQueryReadinessFor)) 268 require.Len(t, table.indexSets, len(tc.usersToDoQueryReadinessFor)+1) 269 for _, userID := range append(tc.usersToDoQueryReadinessFor, "") { 270 ensureIndexSetExistsInTable(t, table, userID) 271 require.InDelta(t, time.Now().Unix(), table.indexSets[userID].(*indexSet).lastUsedAt.Unix(), 5) 272 } 273 274 // change the last used at to verify that it gets updated when we do the query readiness again 275 for _, idxSet := range table.indexSets { 276 idxSet.(*indexSet).lastUsedAt = time.Now().Add(-time.Hour) 277 } 278 279 // Running it multiple times should not have an impact other than updating last used at time 280 for i := 0; i < 2; i++ { 281 require.NoError(t, table.EnsureQueryReadiness(context.Background(), tc.usersToDoQueryReadinessFor)) 282 require.Len(t, table.indexSets, len(tc.usersToDoQueryReadinessFor)+1) 283 for _, userID := range append(tc.usersToDoQueryReadinessFor, "") { 284 ensureIndexSetExistsInTable(t, table, userID) 285 require.InDelta(t, time.Now().Unix(), table.indexSets[userID].(*indexSet).lastUsedAt.Unix(), 5) 286 } 287 } 288 }) 289 } 290 } 291 292 func TestTable_Sync(t *testing.T) { 293 tempDir := t.TempDir() 294 295 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 296 tablePathInStorage := filepath.Join(objectStoragePath, tableName) 297 298 // list of dbs to create except newDB that would be added later as part of updates 299 deleteDB := "delete" 300 noUpdatesDB := "no-updates" 301 newDB := "new" 302 303 require.NoError(t, os.MkdirAll(tablePathInStorage, 0755)) 304 require.NoError(t, ioutil.WriteFile(filepath.Join(tablePathInStorage, deleteDB), []byte(deleteDB), 0755)) 305 require.NoError(t, ioutil.WriteFile(filepath.Join(tablePathInStorage, noUpdatesDB), []byte(noUpdatesDB), 0755)) 306 307 // create table instance 308 table, stopFunc := buildTestTable(t, tempDir) 309 defer stopFunc() 310 311 // replace the storage client with the one that adds fake objects in the list call 312 table.storageClient = newStorageClientWithFakeObjectsInList(table.storageClient) 313 314 // check that table has expected indexes setup 315 var indexesFound []string 316 err := table.ForEach(context.Background(), userID, func(_ bool, idx index.Index) error { 317 indexesFound = append(indexesFound, idx.Name()) 318 return nil 319 }) 320 require.NoError(t, err) 321 sort.Strings(indexesFound) 322 require.Equal(t, []string{deleteDB, noUpdatesDB}, indexesFound) 323 324 // add a sleep since we are updating a file and CI is sometimes too fast to create a difference in mtime of files 325 time.Sleep(time.Second) 326 327 // remove deleteDB and add the newDB 328 require.NoError(t, os.Remove(filepath.Join(tablePathInStorage, deleteDB))) 329 require.NoError(t, ioutil.WriteFile(filepath.Join(tablePathInStorage, newDB), []byte(newDB), 0755)) 330 331 // sync the table 332 table.storageClient.RefreshIndexListCache(context.Background()) 333 require.NoError(t, table.Sync(context.Background())) 334 335 // check that table got the new index and dropped the deleted index 336 indexesFound = []string{} 337 err = table.ForEach(context.Background(), userID, func(_ bool, idx index.Index) error { 338 indexesFound = append(indexesFound, idx.Name()) 339 return nil 340 }) 341 require.NoError(t, err) 342 sort.Strings(indexesFound) 343 require.Equal(t, []string{newDB, noUpdatesDB}, indexesFound) 344 345 // verify files in cache where dbs for the table are synced to double check. 346 expectedFilesInDir := map[string]struct{}{ 347 noUpdatesDB: {}, 348 newDB: {}, 349 } 350 filesInfo, err := ioutil.ReadDir(tablePathInStorage) 351 require.NoError(t, err) 352 require.Len(t, table.indexSets[""].(*indexSet).index, len(expectedFilesInDir)) 353 354 for _, fileInfo := range filesInfo { 355 require.False(t, fileInfo.IsDir()) 356 _, ok := expectedFilesInDir[fileInfo.Name()] 357 require.True(t, ok) 358 } 359 360 // let us simulate a compaction to test stale index list cache handling 361 362 // first, let us add a new file and refresh the index list cache 363 oneMoreDB := "one-more-db" 364 require.NoError(t, ioutil.WriteFile(filepath.Join(tablePathInStorage, oneMoreDB), []byte(oneMoreDB), 0755)) 365 table.storageClient.RefreshIndexListCache(context.Background()) 366 367 // now, without syncing the table, let us compact the index in storage 368 compactedDBName := "compacted-db" 369 require.NoError(t, ioutil.WriteFile(filepath.Join(tablePathInStorage, compactedDBName), []byte(compactedDBName), 0755)) 370 require.NoError(t, os.Remove(filepath.Join(tablePathInStorage, noUpdatesDB))) 371 require.NoError(t, os.Remove(filepath.Join(tablePathInStorage, newDB))) 372 require.NoError(t, os.Remove(filepath.Join(tablePathInStorage, oneMoreDB))) 373 374 // let us run a sync which should detect the stale index list cache and sync the table after refreshing the cache 375 require.NoError(t, table.Sync(context.Background())) 376 377 // verify that table has got only compacted db 378 indexesFound = []string{} 379 err = table.ForEach(context.Background(), userID, func(_ bool, idx index.Index) error { 380 indexesFound = append(indexesFound, idx.Name()) 381 return nil 382 }) 383 require.NoError(t, err) 384 sort.Strings(indexesFound) 385 require.Equal(t, []string{compactedDBName}, indexesFound) 386 } 387 388 func TestLoadTable(t *testing.T) { 389 tempDir := t.TempDir() 390 391 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 392 tablePathInStorage := filepath.Join(objectStoragePath, tableName) 393 394 // setup the table in storage with some records 395 setupIndexesAtPath(t, "", tablePathInStorage, 0, 5) 396 setupIndexesAtPath(t, userID, filepath.Join(tablePathInStorage, userID), 0, 5) 397 398 storageClient := buildTestStorageClient(t, tempDir) 399 tablePathInCache := filepath.Join(tempDir, cacheDirName, tableName) 400 401 storageClient = newStorageClientWithFakeObjectsInList(storageClient) 402 403 // try loading the table. 404 table, err := LoadTable(tableName, tablePathInCache, storageClient, func(path string) (index.Index, error) { 405 return openMockIndexFile(t, path), nil 406 }, newMetrics(nil)) 407 require.NoError(t, err) 408 require.NotNil(t, table) 409 410 // check the loaded table to see it has right index files. 411 expectedIndexes := append(buildListOfExpectedIndexes(userID, 0, 5), buildListOfExpectedIndexes("", 0, 5)...) 412 verifyIndexForEach(t, expectedIndexes, func(callbackFunc index.ForEachIndexCallback) error { 413 return table.ForEach(context.Background(), userID, callbackFunc) 414 }) 415 416 // close the table to test reloading of table with already having files in the cache dir. 417 table.Close() 418 419 // add some more files to the storage. 420 setupIndexesAtPath(t, "", tablePathInStorage, 5, 10) 421 setupIndexesAtPath(t, userID, filepath.Join(tablePathInStorage, userID), 5, 10) 422 423 // try loading the table, it should skip loading corrupt file and reload it from storage. 424 table, err = LoadTable(tableName, tablePathInCache, storageClient, func(path string) (index.Index, error) { 425 return openMockIndexFile(t, path), nil 426 }, newMetrics(nil)) 427 require.NoError(t, err) 428 require.NotNil(t, table) 429 430 defer table.Close() 431 432 expectedIndexes = append(buildListOfExpectedIndexes(userID, 0, 10), buildListOfExpectedIndexes("", 0, 10)...) 433 verifyIndexForEach(t, expectedIndexes, func(callbackFunc index.ForEachIndexCallback) error { 434 return table.ForEach(context.Background(), userID, callbackFunc) 435 }) 436 } 437 438 func buildListOfExpectedIndexes(userID string, start, end int) []string { 439 var expectedIndexes []string 440 for ; start < end; start++ { 441 expectedIndexes = append(expectedIndexes, buildIndexFilename(userID, start)) 442 } 443 444 return expectedIndexes 445 } 446 447 func ensureIndexSetExistsInTable(t *testing.T, table *table, indexSetName string) { 448 _, ok := table.indexSets[indexSetName] 449 require.True(t, ok) 450 } 451 452 func verifyIndexForEach(t *testing.T, expectedIndexes []string, forEachFunc func(callbackFunc index.ForEachIndexCallback) error) { 453 var indexesFound []string 454 err := forEachFunc(func(_ bool, idx index.Index) error { 455 // get the reader for the index. 456 readSeeker, err := idx.Reader() 457 require.NoError(t, err) 458 459 // seek it to 0 460 _, err = readSeeker.Seek(0, 0) 461 require.NoError(t, err) 462 463 // read the contents of the index. 464 buf, err := ioutil.ReadAll(readSeeker) 465 require.NoError(t, err) 466 467 // see if it matches the name of the file 468 require.Equal(t, idx.Name(), string(buf)) 469 470 indexesFound = append(indexesFound, idx.Name()) 471 return nil 472 }) 473 require.NoError(t, err) 474 475 sort.Strings(indexesFound) 476 sort.Strings(expectedIndexes) 477 require.Equal(t, expectedIndexes, indexesFound) 478 }