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 }