github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/bootstrap/bootstrapper/commitlog/source_data_test.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package commitlog 22 23 import ( 24 "errors" 25 "io" 26 "strings" 27 "testing" 28 "time" 29 30 "github.com/m3db/m3/src/dbnode/digest" 31 "github.com/m3db/m3/src/dbnode/namespace" 32 "github.com/m3db/m3/src/dbnode/persist" 33 "github.com/m3db/m3/src/dbnode/persist/fs" 34 "github.com/m3db/m3/src/dbnode/persist/fs/commitlog" 35 "github.com/m3db/m3/src/dbnode/storage/bootstrap" 36 "github.com/m3db/m3/src/dbnode/storage/bootstrap/result" 37 "github.com/m3db/m3/src/dbnode/storage/series" 38 "github.com/m3db/m3/src/dbnode/topology" 39 "github.com/m3db/m3/src/dbnode/ts" 40 "github.com/m3db/m3/src/dbnode/x/xio" 41 "github.com/m3db/m3/src/x/checked" 42 "github.com/m3db/m3/src/x/context" 43 "github.com/m3db/m3/src/x/ident" 44 "github.com/m3db/m3/src/x/instrument" 45 "github.com/m3db/m3/src/x/pool" 46 xtime "github.com/m3db/m3/src/x/time" 47 48 "github.com/golang/mock/gomock" 49 "github.com/stretchr/testify/require" 50 ) 51 52 var ( 53 testNamespaceID = ident.StringID("commitlog_test_ns") 54 testDefaultRunOpts = bootstrap.NewRunOptions().SetInitialTopologyState(&topology.StateSnapshot{}) 55 56 shortAnnotation = ts.Annotation("annot") 57 longAnnotation = ts.Annotation(strings.Repeat("x", ts.OptimizedAnnotationLen*3)) 58 ) 59 60 func testNsMetadata(t *testing.T) namespace.Metadata { 61 md, err := namespace.NewMetadata(testNamespaceID, namespace.NewOptions()) 62 require.NoError(t, err) 63 return md 64 } 65 66 func testCache(t *testing.T) bootstrap.Cache { 67 cache, err := bootstrap.NewCache(bootstrap.NewCacheOptions(). 68 SetFilesystemOptions(fs.NewOptions()). 69 SetInstrumentOptions(instrument.NewOptions())) 70 require.NoError(t, err) 71 72 return cache 73 } 74 75 func TestAvailableEmptyRangeError(t *testing.T) { 76 var ( 77 opts = testDefaultOpts 78 src = newCommitLogSource(opts, fs.Inspection{}) 79 res, err = src.AvailableData(testNsMetadata(t), result.NewShardTimeRanges(), testCache(t), testDefaultRunOpts) 80 ) 81 require.NoError(t, err) 82 require.True(t, result.NewShardTimeRanges().Equal(res)) 83 } 84 85 func TestReadEmpty(t *testing.T) { 86 opts := testDefaultOpts 87 88 src := newCommitLogSource(opts, fs.Inspection{}) 89 md := testNsMetadata(t) 90 target := result.NewShardTimeRanges() 91 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, target, md) 92 defer tester.Finish() 93 94 tester.TestReadWith(src) 95 tester.TestUnfulfilledForNamespaceIsEmpty(md) 96 97 values, err := tester.EnsureDumpAllForNamespace(md) 98 require.NoError(t, err) 99 require.Equal(t, 0, len(values)) 100 tester.EnsureNoLoadedBlocks() 101 tester.EnsureNoWrites() 102 } 103 104 func TestReadOnlyOnce(t *testing.T) { 105 opts := testDefaultOpts 106 md := testNsMetadata(t) 107 nsCtx := namespace.NewContextFrom(md) 108 src := newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 109 110 blockSize := md.Options().RetentionOptions().BlockSize() 111 now := xtime.Now() 112 start := now.Truncate(blockSize).Add(-blockSize) 113 end := now.Truncate(blockSize) 114 115 ranges := xtime.NewRanges(xtime.Range{Start: start, End: end}) 116 117 foo := ts.Series{Namespace: nsCtx.ID, Shard: 0, ID: ident.StringID("foo")} 118 bar := ts.Series{Namespace: nsCtx.ID, Shard: 1, ID: ident.StringID("bar")} 119 baz := ts.Series{Namespace: nsCtx.ID, Shard: 2, ID: ident.StringID("baz")} 120 121 values := testValues{ 122 {foo, start, 1.0, xtime.Second, shortAnnotation}, 123 {foo, start.Add(1 * time.Minute), 2.0, xtime.Second, longAnnotation}, 124 {bar, start.Add(2 * time.Minute), 1.0, xtime.Second, longAnnotation}, 125 {bar, start.Add(3 * time.Minute), 2.0, xtime.Second, shortAnnotation}, 126 // "baz" is in shard 2 and should not be returned 127 {baz, start.Add(4 * time.Minute), 1.0, xtime.Second, shortAnnotation}, 128 } 129 130 var commitLogReads int 131 src.newIteratorFn = func( 132 _ commitlog.IteratorOpts, 133 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 134 commitLogReads++ 135 return newTestCommitLogIterator(values, nil), nil, nil 136 } 137 138 targetRanges := result.NewShardTimeRanges().Set(0, ranges).Set(1, ranges) 139 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, targetRanges, md) 140 defer tester.Finish() 141 142 // simulate 2 passes over the commit log 143 for i := 0; i < 2; i++ { 144 tester.TestReadWith(src) 145 tester.TestUnfulfilledForNamespaceIsEmpty(md) 146 147 read := tester.EnsureDumpWritesForNamespace(md) 148 require.Equal(t, 2, len(read)) 149 enforceValuesAreCorrect(t, values[:4], read) 150 tester.EnsureNoLoadedBlocks() 151 } 152 153 // commit log should only be iterated over once. 154 require.Equal(t, 1, commitLogReads) 155 } 156 157 func TestReadErrorOnNewIteratorError(t *testing.T) { 158 opts := testDefaultOpts 159 src := newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 160 161 src.newIteratorFn = func( 162 _ commitlog.IteratorOpts, 163 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 164 return nil, nil, errors.New("an error") 165 } 166 167 now := xtime.Now() 168 ranges := xtime.NewRanges(xtime.Range{Start: now, End: now.Add(time.Hour)}) 169 170 md := testNsMetadata(t) 171 target := result.NewShardTimeRanges().Set(0, ranges) 172 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, target, md) 173 defer tester.Finish() 174 175 ctx := context.NewBackground() 176 defer ctx.Close() 177 178 res, err := src.Read(ctx, tester.Namespaces, tester.Cache) 179 require.Error(t, err) 180 require.Nil(t, res.Results) 181 tester.EnsureNoLoadedBlocks() 182 tester.EnsureNoWrites() 183 } 184 185 func TestReadOrderedValues(t *testing.T) { 186 opts := testDefaultOpts 187 md := testNsMetadata(t) 188 testReadOrderedValues(t, opts, md, nil) 189 } 190 191 func testReadOrderedValues(t *testing.T, opts Options, md namespace.Metadata, setAnn setAnnotation) { 192 nsCtx := namespace.NewContextFrom(md) 193 194 src := newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 195 196 blockSize := md.Options().RetentionOptions().BlockSize() 197 now := xtime.Now() 198 start := now.Truncate(blockSize).Add(-blockSize) 199 end := now.Truncate(blockSize) 200 201 ranges := xtime.NewRanges(xtime.Range{Start: start, End: end}) 202 203 foo := ts.Series{Namespace: nsCtx.ID, Shard: 0, ID: ident.StringID("foo")} 204 bar := ts.Series{Namespace: nsCtx.ID, Shard: 1, ID: ident.StringID("bar")} 205 baz := ts.Series{Namespace: nsCtx.ID, Shard: 2, ID: ident.StringID("baz")} 206 207 values := testValues{ 208 {foo, start, 1.0, xtime.Second, longAnnotation}, 209 {foo, start.Add(1 * time.Minute), 2.0, xtime.Second, shortAnnotation}, 210 {bar, start.Add(2 * time.Minute), 1.0, xtime.Second, nil}, 211 {bar, start.Add(3 * time.Minute), 2.0, xtime.Second, nil}, 212 // "baz" is in shard 2 and should not be returned 213 {baz, start.Add(4 * time.Minute), 1.0, xtime.Second, nil}, 214 } 215 if setAnn != nil { 216 values = setAnn(values) 217 } 218 219 src.newIteratorFn = func( 220 _ commitlog.IteratorOpts, 221 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 222 return newTestCommitLogIterator(values, nil), nil, nil 223 } 224 225 targetRanges := result.NewShardTimeRanges().Set(0, ranges).Set(1, ranges) 226 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, targetRanges, md) 227 defer tester.Finish() 228 229 tester.TestReadWith(src) 230 tester.TestUnfulfilledForNamespaceIsEmpty(md) 231 232 read := tester.EnsureDumpWritesForNamespace(md) 233 require.Equal(t, 2, len(read)) 234 enforceValuesAreCorrect(t, values[:4], read) 235 tester.EnsureNoLoadedBlocks() 236 } 237 238 func TestReadUnorderedValues(t *testing.T) { 239 opts := testDefaultOpts 240 md := testNsMetadata(t) 241 testReadUnorderedValues(t, opts, md, nil) 242 } 243 244 func testReadUnorderedValues(t *testing.T, opts Options, md namespace.Metadata, setAnn setAnnotation) { 245 nsCtx := namespace.NewContextFrom(md) 246 src := newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 247 248 blockSize := md.Options().RetentionOptions().BlockSize() 249 now := xtime.Now() 250 start := now.Truncate(blockSize).Add(-blockSize) 251 end := now.Truncate(blockSize) 252 253 ranges := xtime.NewRanges(xtime.Range{Start: start, End: end}) 254 255 foo := ts.Series{Namespace: nsCtx.ID, Shard: 0, ID: ident.StringID("foo")} 256 257 values := testValues{ 258 {foo, start.Add(10 * time.Minute), 1.0, xtime.Second, shortAnnotation}, 259 {foo, start.Add(1 * time.Minute), 2.0, xtime.Second, nil}, 260 {foo, start.Add(2 * time.Minute), 3.0, xtime.Second, longAnnotation}, 261 {foo, start.Add(3 * time.Minute), 4.0, xtime.Second, nil}, 262 {foo, start, 5.0, xtime.Second, nil}, 263 } 264 if setAnn != nil { 265 values = setAnn(values) 266 } 267 268 src.newIteratorFn = func( 269 _ commitlog.IteratorOpts, 270 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 271 return newTestCommitLogIterator(values, nil), nil, nil 272 } 273 274 targetRanges := result.NewShardTimeRanges().Set(0, ranges).Set(1, ranges) 275 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, targetRanges, md) 276 defer tester.Finish() 277 278 tester.TestReadWith(src) 279 tester.TestUnfulfilledForNamespaceIsEmpty(md) 280 281 read := tester.EnsureDumpWritesForNamespace(md) 282 require.Equal(t, 1, len(read)) 283 enforceValuesAreCorrect(t, values, read) 284 tester.EnsureNoLoadedBlocks() 285 } 286 287 // TestReadHandlesDifferentSeriesWithIdenticalUniqueIndex was added as a 288 // regression test to make sure that the commit log bootstrapper does not make 289 // any assumptions about series having a unique index because that only holds 290 // for the duration that an M3DB node is on, but commit log files can span 291 // multiple M3DB processes which means that unique indexes could be re-used 292 // for multiple different series. 293 func TestReadHandlesDifferentSeriesWithIdenticalUniqueIndex(t *testing.T) { 294 opts := testDefaultOpts 295 md := testNsMetadata(t) 296 297 nsCtx := namespace.NewContextFrom(md) 298 src := newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 299 300 blockSize := md.Options().RetentionOptions().BlockSize() 301 now := xtime.Now() 302 start := now.Truncate(blockSize).Add(-blockSize) 303 end := now.Truncate(blockSize) 304 305 ranges := xtime.NewRanges(xtime.Range{Start: start, End: end}) 306 307 // All series need to be in the same shard to exercise the regression. 308 foo := ts.Series{ 309 Namespace: nsCtx.ID, 310 Shard: 0, 311 ID: ident.StringID("foo"), 312 UniqueIndex: 0, 313 } 314 bar := ts.Series{ 315 Namespace: nsCtx.ID, 316 Shard: 0, 317 ID: ident.StringID("bar"), 318 UniqueIndex: 0, 319 } 320 321 values := testValues{ 322 {foo, start, 1.0, xtime.Second, nil}, 323 {bar, start, 2.0, xtime.Second, nil}, 324 } 325 326 src.newIteratorFn = func( 327 _ commitlog.IteratorOpts, 328 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 329 return newTestCommitLogIterator(values, nil), nil, nil 330 } 331 332 targetRanges := result.NewShardTimeRanges().Set(0, ranges).Set(1, ranges) 333 tester := bootstrap.BuildNamespacesTester(t, testDefaultRunOpts, targetRanges, md) 334 defer tester.Finish() 335 336 tester.TestReadWith(src) 337 tester.TestUnfulfilledForNamespaceIsEmpty(md) 338 339 read := tester.EnsureDumpWritesForNamespace(md) 340 require.Equal(t, 2, len(read)) 341 enforceValuesAreCorrect(t, values, read) 342 tester.EnsureNoLoadedBlocks() 343 } 344 345 func TestItMergesSnapshotsAndCommitLogs(t *testing.T) { 346 opts := testDefaultOpts 347 md := testNsMetadata(t) 348 349 testItMergesSnapshotsAndCommitLogs(t, opts, md, nil) 350 } 351 352 func testItMergesSnapshotsAndCommitLogs(t *testing.T, opts Options, 353 md namespace.Metadata, setAnn setAnnotation) { 354 ctrl := gomock.NewController(t) 355 defer ctrl.Finish() 356 357 var ( 358 nsCtx = namespace.NewContextFrom(md) 359 src = newCommitLogSource(opts, fs.Inspection{}).(*commitLogSource) 360 blockSize = md.Options().RetentionOptions().BlockSize() 361 now = xtime.Now() 362 start = now.Truncate(blockSize).Add(-blockSize) 363 end = now.Truncate(blockSize) 364 ranges = xtime.NewRanges() 365 366 foo = ts.Series{Namespace: nsCtx.ID, Shard: 0, ID: ident.StringID("foo")} 367 commitLogValues = testValues{ 368 {foo, start.Add(2 * time.Minute), 1.0, xtime.Nanosecond, shortAnnotation}, 369 {foo, start.Add(3 * time.Minute), 2.0, xtime.Nanosecond, nil}, 370 {foo, start.Add(4 * time.Minute), 3.0, xtime.Nanosecond, longAnnotation}, 371 } 372 ) 373 if setAnn != nil { 374 commitLogValues = setAnn(commitLogValues) 375 } 376 377 ranges.AddRange(xtime.Range{ 378 Start: start, 379 End: end, 380 }) 381 382 src.newIteratorFn = func( 383 _ commitlog.IteratorOpts, 384 ) (commitlog.Iterator, []commitlog.ErrorWithPath, error) { 385 return newTestCommitLogIterator(commitLogValues, nil), nil, nil 386 } 387 388 src.snapshotFilesFn = func( 389 filePathPrefix string, 390 namespace ident.ID, 391 shard uint32, 392 ) (fs.FileSetFilesSlice, error) { 393 return fs.FileSetFilesSlice{ 394 fs.FileSetFile{ 395 ID: fs.FileSetFileIdentifier{ 396 Namespace: namespace, 397 BlockStart: start, 398 Shard: shard, 399 VolumeIndex: 0, 400 }, 401 // Make sure path passes the "is snapshot" check in SnapshotTimeAndID method. 402 AbsoluteFilePaths: []string{"snapshots/checkpoint"}, 403 CachedHasCompleteCheckpointFile: fs.EvalTrue, 404 CachedSnapshotTime: start.Add(time.Minute), 405 }, 406 }, nil 407 } 408 409 mockReader := fs.NewMockDataFileSetReader(ctrl) 410 mockReader.EXPECT().Open(fs.ReaderOpenOptionsMatcher{ 411 ID: fs.FileSetFileIdentifier{ 412 Namespace: nsCtx.ID, 413 BlockStart: start, 414 Shard: 0, 415 VolumeIndex: 0, 416 }, 417 FileSetType: persist.FileSetSnapshotType, 418 }).Return(nil).AnyTimes() 419 mockReader.EXPECT().Entries().Return(1).AnyTimes() 420 mockReader.EXPECT().Close().Return(nil).AnyTimes() 421 422 snapshotValues := testValues{ 423 {foo, start.Add(1 * time.Minute), 1.0, xtime.Nanosecond, nil}, 424 } 425 if setAnn != nil { 426 snapshotValues = setAnn(snapshotValues) 427 } 428 429 encoderPool := opts.ResultOptions().DatabaseBlockOptions().EncoderPool() 430 encoder := encoderPool.Get() 431 encoder.Reset(snapshotValues[0].t, 10, nsCtx.Schema) 432 for _, value := range snapshotValues { 433 dp := ts.Datapoint{ 434 TimestampNanos: value.t, 435 Value: value.v, 436 } 437 encoder.Encode(dp, value.u, value.a) 438 } 439 440 ctx := context.NewBackground() 441 defer ctx.Close() 442 443 reader, ok := encoder.Stream(ctx) 444 require.True(t, ok) 445 446 seg, err := reader.Segment() 447 require.NoError(t, err) 448 449 bytes, err := xio.ToBytes(reader) 450 require.Equal(t, io.EOF, err) 451 require.Equal(t, seg.Len(), len(bytes)) 452 453 mockReader.EXPECT().Read().Return( 454 foo.ID, 455 ident.EmptyTagIterator, 456 checked.NewBytes(bytes, nil), 457 digest.Checksum(bytes), 458 nil, 459 ) 460 mockReader.EXPECT().Read().Return(nil, nil, nil, uint32(0), io.EOF) 461 462 src.newReaderFn = func( 463 bytesPool pool.CheckedBytesPool, 464 opts fs.Options, 465 ) (fs.DataFileSetReader, error) { 466 return mockReader, nil 467 } 468 469 targetRanges := result.NewShardTimeRanges().Set(0, ranges) 470 tester := bootstrap.BuildNamespacesTesterWithReaderIteratorPool( 471 t, 472 testDefaultRunOpts, 473 targetRanges, 474 opts.ResultOptions().DatabaseBlockOptions().MultiReaderIteratorPool(), 475 fs.NewOptions(), 476 md, 477 ) 478 479 defer tester.Finish() 480 tester.TestReadWith(src) 481 tester.TestUnfulfilledForNamespaceIsEmpty(md) 482 483 // NB: this case is a little tricky in that this test is combining writes 484 // that come through both the `LoadBlock()` methods (for snapshotted data), 485 // and the `Write()` method (for data that is not snapshotted) into the 486 // namespace data accumulator. Thus writes into the accumulated series should 487 // be verified against both of these methods. 488 read := tester.EnsureDumpWritesForNamespace(md) 489 require.Equal(t, 1, len(read)) 490 enforceValuesAreCorrect(t, commitLogValues[0:3], read) 491 492 read = tester.EnsureDumpLoadedBlocksForNamespace(md) 493 enforceValuesAreCorrect(t, snapshotValues, read) 494 } 495 496 type setAnnotation func(testValues) testValues 497 type annotationEqual func([]byte, []byte) bool 498 499 type testValue struct { 500 s ts.Series 501 t xtime.UnixNano 502 v float64 503 u xtime.Unit 504 a ts.Annotation 505 } 506 507 type testValues []testValue 508 509 func (v testValues) toDecodedBlockMap() bootstrap.DecodedBlockMap { 510 blockMap := make(bootstrap.DecodedBlockMap, len(v)) 511 for _, bl := range v { 512 id := bl.s.ID.String() 513 val := series.DecodedTestValue{ 514 Timestamp: bl.t, 515 Value: bl.v, 516 Unit: bl.u, 517 Annotation: bl.a, 518 } 519 520 if values, found := blockMap[id]; found { 521 blockMap[id] = append(values, val) 522 } else { 523 blockMap[id] = bootstrap.DecodedValues{val} 524 } 525 } 526 527 return blockMap 528 } 529 530 func enforceValuesAreCorrect( 531 t *testing.T, 532 values testValues, 533 actual bootstrap.DecodedBlockMap, 534 ) { 535 require.NoError(t, verifyValuesAreCorrect(values, actual)) 536 } 537 538 func verifyValuesAreCorrect( 539 values testValues, 540 actual bootstrap.DecodedBlockMap, 541 ) error { 542 expected := values.toDecodedBlockMap() 543 return expected.VerifyEquals(actual) 544 } 545 546 type testCommitLogIterator struct { 547 values testValues 548 idx int 549 err error 550 closed bool 551 } 552 553 func newTestCommitLogIterator(values testValues, err error) *testCommitLogIterator { 554 return &testCommitLogIterator{values: values, idx: -1, err: err} 555 } 556 557 func (i *testCommitLogIterator) Next() bool { 558 i.idx++ 559 return i.idx < len(i.values) 560 } 561 562 func (i *testCommitLogIterator) Current() commitlog.LogEntry { 563 idx := i.idx 564 if idx == -1 { 565 idx = 0 566 } 567 v := i.values[idx] 568 return commitlog.LogEntry{ 569 Series: v.s, 570 Datapoint: ts.Datapoint{TimestampNanos: v.t, Value: v.v}, 571 Unit: v.u, 572 Annotation: v.a, 573 Metadata: commitlog.LogEntryMetadata{ 574 FileReadID: uint64(idx) + 1, 575 SeriesUniqueIndex: v.s.UniqueIndex, 576 }, 577 } 578 } 579 580 func (i *testCommitLogIterator) Err() error { 581 return i.err 582 } 583 584 func (i *testCommitLogIterator) Close() { 585 i.closed = true 586 }