github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/shipper/index/table_test.go (about)

     1  package index
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"strconv"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/stretchr/testify/require"
    13  	"github.com/weaveworks/common/user"
    14  	"go.etcd.io/bbolt"
    15  
    16  	"github.com/grafana/loki/pkg/storage/chunk/client/local"
    17  	"github.com/grafana/loki/pkg/storage/chunk/client/util"
    18  	shipper_index "github.com/grafana/loki/pkg/storage/stores/indexshipper/index"
    19  	"github.com/grafana/loki/pkg/storage/stores/series/index"
    20  	"github.com/grafana/loki/pkg/storage/stores/shipper/index/indexfile"
    21  	"github.com/grafana/loki/pkg/storage/stores/shipper/testutil"
    22  )
    23  
    24  const (
    25  	indexDirName = "index"
    26  	userID       = "user-id"
    27  )
    28  
    29  type mockIndexShipper struct {
    30  	addedIndexes map[string][]shipper_index.Index
    31  }
    32  
    33  func newMockIndexShipper() Shipper {
    34  	return &mockIndexShipper{
    35  		addedIndexes: make(map[string][]shipper_index.Index),
    36  	}
    37  }
    38  
    39  func (m *mockIndexShipper) AddIndex(tableName, _ string, index shipper_index.Index) error {
    40  	m.addedIndexes[tableName] = append(m.addedIndexes[tableName], index)
    41  	return nil
    42  }
    43  
    44  func (m *mockIndexShipper) ForEach(ctx context.Context, tableName, _ string, callback shipper_index.ForEachIndexCallback) error {
    45  	for _, idx := range m.addedIndexes[tableName] {
    46  		if err := callback(false, idx); err != nil {
    47  			return err
    48  		}
    49  	}
    50  
    51  	return nil
    52  }
    53  
    54  func (m *mockIndexShipper) hasIndex(tableName, indexName string) bool {
    55  	for _, index := range m.addedIndexes[tableName] {
    56  		if indexName == index.Name() {
    57  			return true
    58  		}
    59  	}
    60  
    61  	return false
    62  }
    63  
    64  type stopFunc func()
    65  
    66  func buildTestTable(t *testing.T, path string, makePerTenantBuckets bool) (*Table, stopFunc) {
    67  	mockIndexShipper := newMockIndexShipper()
    68  	indexPath := filepath.Join(path, indexDirName)
    69  
    70  	require.NoError(t, util.EnsureDirectory(indexPath))
    71  
    72  	table, err := NewTable(indexPath, "test", mockIndexShipper, makePerTenantBuckets)
    73  	require.NoError(t, err)
    74  
    75  	return table, table.Stop
    76  }
    77  
    78  func TestLoadTable(t *testing.T) {
    79  	indexPath := t.TempDir()
    80  
    81  	boltDBIndexClient, err := local.NewBoltDBIndexClient(local.BoltDBConfig{Directory: indexPath})
    82  	require.NoError(t, err)
    83  
    84  	defer func() {
    85  		boltDBIndexClient.Stop()
    86  	}()
    87  
    88  	// setup some dbs with default bucket and per tenant bucket for a table at a path.
    89  	tablePath := filepath.Join(indexPath, "test-table")
    90  	testutil.SetupDBsAtPath(t, tablePath, map[string]testutil.DBConfig{
    91  		"db1": {
    92  			DBRecords: testutil.DBRecords{
    93  				Start:      0,
    94  				NumRecords: 10,
    95  			},
    96  		},
    97  		"db2": {
    98  			DBRecords: testutil.DBRecords{
    99  				Start:      10,
   100  				NumRecords: 10,
   101  			},
   102  		},
   103  	}, nil)
   104  
   105  	// change a boltdb file to text file which would fail to open.
   106  	invalidFilePath := filepath.Join(tablePath, "invalid")
   107  	require.NoError(t, ioutil.WriteFile(invalidFilePath, []byte("invalid boltdb file"), 0o666))
   108  
   109  	// verify that changed boltdb file can't be opened.
   110  	_, err = local.OpenBoltdbFile(invalidFilePath)
   111  	require.Error(t, err)
   112  
   113  	// try loading the table.
   114  	table, err := LoadTable(tablePath, "test", newMockIndexShipper(), false, newMetrics(nil))
   115  	require.NoError(t, err)
   116  	require.NotNil(t, table)
   117  
   118  	defer func() {
   119  		table.Stop()
   120  	}()
   121  
   122  	// verify that we still have 3 files(2 valid, 1 invalid)
   123  	filesInfo, err := ioutil.ReadDir(tablePath)
   124  	require.NoError(t, err)
   125  	require.Len(t, filesInfo, 3)
   126  
   127  	// query the loaded table to see if it has right data.
   128  	require.NoError(t, table.Snapshot())
   129  	testutil.VerifyIndexes(t, userID, []index.Query{{TableName: table.name}}, func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   130  		return table.ForEach(ctx, callback)
   131  	}, 0, 20)
   132  }
   133  
   134  func TestTable_Write(t *testing.T) {
   135  	for _, withPerTenantBucket := range []bool{false, true} {
   136  		t.Run(fmt.Sprintf("withPerTenantBucket=%v", withPerTenantBucket), func(t *testing.T) {
   137  			tempDir := t.TempDir()
   138  
   139  			table, stopFunc := buildTestTable(t, tempDir, withPerTenantBucket)
   140  			defer stopFunc()
   141  
   142  			now := time.Now()
   143  
   144  			// allow modifying last 5 shards
   145  			table.modifyShardsSince = now.Add(-5 * ShardDBsByDuration).Unix()
   146  
   147  			// a couple of times for which we want to do writes to make the table create different shards
   148  			testCases := []struct {
   149  				writeTime time.Time
   150  				dbName    string // set only when it is supposed to be written to a different name than usual
   151  			}{
   152  				{
   153  					writeTime: now,
   154  				},
   155  				{
   156  					writeTime: now.Add(-(ShardDBsByDuration + 5*time.Minute)),
   157  				},
   158  				{
   159  					writeTime: now.Add(-(ShardDBsByDuration*3 + 3*time.Minute)),
   160  				},
   161  				{
   162  					writeTime: now.Add(-6 * ShardDBsByDuration), // write with time older than table.modifyShardsSince
   163  					dbName:    fmt.Sprint(table.modifyShardsSince),
   164  				},
   165  			}
   166  
   167  			numFiles := 0
   168  
   169  			// performing writes and checking whether the index gets written to right shard
   170  			for i, tc := range testCases {
   171  				t.Run(fmt.Sprint(i), func(t *testing.T) {
   172  					batch := local.NewWriteBatch()
   173  					testutil.AddRecordsToBatch(batch, "test", i*10, 10)
   174  					require.NoError(t, table.write(user.InjectOrgID(context.Background(), userID), tc.writeTime, batch.(*local.BoltWriteBatch).Writes["test"]))
   175  
   176  					numFiles++
   177  					require.Equal(t, numFiles, len(table.dbs))
   178  
   179  					expectedDBName := tc.dbName
   180  					if expectedDBName == "" {
   181  						expectedDBName = fmt.Sprint(tc.writeTime.Truncate(ShardDBsByDuration).Unix())
   182  					}
   183  					db, ok := table.dbs[expectedDBName]
   184  					require.True(t, ok)
   185  
   186  					require.NoError(t, table.Snapshot())
   187  
   188  					// test that the table has current + previous records
   189  					testutil.VerifyIndexes(t, userID, []index.Query{{}},
   190  						func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   191  							return table.ForEach(ctx, callback)
   192  						},
   193  						0, (i+1)*10)
   194  					bucketToQuery := local.IndexBucketName
   195  					if withPerTenantBucket {
   196  						bucketToQuery = []byte(userID)
   197  					}
   198  					testutil.VerifySingleIndexFile(t, index.Query{}, db, bucketToQuery, i*10, 10)
   199  				})
   200  			}
   201  		})
   202  	}
   203  }
   204  
   205  func TestTable_HandoverIndexesToShipper(t *testing.T) {
   206  	for _, withPerTenantBucket := range []bool{false, true} {
   207  		t.Run(fmt.Sprintf("withPerTenantBucket=%v", withPerTenantBucket), func(t *testing.T) {
   208  			tempDir := t.TempDir()
   209  
   210  			table, stopFunc := buildTestTable(t, tempDir, withPerTenantBucket)
   211  			defer stopFunc()
   212  
   213  			now := time.Now()
   214  
   215  			// write a batch for now
   216  			batch := local.NewWriteBatch()
   217  			testutil.AddRecordsToBatch(batch, table.name, 0, 10)
   218  			require.NoError(t, table.write(user.InjectOrgID(context.Background(), userID), now, batch.(*local.BoltWriteBatch).Writes[table.name]))
   219  
   220  			// handover indexes from the table
   221  			require.NoError(t, table.HandoverIndexesToShipper(true))
   222  			require.Len(t, table.dbs, 0)
   223  			require.Len(t, table.dbSnapshots, 0)
   224  
   225  			// check that shipper has the data we handed over
   226  			indexShipper := table.indexShipper.(*mockIndexShipper)
   227  			require.Len(t, indexShipper.addedIndexes[table.name], 1)
   228  
   229  			testutil.VerifyIndexes(t, userID, []index.Query{{TableName: table.name}},
   230  				func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   231  					return indexShipper.ForEach(ctx, table.name, "", func(_ bool, index shipper_index.Index) error {
   232  						return callback(index.(*indexfile.IndexFile).GetBoltDB())
   233  					})
   234  				},
   235  				0, 10)
   236  
   237  			// write a batch to another shard
   238  			batch = local.NewWriteBatch()
   239  			testutil.AddRecordsToBatch(batch, table.name, 10, 10)
   240  			require.NoError(t, table.write(user.InjectOrgID(context.Background(), userID), now.Add(ShardDBsByDuration), batch.(*local.BoltWriteBatch).Writes[table.name]))
   241  
   242  			// handover indexes from the table
   243  			require.NoError(t, table.HandoverIndexesToShipper(true))
   244  			require.Len(t, table.dbs, 0)
   245  			require.Len(t, table.dbSnapshots, 0)
   246  
   247  			// check that shipper got the new data we handed over
   248  			require.Len(t, indexShipper.addedIndexes[table.name], 2)
   249  			testutil.VerifyIndexes(t, userID, []index.Query{{TableName: table.name}},
   250  				func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   251  					return indexShipper.ForEach(ctx, table.name, "", func(_ bool, index shipper_index.Index) error {
   252  						return callback(index.(*indexfile.IndexFile).GetBoltDB())
   253  					})
   254  				},
   255  				0, 20)
   256  		})
   257  	}
   258  }
   259  
   260  func Test_LoadBoltDBsFromDir(t *testing.T) {
   261  	indexPath := t.TempDir()
   262  
   263  	// setup some dbs with a snapshot file.
   264  	tablePath := testutil.SetupDBsAtPath(t, filepath.Join(indexPath, "test-table"), map[string]testutil.DBConfig{
   265  		"db1": {
   266  			DBRecords: testutil.DBRecords{
   267  				Start:      0,
   268  				NumRecords: 10,
   269  			},
   270  		},
   271  		"db1" + indexfile.TempFileSuffix: { // a snapshot file which should be ignored.
   272  			DBRecords: testutil.DBRecords{
   273  				Start:      0,
   274  				NumRecords: 10,
   275  			},
   276  		},
   277  		"db2": {
   278  			DBRecords: testutil.DBRecords{
   279  				Start:      10,
   280  				NumRecords: 10,
   281  			},
   282  		},
   283  	}, nil)
   284  
   285  	// create a boltdb file without bucket which should get removed
   286  	db, err := local.OpenBoltdbFile(filepath.Join(tablePath, "no-bucket"))
   287  	require.NoError(t, err)
   288  	require.NoError(t, db.Close())
   289  
   290  	// try loading the dbs
   291  	dbs, err := loadBoltDBsFromDir(tablePath, newMetrics(nil))
   292  	require.NoError(t, err)
   293  
   294  	// check that we have just 2 dbs
   295  	require.Len(t, dbs, 2)
   296  	require.NotNil(t, dbs["db1"])
   297  	require.NotNil(t, dbs["db2"])
   298  
   299  	// close all the open dbs
   300  	for _, boltdb := range dbs {
   301  		require.NoError(t, boltdb.Close())
   302  	}
   303  
   304  	filesInfo, err := ioutil.ReadDir(tablePath)
   305  	require.NoError(t, err)
   306  	require.Len(t, filesInfo, 2)
   307  }
   308  
   309  func TestTable_ImmutableUploads(t *testing.T) {
   310  	tempDir := t.TempDir()
   311  
   312  	indexShipper := newMockIndexShipper()
   313  	indexPath := filepath.Join(tempDir, indexDirName)
   314  
   315  	// shardCutoff is calculated based on when shards are considered to not be active anymore and are safe to be
   316  	// handed over to shipper for uploading.
   317  	shardCutoff := getOldestActiveShardTime()
   318  
   319  	// some dbs to setup
   320  	dbNames := []int64{
   321  		shardCutoff.Add(-ShardDBsByDuration).Unix(),    // inactive shard, should handover
   322  		shardCutoff.Add(-1 * time.Minute).Unix(),       // 1 minute before shard cutoff, should handover
   323  		time.Now().Truncate(ShardDBsByDuration).Unix(), // active shard, should not handover
   324  	}
   325  
   326  	dbs := map[string]testutil.DBConfig{}
   327  	for _, dbName := range dbNames {
   328  		dbs[fmt.Sprint(dbName)] = testutil.DBConfig{
   329  			DBRecords: testutil.DBRecords{
   330  				NumRecords: 10,
   331  			},
   332  		}
   333  	}
   334  
   335  	// setup some dbs for a table at a path.
   336  	tableName := "test-table"
   337  	tablePath := testutil.SetupDBsAtPath(t, filepath.Join(indexPath, tableName), dbs, nil)
   338  
   339  	table, err := LoadTable(tablePath, "test", indexShipper, false, newMetrics(nil))
   340  	require.NoError(t, err)
   341  	require.NotNil(t, table)
   342  
   343  	defer func() {
   344  		table.Stop()
   345  	}()
   346  
   347  	// db expected to be handed over without forcing it
   348  	expectedDBsToHandedOver := []int64{dbNames[0], dbNames[1]}
   349  
   350  	// handover dbs without forcing it which should not handover active shard or shard which has been active upto a minute back.
   351  	require.NoError(t, table.HandoverIndexesToShipper(false))
   352  
   353  	mockIndexShipper := table.indexShipper.(*mockIndexShipper)
   354  
   355  	// verify that only expected dbs are handed over
   356  	require.Len(t, mockIndexShipper.addedIndexes, 1)
   357  	require.Len(t, mockIndexShipper.addedIndexes[table.name], len(expectedDBsToHandedOver))
   358  	for _, expectedDB := range expectedDBsToHandedOver {
   359  		require.True(t, mockIndexShipper.hasIndex(tableName, table.buildFileName(fmt.Sprint(expectedDB))))
   360  	}
   361  
   362  	// force handover of dbs
   363  	require.NoError(t, table.HandoverIndexesToShipper(true))
   364  	expectedDBsToHandedOver = dbNames
   365  
   366  	// verify that all the dbs are handed over
   367  	require.Len(t, mockIndexShipper.addedIndexes, 1)
   368  	require.Len(t, mockIndexShipper.addedIndexes[table.name], len(expectedDBsToHandedOver))
   369  	for _, expectedDB := range expectedDBsToHandedOver {
   370  		require.True(t, mockIndexShipper.hasIndex(tableName, table.buildFileName(fmt.Sprint(expectedDB))))
   371  	}
   372  
   373  	// clear dbs handed over to shipper
   374  	mockIndexShipper.addedIndexes = map[string][]shipper_index.Index{}
   375  
   376  	// force handover of dbs
   377  	require.NoError(t, table.HandoverIndexesToShipper(true))
   378  
   379  	// make sure nothing was added to shipper again
   380  	require.Len(t, mockIndexShipper.addedIndexes, 0)
   381  }
   382  
   383  func TestTable_MultiQueries(t *testing.T) {
   384  	indexPath := t.TempDir()
   385  
   386  	boltDBIndexClient, err := local.NewBoltDBIndexClient(local.BoltDBConfig{Directory: indexPath})
   387  	require.NoError(t, err)
   388  
   389  	defer func() {
   390  		boltDBIndexClient.Stop()
   391  	}()
   392  
   393  	user1, user2 := "user1", "user2"
   394  
   395  	// setup some dbs with default bucket and per tenant bucket for a table at a path.
   396  	tablePath := filepath.Join(indexPath, "test-table")
   397  	testutil.SetupDBsAtPath(t, tablePath, map[string]testutil.DBConfig{
   398  		"db1": {
   399  			DBRecords: testutil.DBRecords{
   400  				NumRecords: 10,
   401  			},
   402  		},
   403  		"db2": {
   404  			DBRecords: testutil.DBRecords{
   405  				Start:      10,
   406  				NumRecords: 10,
   407  			},
   408  		},
   409  	}, nil)
   410  	testutil.SetupDBsAtPath(t, tablePath, map[string]testutil.DBConfig{
   411  		"db3": {
   412  			DBRecords: testutil.DBRecords{
   413  				Start:      20,
   414  				NumRecords: 10,
   415  			},
   416  		},
   417  		"db4": {
   418  			DBRecords: testutil.DBRecords{
   419  				Start:      30,
   420  				NumRecords: 10,
   421  			},
   422  		},
   423  	}, []byte(user1))
   424  
   425  	// try loading the table.
   426  	table, err := LoadTable(tablePath, "test", newMockIndexShipper(), false, newMetrics(nil))
   427  	require.NoError(t, err)
   428  	require.NotNil(t, table)
   429  	defer func() {
   430  		table.Stop()
   431  	}()
   432  
   433  	require.NoError(t, table.Snapshot())
   434  
   435  	// build queries each looking for specific value from all the dbs
   436  	var queries []index.Query
   437  	for i := 5; i < 35; i++ {
   438  		queries = append(queries, index.Query{TableName: table.name, ValueEqual: []byte(strconv.Itoa(i))})
   439  	}
   440  
   441  	// querying data for user1 should return both data from common index and user1's index
   442  	testutil.VerifyIndexes(t, user1, queries,
   443  		func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   444  			return table.ForEach(ctx, callback)
   445  		},
   446  		5, 30)
   447  
   448  	// querying data for user2 should return only common index
   449  	testutil.VerifyIndexes(t, user2, queries,
   450  		func(ctx context.Context, _ string, callback func(boltdb *bbolt.DB) error) error {
   451  			return table.ForEach(ctx, callback)
   452  		},
   453  		5, 15)
   454  }