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  }