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  }