github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/tsdb/compactor_test.go (about) 1 package tsdb 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "path" 8 "path/filepath" 9 "strings" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/go-kit/log" 15 "github.com/prometheus/common/model" 16 "github.com/prometheus/prometheus/model/labels" 17 "github.com/stretchr/testify/require" 18 19 "github.com/grafana/loki/pkg/logproto" 20 "github.com/grafana/loki/pkg/storage/chunk" 21 "github.com/grafana/loki/pkg/storage/chunk/client" 22 "github.com/grafana/loki/pkg/storage/chunk/client/local" 23 "github.com/grafana/loki/pkg/storage/chunk/client/util" 24 "github.com/grafana/loki/pkg/storage/config" 25 "github.com/grafana/loki/pkg/storage/stores/indexshipper/compactor" 26 "github.com/grafana/loki/pkg/storage/stores/indexshipper/compactor/retention" 27 "github.com/grafana/loki/pkg/storage/stores/indexshipper/storage" 28 "github.com/grafana/loki/pkg/storage/stores/tsdb/index" 29 util_log "github.com/grafana/loki/pkg/util/log" 30 ) 31 32 const ( 33 objectsStorageDirName = "objects" 34 workingDirName = "working-dir" 35 ) 36 37 type mockIndexSet struct { 38 userID string 39 tableName string 40 workingDir string 41 sourceFiles []storage.IndexFile 42 objectClient client.ObjectClient 43 compactedIndex compactor.CompactedIndex 44 removeSourceFiles bool 45 } 46 47 func newMockIndexSet(userID, tableName, workingDir string, objectClient client.ObjectClient) (compactor.IndexSet, error) { 48 err := util.EnsureDirectory(workingDir) 49 if err != nil { 50 return nil, err 51 } 52 objects, _, err := objectClient.List(context.Background(), path.Join(tableName, userID), "/") 53 if err != nil { 54 return nil, err 55 } 56 57 sourceFiles := make([]storage.IndexFile, 0, len(objects)) 58 for _, obj := range objects { 59 sourceFiles = append(sourceFiles, storage.IndexFile{ 60 Name: path.Base(obj.Key), 61 ModifiedAt: obj.ModifiedAt, 62 }) 63 } 64 65 return &mockIndexSet{ 66 userID: userID, 67 tableName: tableName, 68 workingDir: workingDir, 69 sourceFiles: sourceFiles, 70 objectClient: objectClient, 71 }, nil 72 } 73 74 func (m *mockIndexSet) GetTableName() string { 75 return m.tableName 76 } 77 78 func (m *mockIndexSet) ListSourceFiles() []storage.IndexFile { 79 return m.sourceFiles 80 } 81 82 func (m *mockIndexSet) GetSourceFile(indexFile storage.IndexFile) (string, error) { 83 decompress := storage.IsCompressedFile(indexFile.Name) 84 dst := filepath.Join(m.workingDir, indexFile.Name) 85 if decompress { 86 dst = strings.Trim(dst, ".gz") 87 } 88 89 err := storage.DownloadFileFromStorage(dst, storage.IsCompressedFile(indexFile.Name), 90 false, storage.LoggerWithFilename(util_log.Logger, indexFile.Name), 91 func() (io.ReadCloser, error) { 92 rc, _, err := m.objectClient.GetObject(context.Background(), path.Join(m.tableName, m.userID, indexFile.Name)) 93 return rc, err 94 }) 95 if err != nil { 96 return "", err 97 } 98 99 return dst, nil 100 101 } 102 103 func (m *mockIndexSet) GetLogger() log.Logger { 104 return util_log.Logger 105 } 106 107 func (m *mockIndexSet) GetWorkingDir() string { 108 return m.workingDir 109 } 110 111 func (m *mockIndexSet) SetCompactedIndex(compactedIndex compactor.CompactedIndex, removeSourceFiles bool) error { 112 m.compactedIndex = compactedIndex 113 m.removeSourceFiles = removeSourceFiles 114 return nil 115 } 116 117 func setupMultiTenantIndex(t *testing.T, userStreams map[string][]stream, destDir string, ts time.Time) string { 118 require.NoError(t, util.EnsureDirectory(destDir)) 119 b := NewBuilder() 120 for userID, streams := range userStreams { 121 for _, stream := range streams { 122 lb := labels.NewBuilder(stream.labels) 123 lb.Set(TenantLabel, userID) 124 withTenant := lb.Labels() 125 126 b.AddSeries( 127 withTenant, 128 stream.fp, 129 stream.chunks, 130 ) 131 } 132 } 133 134 dst := newPrefixedIdentifier( 135 MultitenantTSDBIdentifier{ 136 nodeName: "test", 137 ts: ts, 138 }, 139 destDir, 140 "", 141 ) 142 143 _, err := b.Build( 144 context.Background(), 145 t.TempDir(), 146 func(from, through model.Time, checksum uint32) Identifier { 147 return dst 148 }, 149 ) 150 151 require.NoError(t, err) 152 return dst.Path() 153 } 154 155 func setupPerTenantIndex(t *testing.T, streams []stream, destDir string, ts time.Time) string { 156 require.NoError(t, util.EnsureDirectory(destDir)) 157 b := NewBuilder() 158 for _, stream := range streams { 159 b.AddSeries( 160 stream.labels, 161 stream.fp, 162 stream.chunks, 163 ) 164 } 165 166 id, err := b.Build( 167 context.Background(), 168 t.TempDir(), 169 func(from, through model.Time, checksum uint32) Identifier { 170 id := SingleTenantTSDBIdentifier{ 171 TS: ts, 172 From: from, 173 Through: through, 174 Checksum: checksum, 175 } 176 return newPrefixedIdentifier(id, destDir, "") 177 }, 178 ) 179 180 require.NoError(t, err) 181 return id.Path() 182 } 183 184 func buildStream(lbls labels.Labels, chunks index.ChunkMetas, userLabel string) stream { 185 if userLabel != "" { 186 lbls = labels.NewBuilder(lbls.Copy()).Set("user_id", userLabel).Labels() 187 } 188 return stream{ 189 labels: lbls, 190 fp: model.Fingerprint(lbls.Hash()), 191 chunks: chunks, 192 } 193 } 194 195 func buildChunkMetas(from, to int64) index.ChunkMetas { 196 var chunkMetas index.ChunkMetas 197 for i := from; i <= to; i++ { 198 chunkMetas = append(chunkMetas, index.ChunkMeta{ 199 MinTime: i, 200 MaxTime: i, 201 Checksum: uint32(i), 202 }) 203 } 204 205 return chunkMetas 206 } 207 208 func buildUserID(i int) string { 209 return fmt.Sprintf("user_%d", i) 210 } 211 212 type streamConfig struct { 213 labels labels.Labels 214 chunkMetas index.ChunkMetas 215 } 216 217 type multiTenantIndexConfig struct { 218 createdAt time.Time 219 streamsConfig []streamConfig 220 } 221 222 type perTenantIndexConfig struct { 223 createdAt time.Time 224 streamsConfig []streamConfig 225 } 226 227 func TestCompactor_Compact(t *testing.T) { 228 now := model.Now() 229 periodConfig := config.PeriodConfig{ 230 IndexTables: config.PeriodicTableConfig{Period: config.ObjectStorageIndexRequiredPeriod}, 231 Schema: "v12", 232 } 233 indexBkts, err := indexBuckets(now, now, []config.TableRange{periodConfig.GetIndexTableNumberRange(config.DayTime{Time: now})}) 234 require.NoError(t, err) 235 236 tableName := indexBkts[0] 237 lbls1 := mustParseLabels(`{foo="bar", a="b"}`) 238 lbls2 := mustParseLabels(`{fizz="buzz", a="b"}`) 239 240 for _, numUsers := range []int{5, 10, 20} { 241 t.Run(fmt.Sprintf("numUsers=%d", numUsers), func(t *testing.T) { 242 for name, tc := range map[string]struct { 243 multiTenantIndexConfigs []multiTenantIndexConfig 244 perTenantIndexConfigs []perTenantIndexConfig 245 246 expectedNumCompactedIndexes int 247 shouldRemoveCommonSourceIndexes bool 248 shouldRemoveUserSourceIndexes bool 249 expectedStreams []streamConfig 250 }{ 251 "no data in storage": {}, 252 "only one multi-tenant index file": { 253 expectedNumCompactedIndexes: numUsers, 254 shouldRemoveCommonSourceIndexes: true, 255 shouldRemoveUserSourceIndexes: true, 256 multiTenantIndexConfigs: []multiTenantIndexConfig{ 257 { 258 createdAt: time.Unix(0, 0), 259 streamsConfig: []streamConfig{ 260 { 261 labels: lbls1, 262 chunkMetas: buildChunkMetas(0, 5), 263 }, 264 }, 265 }, 266 }, 267 expectedStreams: []streamConfig{ 268 { 269 labels: lbls1, 270 chunkMetas: buildChunkMetas(0, 5), 271 }, 272 }, 273 }, 274 "multiple multi-tenant index files": { 275 expectedNumCompactedIndexes: numUsers, 276 shouldRemoveCommonSourceIndexes: true, 277 shouldRemoveUserSourceIndexes: true, 278 multiTenantIndexConfigs: []multiTenantIndexConfig{ 279 { 280 createdAt: time.Unix(0, 0), 281 streamsConfig: []streamConfig{ 282 { 283 labels: lbls1, 284 chunkMetas: buildChunkMetas(0, 5), 285 }, 286 { 287 labels: lbls2, 288 chunkMetas: buildChunkMetas(0, 5), 289 }, 290 }, 291 }, 292 { 293 createdAt: time.Unix(1, 0), 294 streamsConfig: []streamConfig{ 295 { 296 labels: lbls1, 297 chunkMetas: buildChunkMetas(0, 10), 298 }, 299 }, 300 }, 301 { 302 createdAt: time.Unix(2, 0), 303 streamsConfig: []streamConfig{ 304 { 305 labels: lbls2, 306 chunkMetas: buildChunkMetas(0, 10), 307 }, 308 }, 309 }, 310 }, 311 expectedStreams: []streamConfig{ 312 { 313 labels: lbls1, 314 chunkMetas: buildChunkMetas(0, 10), 315 }, 316 { 317 labels: lbls2, 318 chunkMetas: buildChunkMetas(0, 10), 319 }, 320 }, 321 }, 322 "both multi-tenant and per-tenant index files with no duplicates": { 323 expectedNumCompactedIndexes: numUsers, 324 shouldRemoveCommonSourceIndexes: true, 325 shouldRemoveUserSourceIndexes: true, 326 multiTenantIndexConfigs: []multiTenantIndexConfig{ 327 { 328 createdAt: time.Unix(0, 0), 329 streamsConfig: []streamConfig{ 330 { 331 labels: lbls1, 332 chunkMetas: buildChunkMetas(0, 5), 333 }, 334 { 335 labels: lbls2, 336 chunkMetas: buildChunkMetas(0, 5), 337 }, 338 }, 339 }, 340 }, 341 perTenantIndexConfigs: []perTenantIndexConfig{ 342 { 343 createdAt: time.Unix(0, 0), 344 streamsConfig: []streamConfig{ 345 { 346 labels: lbls1, 347 chunkMetas: buildChunkMetas(6, 10), 348 }, 349 { 350 labels: lbls2, 351 chunkMetas: buildChunkMetas(6, 10), 352 }, 353 }, 354 }, 355 }, 356 expectedStreams: []streamConfig{ 357 { 358 labels: lbls1, 359 chunkMetas: buildChunkMetas(0, 10), 360 }, 361 { 362 labels: lbls2, 363 chunkMetas: buildChunkMetas(0, 10), 364 }, 365 }, 366 }, 367 "both multi-tenant and per-tenant index files with duplicates": { 368 expectedNumCompactedIndexes: numUsers, 369 shouldRemoveCommonSourceIndexes: true, 370 shouldRemoveUserSourceIndexes: true, 371 multiTenantIndexConfigs: []multiTenantIndexConfig{ 372 { 373 createdAt: time.Unix(0, 0), 374 streamsConfig: []streamConfig{ 375 { 376 labels: lbls1, 377 chunkMetas: buildChunkMetas(0, 5), 378 }, 379 { 380 labels: lbls2, 381 chunkMetas: buildChunkMetas(0, 5), 382 }, 383 }, 384 }, 385 }, 386 perTenantIndexConfigs: []perTenantIndexConfig{ 387 { 388 createdAt: time.Unix(0, 0), 389 streamsConfig: []streamConfig{ 390 { 391 labels: lbls1, 392 chunkMetas: buildChunkMetas(0, 5), 393 }, 394 { 395 labels: lbls2, 396 chunkMetas: buildChunkMetas(0, 5), 397 }, 398 }, 399 }, 400 }, 401 expectedStreams: []streamConfig{ 402 { 403 labels: lbls1, 404 chunkMetas: buildChunkMetas(0, 5), 405 }, 406 { 407 labels: lbls2, 408 chunkMetas: buildChunkMetas(0, 5), 409 }, 410 }, 411 }, 412 "multiple per-tenant index files with no duplicates": { 413 expectedNumCompactedIndexes: numUsers, 414 shouldRemoveUserSourceIndexes: true, 415 perTenantIndexConfigs: []perTenantIndexConfig{ 416 { 417 createdAt: time.Unix(0, 0), 418 streamsConfig: []streamConfig{ 419 { 420 labels: lbls1, 421 chunkMetas: buildChunkMetas(0, 5), 422 }, 423 }, 424 }, 425 { 426 createdAt: time.Unix(1, 0), 427 streamsConfig: []streamConfig{ 428 { 429 labels: lbls1, 430 chunkMetas: buildChunkMetas(6, 10), 431 }, 432 }, 433 }, 434 }, 435 expectedStreams: []streamConfig{ 436 { 437 labels: lbls1, 438 chunkMetas: buildChunkMetas(0, 10), 439 }, 440 }, 441 }, 442 "multiple per-tenant index files with duplicates": { 443 expectedNumCompactedIndexes: numUsers, 444 shouldRemoveUserSourceIndexes: true, 445 perTenantIndexConfigs: []perTenantIndexConfig{ 446 { 447 createdAt: time.Unix(0, 0), 448 streamsConfig: []streamConfig{ 449 { 450 labels: lbls1, 451 chunkMetas: buildChunkMetas(0, 5), 452 }, 453 }, 454 }, 455 { 456 createdAt: time.Unix(1, 0), 457 streamsConfig: []streamConfig{ 458 { 459 labels: lbls1, 460 chunkMetas: buildChunkMetas(0, 5), 461 }, 462 }, 463 }, 464 }, 465 expectedStreams: []streamConfig{ 466 { 467 labels: lbls1, 468 chunkMetas: buildChunkMetas(0, 5), 469 }, 470 }, 471 }, 472 "nothing to compact": { 473 perTenantIndexConfigs: []perTenantIndexConfig{ 474 { 475 createdAt: time.Unix(0, 0), 476 streamsConfig: []streamConfig{ 477 { 478 labels: lbls1, 479 chunkMetas: buildChunkMetas(0, 5), 480 }, 481 }, 482 }, 483 }, 484 }, 485 } { 486 t.Run(name, func(t *testing.T) { 487 tempDir := t.TempDir() 488 objectStoragePath := filepath.Join(tempDir, objectsStorageDirName) 489 tablePathInStorage := filepath.Join(objectStoragePath, tableName) 490 tableWorkingDirectory := filepath.Join(tempDir, workingDirName, tableName) 491 492 require.NoError(t, util.EnsureDirectory(objectStoragePath)) 493 require.NoError(t, util.EnsureDirectory(tablePathInStorage)) 494 require.NoError(t, util.EnsureDirectory(tableWorkingDirectory)) 495 496 // setup multi-tenant indexes 497 for _, multiTenantIndexConfig := range tc.multiTenantIndexConfigs { 498 userStreams := map[string][]stream{} 499 for i := 0; i < numUsers; i++ { 500 userID := buildUserID(i) 501 userStreams[userID] = []stream{} 502 503 for _, streamConfig := range multiTenantIndexConfig.streamsConfig { 504 // unique stream for user with user_id label 505 stream := buildStream(streamConfig.labels, streamConfig.chunkMetas, userID) 506 userStreams[userID] = append(userStreams[userID], stream) 507 508 // without user_id label 509 stream = buildStream(streamConfig.labels, streamConfig.chunkMetas, "") 510 userStreams[userID] = append(userStreams[userID], stream) 511 } 512 } 513 setupMultiTenantIndex(t, userStreams, tablePathInStorage, multiTenantIndexConfig.createdAt) 514 } 515 516 // setup per-tenant indexes i.e compacted ones 517 for _, perTenantIndexConfig := range tc.perTenantIndexConfigs { 518 for i := 0; i < numUsers; i++ { 519 userID := buildUserID(i) 520 521 var streams []stream 522 for _, streamConfig := range perTenantIndexConfig.streamsConfig { 523 // unique stream for user with user_id label 524 stream := buildStream(streamConfig.labels, streamConfig.chunkMetas, userID) 525 streams = append(streams, stream) 526 527 // without user_id label 528 stream = buildStream(streamConfig.labels, streamConfig.chunkMetas, "") 529 streams = append(streams, stream) 530 } 531 setupPerTenantIndex(t, streams, filepath.Join(tablePathInStorage, userID), perTenantIndexConfig.createdAt) 532 } 533 } 534 535 // build the clients and index sets 536 objectClient, err := local.NewFSObjectClient(local.FSConfig{Directory: objectStoragePath}) 537 require.NoError(t, err) 538 539 _, commonPrefixes, err := objectClient.List(context.Background(), tableName, "/") 540 require.NoError(t, err) 541 542 initializedIndexSets := map[string]compactor.IndexSet{} 543 initializedIndexSetsMtx := sync.Mutex{} 544 existingUserIndexSets := make(map[string]compactor.IndexSet, len(commonPrefixes)) 545 for _, commonPrefix := range commonPrefixes { 546 userID := path.Base(string(commonPrefix)) 547 idxSet, err := newMockIndexSet(userID, tableName, filepath.Join(tableWorkingDirectory, userID), objectClient) 548 require.NoError(t, err) 549 550 existingUserIndexSets[userID] = idxSet 551 initializedIndexSets[userID] = idxSet 552 } 553 554 commonIndexSet, err := newMockIndexSet("", tableName, tableWorkingDirectory, objectClient) 555 require.NoError(t, err) 556 557 // build TableCompactor and compact the index 558 tCompactor := newTableCompactor(context.Background(), commonIndexSet, existingUserIndexSets, func(userID string) (compactor.IndexSet, error) { 559 idxSet, err := newMockIndexSet(userID, tableName, filepath.Join(tableWorkingDirectory, userID), objectClient) 560 require.NoError(t, err) 561 562 initializedIndexSetsMtx.Lock() 563 defer initializedIndexSetsMtx.Unlock() 564 initializedIndexSets[userID] = idxSet 565 return idxSet, nil 566 }, config.PeriodConfig{}) 567 568 require.NoError(t, tCompactor.CompactTable()) 569 570 // verify that we have CompactedIndex for numUsers 571 require.Len(t, tCompactor.compactedIndexes, tc.expectedNumCompactedIndexes) 572 for userID, compactedIdx := range tCompactor.compactedIndexes { 573 require.Equal(t, tc.shouldRemoveUserSourceIndexes, initializedIndexSets[userID].(*mockIndexSet).removeSourceFiles) 574 require.NotNil(t, initializedIndexSets[userID].(*mockIndexSet).compactedIndex) 575 576 expectedChunks := map[string]index.ChunkMetas{} 577 for _, streamsConfig := range tc.expectedStreams { 578 // we should have both streams with user_id label and without user_id label 579 seriesID := buildStream(streamsConfig.labels, index.ChunkMetas{}, userID).labels.String() 580 expectedChunks[seriesID] = streamsConfig.chunkMetas 581 582 seriesID = buildStream(streamsConfig.labels, index.ChunkMetas{}, "").labels.String() 583 expectedChunks[seriesID] = streamsConfig.chunkMetas 584 } 585 586 // verify the chunkmetas in the builder 587 actualChunks := map[string]index.ChunkMetas{} 588 for seriesID, stream := range initializedIndexSets[userID].(*mockIndexSet).compactedIndex.(*compactedIndex).builder.streams { 589 actualChunks[seriesID] = stream.chunks 590 } 591 592 // now convert the compactedIndex to index.Index and verify the chunkmetas again 593 indexFile, err := compactedIdx.ToIndexFile() 594 require.NoError(t, err) 595 596 actualChunks = map[string]index.ChunkMetas{} 597 err = indexFile.(*TSDBFile).Index.(*TSDBIndex).forSeries(context.Background(), nil, func(lbls labels.Labels, fp model.Fingerprint, chks []index.ChunkMeta) { 598 actualChunks[lbls.String()] = chks 599 }, labels.MustNewMatcher(labels.MatchEqual, "", "")) 600 require.NoError(t, err) 601 602 require.Equal(t, expectedChunks, actualChunks) 603 } 604 605 require.Nil(t, commonIndexSet.(*mockIndexSet).compactedIndex) 606 require.Equal(t, tc.shouldRemoveCommonSourceIndexes, commonIndexSet.(*mockIndexSet).removeSourceFiles) 607 }) 608 } 609 }) 610 } 611 } 612 613 func chunkMetasToChunkEntry(schemaCfg config.SchemaConfig, userID string, lbls labels.Labels, chunkMetas index.ChunkMetas) []retention.ChunkEntry { 614 chunkEntries := make([]retention.ChunkEntry, 0, len(chunkMetas)) 615 for _, chunkMeta := range chunkMetas { 616 chunkEntries = append(chunkEntries, retention.ChunkEntry{ 617 ChunkRef: retention.ChunkRef{ 618 UserID: []byte(userID), 619 SeriesID: []byte(lbls.String()), 620 ChunkID: []byte(schemaCfg.ExternalKey(chunkMetaToChunkRef(userID, chunkMeta, lbls))), 621 From: chunkMeta.From(), 622 Through: chunkMeta.Through(), 623 }, 624 Labels: lbls, 625 }) 626 } 627 628 return chunkEntries 629 } 630 631 func chunkMetaToChunkRef(userID string, chunkMeta index.ChunkMeta, lbls labels.Labels) logproto.ChunkRef { 632 return logproto.ChunkRef{ 633 Fingerprint: lbls.Hash(), 634 UserID: userID, 635 From: chunkMeta.From(), 636 Through: chunkMeta.Through(), 637 Checksum: chunkMeta.Checksum, 638 } 639 } 640 641 func TestCompactedIndex(t *testing.T) { 642 testCtx := setupCompactedIndex(t) 643 644 for name, tc := range map[string]struct { 645 deleteChunks map[string]index.ChunkMetas 646 addChunks []chunk.Chunk 647 deleteSeries []labels.Labels 648 649 shouldErr bool 650 finalExpectedChunks map[string]index.ChunkMetas 651 }{ 652 "no changes": { 653 finalExpectedChunks: map[string]index.ChunkMetas{ 654 testCtx.lbls1.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(10)), 655 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 656 }, 657 }, 658 "delete some chunks from a stream": { 659 deleteChunks: map[string]index.ChunkMetas{ 660 testCtx.lbls1.String(): append(buildChunkMetas(testCtx.shiftTableStart(3), testCtx.shiftTableStart(5)), buildChunkMetas(testCtx.shiftTableStart(7), testCtx.shiftTableStart(8))...), 661 }, 662 finalExpectedChunks: map[string]index.ChunkMetas{ 663 testCtx.lbls1.String(): append(buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(2)), append(buildChunkMetas(testCtx.shiftTableStart(6), testCtx.shiftTableStart(6)), buildChunkMetas(testCtx.shiftTableStart(9), testCtx.shiftTableStart(10))...)...), 664 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 665 }, 666 }, 667 "delete all chunks from a stream": { 668 deleteChunks: map[string]index.ChunkMetas{ 669 testCtx.lbls1.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(10)), 670 }, 671 deleteSeries: []labels.Labels{testCtx.lbls1}, 672 finalExpectedChunks: map[string]index.ChunkMetas{ 673 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 674 }, 675 }, 676 "add some chunks to a stream": { 677 addChunks: []chunk.Chunk{ 678 { 679 Metric: testCtx.lbls1, 680 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(11), testCtx.shiftTableStart(11))[0], testCtx.lbls1), 681 Data: dummyChunkData{}, 682 }, 683 { 684 Metric: testCtx.lbls1, 685 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(12), testCtx.shiftTableStart(12))[0], testCtx.lbls1), 686 Data: dummyChunkData{}, 687 }, 688 }, 689 finalExpectedChunks: map[string]index.ChunkMetas{ 690 testCtx.lbls1.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(12)), 691 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 692 }, 693 }, 694 "add some chunks out of table interval to a stream": { 695 addChunks: []chunk.Chunk{ 696 { 697 Metric: testCtx.lbls1, 698 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(11), testCtx.shiftTableStart(11))[0], testCtx.lbls1), 699 Data: dummyChunkData{}, 700 }, 701 { 702 Metric: testCtx.lbls1, 703 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(12), testCtx.shiftTableStart(12))[0], testCtx.lbls1), 704 Data: dummyChunkData{}, 705 }, 706 // these chunks should not be added 707 { 708 Metric: testCtx.lbls1, 709 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(int64(testCtx.tableInterval.End+100), int64(testCtx.tableInterval.End+100))[0], testCtx.lbls1), 710 Data: dummyChunkData{}, 711 }, 712 { 713 Metric: testCtx.lbls1, 714 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(int64(testCtx.tableInterval.End+200), int64(testCtx.tableInterval.End+200))[0], testCtx.lbls1), 715 Data: dummyChunkData{}, 716 }, 717 }, 718 finalExpectedChunks: map[string]index.ChunkMetas{ 719 testCtx.lbls1.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(12)), 720 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 721 }, 722 }, 723 "add and delete some chunks in a stream": { 724 addChunks: []chunk.Chunk{ 725 { 726 Metric: testCtx.lbls1, 727 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(11), testCtx.shiftTableStart(11))[0], testCtx.lbls1), 728 Data: dummyChunkData{}, 729 }, 730 { 731 Metric: testCtx.lbls1, 732 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(12), testCtx.shiftTableStart(12))[0], testCtx.lbls1), 733 Data: dummyChunkData{}, 734 }, 735 }, 736 deleteChunks: map[string]index.ChunkMetas{ 737 testCtx.lbls1.String(): buildChunkMetas(testCtx.shiftTableStart(3), testCtx.shiftTableStart(5)), 738 }, 739 finalExpectedChunks: map[string]index.ChunkMetas{ 740 testCtx.lbls1.String(): append(buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(2)), buildChunkMetas(testCtx.shiftTableStart(6), testCtx.shiftTableStart(12))...), 741 testCtx.lbls2.String(): buildChunkMetas(testCtx.shiftTableStart(0), testCtx.shiftTableStart(20)), 742 }, 743 }, 744 "adding chunk to non-existing stream should error": { 745 addChunks: []chunk.Chunk{ 746 { 747 Metric: labels.NewBuilder(testCtx.lbls1).Set("new", "label").Labels(), 748 ChunkRef: chunkMetaToChunkRef(testCtx.userID, buildChunkMetas(testCtx.shiftTableStart(11), testCtx.shiftTableStart(11))[0], testCtx.lbls1), 749 Data: dummyChunkData{}, 750 }, 751 }, 752 shouldErr: true, 753 }, 754 } { 755 t.Run(name, func(t *testing.T) { 756 compactedIndex := testCtx.buildCompactedIndex() 757 758 foundChunkEntries := map[string][]retention.ChunkEntry{} 759 err := compactedIndex.ForEachChunk(context.Background(), func(chunkEntry retention.ChunkEntry) (deleteChunk bool, err error) { 760 seriesIDStr := string(chunkEntry.SeriesID) 761 foundChunkEntries[seriesIDStr] = append(foundChunkEntries[seriesIDStr], chunkEntry) 762 if chks, ok := tc.deleteChunks[string(chunkEntry.SeriesID)]; ok { 763 for _, chk := range chks { 764 if chk.MinTime == int64(chunkEntry.From) && chk.MaxTime == int64(chunkEntry.Through) { 765 return true, nil 766 } 767 } 768 } 769 770 return false, nil 771 }) 772 require.NoError(t, err) 773 774 require.Equal(t, testCtx.expectedChunkEntries, foundChunkEntries) 775 776 for _, lbls := range tc.deleteSeries { 777 require.NoError(t, compactedIndex.CleanupSeries(nil, lbls)) 778 } 779 780 for _, chk := range tc.addChunks { 781 _, err := compactedIndex.IndexChunk(chk) 782 require.NoError(t, err) 783 } 784 785 indexFile, err := compactedIndex.ToIndexFile() 786 if tc.shouldErr { 787 require.NotNil(t, err) 788 return 789 } 790 require.NoError(t, err) 791 792 foundChunks := map[string]index.ChunkMetas{} 793 err = indexFile.(*TSDBFile).Index.(*TSDBIndex).forSeries(context.Background(), nil, func(lbls labels.Labels, fp model.Fingerprint, chks []index.ChunkMeta) { 794 foundChunks[lbls.String()] = append(index.ChunkMetas{}, chks...) 795 }, labels.MustNewMatcher(labels.MatchEqual, "", "")) 796 require.NoError(t, err) 797 798 require.Equal(t, tc.finalExpectedChunks, foundChunks) 799 }) 800 } 801 802 } 803 804 func TestIteratorContextCancelation(t *testing.T) { 805 tc := setupCompactedIndex(t) 806 compactedIndex := tc.buildCompactedIndex() 807 808 ctx, cancel := context.WithCancel(context.Background()) 809 cancel() 810 811 var foundChunkEntries []retention.ChunkEntry 812 err := compactedIndex.ForEachChunk(ctx, func(chunkEntry retention.ChunkEntry) (deleteChunk bool, err error) { 813 foundChunkEntries = append(foundChunkEntries, chunkEntry) 814 815 return false, nil 816 }) 817 818 require.ErrorIs(t, err, context.Canceled) 819 } 820 821 type testContext struct { 822 lbls1 labels.Labels 823 lbls2 labels.Labels 824 userID string 825 tableInterval model.Interval 826 shiftTableStart func(ms int64) int64 827 buildCompactedIndex func() *compactedIndex 828 expectedChunkEntries map[string][]retention.ChunkEntry 829 } 830 831 func setupCompactedIndex(t *testing.T) *testContext { 832 t.Helper() 833 834 now := model.Now() 835 periodConfig := config.PeriodConfig{ 836 IndexTables: config.PeriodicTableConfig{Period: config.ObjectStorageIndexRequiredPeriod}, 837 Schema: "v12", 838 } 839 schemaCfg := config.SchemaConfig{ 840 Configs: []config.PeriodConfig{periodConfig}, 841 } 842 indexBuckets, err := indexBuckets(now, now, []config.TableRange{periodConfig.GetIndexTableNumberRange(config.DayTime{Time: now})}) 843 require.NoError(t, err) 844 tableName := indexBuckets[0] 845 tableInterval := retention.ExtractIntervalFromTableName(tableName) 846 // shiftTableStart shift tableInterval.Start by the given amount of milliseconds. 847 // It is used for building chunkmetas relative to start time of the table. 848 shiftTableStart := func(ms int64) int64 { 849 return int64(tableInterval.Start) + ms 850 } 851 852 lbls1 := mustParseLabels(`{foo="bar", a="b"}`) 853 lbls2 := mustParseLabels(`{fizz="buzz", a="b"}`) 854 userID := buildUserID(0) 855 856 buildCompactedIndex := func() *compactedIndex { 857 builder := NewBuilder() 858 stream := buildStream(lbls1, buildChunkMetas(shiftTableStart(0), shiftTableStart(10)), "") 859 builder.AddSeries(stream.labels, stream.fp, stream.chunks) 860 861 stream = buildStream(lbls2, buildChunkMetas(shiftTableStart(0), shiftTableStart(20)), "") 862 builder.AddSeries(stream.labels, stream.fp, stream.chunks) 863 864 builder.FinalizeChunks() 865 866 return newCompactedIndex(context.Background(), tableName, buildUserID(0), t.TempDir(), periodConfig, builder) 867 } 868 869 expectedChunkEntries := map[string][]retention.ChunkEntry{ 870 lbls1.String(): chunkMetasToChunkEntry(schemaCfg, userID, lbls1, buildChunkMetas(shiftTableStart(0), shiftTableStart(10))), 871 lbls2.String(): chunkMetasToChunkEntry(schemaCfg, userID, lbls2, buildChunkMetas(shiftTableStart(0), shiftTableStart(20))), 872 } 873 874 return &testContext{lbls1, lbls2, userID, tableInterval, shiftTableStart, buildCompactedIndex, expectedChunkEntries} 875 } 876 877 type dummyChunkData struct { 878 chunk.Data 879 } 880 881 func (d dummyChunkData) Entries() int { 882 return 0 883 }