github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/indexshipper/downloads/table_manager_test.go (about) 1 package downloads 2 3 import ( 4 "context" 5 "fmt" 6 "math" 7 "path/filepath" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/require" 12 13 "github.com/grafana/loki/pkg/storage/chunk/client/local" 14 "github.com/grafana/loki/pkg/storage/config" 15 "github.com/grafana/loki/pkg/storage/stores/indexshipper/index" 16 "github.com/grafana/loki/pkg/storage/stores/indexshipper/storage" 17 "github.com/grafana/loki/pkg/validation" 18 ) 19 20 const ( 21 objectsStorageDirName = "objects" 22 cacheDirName = "cache" 23 ) 24 25 func buildTestStorageClient(t *testing.T, path string) storage.Client { 26 objectStoragePath := filepath.Join(path, objectsStorageDirName) 27 fsObjectClient, err := local.NewFSObjectClient(local.FSConfig{Directory: objectStoragePath}) 28 require.NoError(t, err) 29 30 return storage.NewIndexStorageClient(fsObjectClient, "") 31 } 32 33 type stopFunc func() 34 35 func buildTestTableManager(t *testing.T, path string, tableRangesToHandle config.TableRanges) (*tableManager, stopFunc) { 36 indexStorageClient := buildTestStorageClient(t, path) 37 cachePath := filepath.Join(path, cacheDirName) 38 39 cfg := Config{ 40 CacheDir: cachePath, 41 SyncInterval: time.Hour, 42 CacheTTL: time.Hour, 43 Limits: &mockLimits{}, 44 } 45 46 if tableRangesToHandle == nil { 47 tableRangesToHandle = config.TableRanges{ 48 { 49 Start: 0, 50 End: math.MaxInt64, 51 PeriodConfig: &config.PeriodConfig{}, 52 }, 53 } 54 } 55 tblManager, err := NewTableManager(cfg, func(s string) (index.Index, error) { 56 return openMockIndexFile(t, s), nil 57 }, indexStorageClient, nil, tableRangesToHandle, nil) 58 require.NoError(t, err) 59 60 return tblManager.(*tableManager), func() { 61 tblManager.Stop() 62 } 63 } 64 65 func TestTableManager_ForEach(t *testing.T) { 66 tempDir := t.TempDir() 67 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 68 69 tables := []string{"table1", "table2"} 70 users := []string{"", "user1"} 71 for _, tableName := range tables { 72 for _, userID := range users { 73 setupIndexesAtPath(t, userID, filepath.Join(objectStoragePath, tableName, userID), 1, 5) 74 } 75 } 76 77 tableManager, stopFunc := buildTestTableManager(t, tempDir, nil) 78 defer stopFunc() 79 80 for _, tableName := range tables { 81 for i, userID := range []string{"user1", "common-index-user"} { 82 expectedIndexes := buildListOfExpectedIndexes("", 1, 5) 83 if i == 0 { 84 expectedIndexes = append(expectedIndexes, buildListOfExpectedIndexes(userID, 1, 5)...) 85 } 86 verifyIndexForEach(t, expectedIndexes, func(callbackFunc index.ForEachIndexCallback) error { 87 return tableManager.ForEach(context.Background(), tableName, userID, callbackFunc) 88 }) 89 } 90 } 91 } 92 93 func TestTableManager_cleanupCache(t *testing.T) { 94 tempDir := t.TempDir() 95 96 tableManager, stopFunc := buildTestTableManager(t, tempDir, nil) 97 defer stopFunc() 98 99 // one table that would expire and other one won't 100 expiredTableName := "expired-table" 101 nonExpiredTableName := "non-expired-table" 102 103 tableManager.tables[expiredTableName] = &mockTable{} 104 tableManager.tables[nonExpiredTableName] = &mockTable{} 105 106 // call cleanupCache and verify that no tables are cleaned up because they are not yet expired. 107 require.NoError(t, tableManager.cleanupCache()) 108 require.Len(t, tableManager.tables, 2) 109 110 // set the flag for expiredTable to expire. 111 tableManager.tables[expiredTableName].(*mockTable).tableExpired = true 112 113 // call the cleanupCache and verify that we still have nonExpiredTable and expiredTable is gone. 114 require.NoError(t, tableManager.cleanupCache()) 115 require.Len(t, tableManager.tables, 1) 116 117 _, ok := tableManager.tables[expiredTableName] 118 require.False(t, ok) 119 120 _, ok = tableManager.tables[nonExpiredTableName] 121 require.True(t, ok) 122 } 123 124 func TestTableManager_ensureQueryReadiness(t *testing.T) { 125 mockIndexStorageClient := &mockIndexStorageClient{ 126 userIndexesInTables: map[string][]string{}, 127 } 128 129 cfg := Config{ 130 SyncInterval: time.Hour, 131 CacheTTL: time.Hour, 132 } 133 134 tableManager := &tableManager{ 135 cfg: cfg, 136 indexStorageClient: mockIndexStorageClient, 137 tables: make(map[string]Table), 138 tableRangesToHandle: config.TableRanges{{ 139 Start: 0, End: math.MaxInt64, PeriodConfig: &config.PeriodConfig{}, 140 }}, 141 ctx: context.Background(), 142 cancel: func() {}, 143 } 144 145 // setup 10 tables with 5 latest tables having user index for user1 and user2 146 for i := 0; i < 10; i++ { 147 tableName := buildTableName(i) 148 tableManager.tables[tableName] = &mockTable{} 149 mockIndexStorageClient.tablesInStorage = append(mockIndexStorageClient.tablesInStorage, tableName) 150 if i < 5 { 151 mockIndexStorageClient.userIndexesInTables[tableName] = []string{"user1", "user2"} 152 } 153 } 154 155 // function for resetting state of mockTables 156 resetTables := func() { 157 for _, table := range tableManager.tables { 158 table.(*mockTable).queryReadinessDoneForUsers = nil 159 } 160 } 161 162 for _, tc := range []struct { 163 name string 164 queryReadyNumDaysCfg int 165 queryReadinessLimits mockLimits 166 tableRangesToHandle config.TableRanges 167 168 expectedQueryReadinessDoneForUsers map[string][]string 169 }{ 170 // includes whole table range 171 { 172 name: "no query readiness configured", 173 queryReadinessLimits: mockLimits{}, 174 }, 175 { 176 name: "common index: 5 days", 177 queryReadyNumDaysCfg: 5, 178 expectedQueryReadinessDoneForUsers: map[string][]string{ 179 buildTableName(0): {}, 180 buildTableName(1): {}, 181 buildTableName(2): {}, 182 buildTableName(3): {}, 183 buildTableName(4): {}, 184 buildTableName(5): {}, // NOTE: we include an extra table since we are counting days back from current point in time 185 }, 186 }, 187 { 188 name: "common index: 20 days", 189 queryReadyNumDaysCfg: 20, 190 expectedQueryReadinessDoneForUsers: map[string][]string{ 191 buildTableName(0): {}, 192 buildTableName(1): {}, 193 buildTableName(2): {}, 194 buildTableName(3): {}, 195 buildTableName(4): {}, 196 buildTableName(5): {}, 197 buildTableName(6): {}, 198 buildTableName(7): {}, 199 buildTableName(8): {}, 200 buildTableName(9): {}, 201 }, 202 }, 203 { 204 name: "user index default: 2 days", 205 queryReadinessLimits: mockLimits{ 206 queryReadyIndexNumDaysDefault: 2, 207 }, 208 expectedQueryReadinessDoneForUsers: map[string][]string{ 209 buildTableName(0): {"user1", "user2"}, 210 buildTableName(1): {"user1", "user2"}, 211 buildTableName(2): {"user1", "user2"}, 212 }, 213 }, 214 { 215 name: "common index: 5 days, user index default: 2 days", 216 queryReadinessLimits: mockLimits{ 217 queryReadyIndexNumDaysDefault: 2, 218 }, 219 queryReadyNumDaysCfg: 5, 220 expectedQueryReadinessDoneForUsers: map[string][]string{ 221 buildTableName(0): {"user1", "user2"}, 222 buildTableName(1): {"user1", "user2"}, 223 buildTableName(2): {"user1", "user2"}, 224 buildTableName(3): {}, 225 buildTableName(4): {}, 226 buildTableName(5): {}, 227 }, 228 }, 229 { 230 name: "user1: 2 days", 231 queryReadinessLimits: mockLimits{ 232 queryReadyIndexNumDaysByUser: map[string]int{"user1": 2}, 233 }, 234 expectedQueryReadinessDoneForUsers: map[string][]string{ 235 buildTableName(0): {"user1"}, 236 buildTableName(1): {"user1"}, 237 buildTableName(2): {"user1"}, 238 }, 239 }, 240 { 241 name: "user1: 2 days, user2: 20 days", 242 queryReadinessLimits: mockLimits{ 243 queryReadyIndexNumDaysByUser: map[string]int{"user1": 2, "user2": 20}, 244 }, 245 expectedQueryReadinessDoneForUsers: map[string][]string{ 246 buildTableName(0): {"user1", "user2"}, 247 buildTableName(1): {"user1", "user2"}, 248 buildTableName(2): {"user1", "user2"}, 249 buildTableName(3): {"user2"}, 250 buildTableName(4): {"user2"}, 251 }, 252 }, 253 { 254 name: "user index default: 3 days, user1: 2 days", 255 queryReadinessLimits: mockLimits{ 256 queryReadyIndexNumDaysDefault: 3, 257 queryReadyIndexNumDaysByUser: map[string]int{"user1": 2}, 258 }, 259 expectedQueryReadinessDoneForUsers: map[string][]string{ 260 buildTableName(0): {"user1", "user2"}, 261 buildTableName(1): {"user1", "user2"}, 262 buildTableName(2): {"user1", "user2"}, 263 buildTableName(3): {"user2"}, 264 }, 265 }, 266 // includes limited table range 267 { 268 name: "common index: 20 days", 269 queryReadyNumDaysCfg: 20, 270 tableRangesToHandle: config.TableRanges{ 271 { 272 End: buildTableNumber(0), 273 Start: buildTableNumber(4), 274 PeriodConfig: &config.PeriodConfig{}, 275 }, 276 { 277 End: buildTableNumber(7), 278 Start: buildTableNumber(9), 279 PeriodConfig: &config.PeriodConfig{}, 280 }, 281 }, 282 expectedQueryReadinessDoneForUsers: map[string][]string{ 283 buildTableName(0): {}, 284 buildTableName(1): {}, 285 buildTableName(2): {}, 286 buildTableName(3): {}, 287 buildTableName(4): {}, 288 289 buildTableName(7): {}, 290 buildTableName(8): {}, 291 buildTableName(9): {}, 292 }, 293 }, 294 { 295 name: "common index: 5 days, user index default: 2 days", 296 queryReadinessLimits: mockLimits{ 297 queryReadyIndexNumDaysDefault: 2, 298 }, 299 queryReadyNumDaysCfg: 5, 300 tableRangesToHandle: config.TableRanges{ 301 { 302 End: buildTableNumber(0), 303 Start: buildTableNumber(1), 304 PeriodConfig: &config.PeriodConfig{}, 305 }, 306 { 307 End: buildTableNumber(4), 308 Start: buildTableNumber(5), 309 PeriodConfig: &config.PeriodConfig{}, 310 }, 311 }, 312 expectedQueryReadinessDoneForUsers: map[string][]string{ 313 buildTableName(0): {"user1", "user2"}, 314 buildTableName(1): {"user1", "user2"}, 315 buildTableName(4): {}, 316 buildTableName(5): {}, 317 }, 318 }, 319 } { 320 t.Run(tc.name, func(t *testing.T) { 321 tc := tc // just to make the linter happy 322 resetTables() 323 tableManager.cfg.QueryReadyNumDays = tc.queryReadyNumDaysCfg 324 tableManager.cfg.Limits = &tc.queryReadinessLimits 325 if tc.tableRangesToHandle == nil { 326 tableManager.tableRangesToHandle = config.TableRanges{{ 327 Start: 0, End: math.MaxInt64, PeriodConfig: &config.PeriodConfig{}, 328 }} 329 } else { 330 tableManager.tableRangesToHandle = tc.tableRangesToHandle 331 } 332 require.NoError(t, tableManager.ensureQueryReadiness(context.Background())) 333 334 for name, table := range tableManager.tables { 335 require.Equal(t, tc.expectedQueryReadinessDoneForUsers[name], table.(*mockTable).queryReadinessDoneForUsers, "table: %s", name) 336 } 337 }) 338 } 339 } 340 341 func TestTableManager_loadTables(t *testing.T) { 342 tempDir := t.TempDir() 343 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 344 cachePath := filepath.Join(tempDir, cacheDirName) 345 346 var tables []string 347 for i := 0; i < 10; i++ { 348 tables = append(tables, buildTableName(i)) 349 } 350 users := []string{"", "user1"} 351 for _, tableName := range tables { 352 for _, userID := range users { 353 setupIndexesAtPath(t, userID, filepath.Join(objectStoragePath, tableName, userID), 1, 5) 354 setupIndexesAtPath(t, userID, filepath.Join(cachePath, tableName, userID), 1, 5) 355 } 356 } 357 358 verifyTables := func(tableManager *tableManager, tables []string) { 359 for _, tableName := range tables { 360 for i, userID := range []string{"user1", "common-index-user"} { 361 expectedIndexes := buildListOfExpectedIndexes("", 1, 5) 362 if i == 0 { 363 expectedIndexes = append(expectedIndexes, buildListOfExpectedIndexes(userID, 1, 5)...) 364 } 365 verifyIndexForEach(t, expectedIndexes, func(callbackFunc index.ForEachIndexCallback) error { 366 return tableManager.ForEach(context.Background(), tableName, userID, callbackFunc) 367 }) 368 } 369 } 370 } 371 372 tableManager, stopFunc := buildTestTableManager(t, tempDir, nil) 373 require.Equal(t, len(tables), len(tableManager.tables)) 374 verifyTables(tableManager, tables) 375 376 stopFunc() 377 378 tableManager, stopFunc = buildTestTableManager(t, tempDir, config.TableRanges{ 379 { 380 End: buildTableNumber(0), 381 Start: buildTableNumber(1), 382 PeriodConfig: &config.PeriodConfig{}, 383 }, 384 { 385 End: buildTableNumber(5), 386 Start: buildTableNumber(8), 387 PeriodConfig: &config.PeriodConfig{}, 388 }, 389 }) 390 defer stopFunc() 391 require.Equal(t, 6, len(tableManager.tables)) 392 393 tables = []string{ 394 buildTableName(0), 395 buildTableName(1), 396 buildTableName(5), 397 buildTableName(6), 398 buildTableName(7), 399 buildTableName(8), 400 } 401 verifyTables(tableManager, tables) 402 } 403 404 type mockLimits struct { 405 queryReadyIndexNumDaysDefault int 406 queryReadyIndexNumDaysByUser map[string]int 407 } 408 409 func (m *mockLimits) AllByUserID() map[string]*validation.Limits { 410 allByUserID := map[string]*validation.Limits{} 411 for userID := range m.queryReadyIndexNumDaysByUser { 412 allByUserID[userID] = &validation.Limits{ 413 QueryReadyIndexNumDays: m.queryReadyIndexNumDaysByUser[userID], 414 } 415 } 416 417 return allByUserID 418 } 419 420 func (m *mockLimits) DefaultLimits() *validation.Limits { 421 return &validation.Limits{ 422 QueryReadyIndexNumDays: m.queryReadyIndexNumDaysDefault, 423 } 424 } 425 426 type mockTable struct { 427 tableExpired bool 428 queryReadinessDoneForUsers []string 429 } 430 431 func (m *mockTable) ForEach(ctx context.Context, userID string, callback index.ForEachIndexCallback) error { 432 return nil 433 } 434 435 func (m *mockTable) Close() {} 436 437 func (m *mockTable) DropUnusedIndex(ttl time.Duration, now time.Time) (bool, error) { 438 return m.tableExpired, nil 439 } 440 441 func (m *mockTable) Sync(ctx context.Context) error { 442 return nil 443 } 444 445 func (m *mockTable) EnsureQueryReadiness(ctx context.Context, userIDs []string) error { 446 m.queryReadinessDoneForUsers = userIDs 447 return nil 448 } 449 450 type mockIndexStorageClient struct { 451 storage.Client 452 tablesInStorage []string 453 userIndexesInTables map[string][]string 454 } 455 456 func (m *mockIndexStorageClient) ListTables(ctx context.Context) ([]string, error) { 457 return m.tablesInStorage, nil 458 } 459 460 func (m *mockIndexStorageClient) ListFiles(ctx context.Context, tableName string, bypassCache bool) ([]storage.IndexFile, []string, error) { 461 return []storage.IndexFile{}, m.userIndexesInTables[tableName], nil 462 } 463 464 func buildTableNumber(idx int) int64 { 465 return getActiveTableNumber() - int64(idx) 466 } 467 468 func buildTableName(idx int) string { 469 return fmt.Sprintf("table_%d", buildTableNumber(idx)) 470 }