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  }