github.com/m3db/m3@v1.5.0/src/dbnode/persist/fs/commitlog/read_write_prop_test.go (about) 1 // +build big 2 // 3 // Copyright (c) 2018 Uber Technologies, Inc. 4 // 5 // Permission is hereby granted, free of charge, to any person obtaining a copy 6 // of this software and associated documentation files (the "Software"), to deal 7 // in the Software without restriction, including without limitation the rights 8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 // copies of the Software, and to permit persons to whom the Software is 10 // furnished to do so, subject to the following conditions: 11 // 12 // The above copyright notice and this permission notice shall be included in 13 // all copies or substantial portions of the Software. 14 // 15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 // THE SOFTWARE. 22 23 package commitlog 24 25 import ( 26 "bytes" 27 "errors" 28 "fmt" 29 "io/ioutil" 30 "os" 31 "path" 32 "reflect" 33 "sync" 34 "testing" 35 "time" 36 37 "github.com/m3db/m3/src/x/pool" 38 "github.com/m3db/m3/src/x/serialize" 39 40 "github.com/m3db/m3/src/dbnode/ts" 41 "github.com/m3db/m3/src/x/context" 42 "github.com/m3db/m3/src/x/ident" 43 xos "github.com/m3db/m3/src/x/os" 44 xtest "github.com/m3db/m3/src/x/test" 45 xtime "github.com/m3db/m3/src/x/time" 46 47 "github.com/leanovate/gopter" 48 "github.com/leanovate/gopter/commands" 49 "github.com/leanovate/gopter/gen" 50 "github.com/stretchr/testify/require" 51 ) 52 53 const ( 54 defaultTestFlushInterval = time.Millisecond 55 ) 56 57 type seriesWritesAndReadPosition struct { 58 writes []generatedWrite 59 readPosition int 60 } 61 62 func TestCommitLogReadWrite(t *testing.T) { 63 baseTestPath, err := ioutil.TempDir("", "commit-log-test-base-dir") 64 require.NoError(t, err) 65 defer os.RemoveAll(baseTestPath) 66 67 opts := testOpts.SetStrategy(StrategyWriteBehind) 68 fsOpts := opts.FilesystemOptions().SetFilePathPrefix(baseTestPath) 69 opts = opts.SetFilesystemOptions(fsOpts).SetFlushInterval(time.Millisecond) 70 71 cl, err := NewCommitLog(opts) 72 require.NoError(t, err) 73 require.NoError(t, cl.Open()) 74 75 params := gopter.DefaultGenParameters() 76 writeResult := gen.SliceOfN(100, genWrite())(params) 77 writesInterface, ok := writeResult.Retrieve() 78 require.True(t, ok) 79 writes, ok := writesInterface.([]generatedWrite) 80 require.True(t, ok) 81 82 ctx := context.NewBackground() 83 for _, w := range writes { 84 require.NoError(t, cl.Write(ctx, w.series, w.datapoint, w.unit, w.annotation)) 85 } 86 ctx.Close() 87 require.NoError(t, cl.Close()) 88 89 i := 0 90 iterOpts := IteratorOpts{ 91 CommitLogOptions: opts, 92 FileFilterPredicate: ReadAllPredicate(), 93 } 94 iter, corruptFiles, err := NewIterator(iterOpts) 95 require.NoError(t, err) 96 require.True(t, len(corruptFiles) == 0) 97 defer iter.Close() 98 99 // Convert the writes to be in-order, but keyed by series ID because the 100 // commitlog reader only guarantees the same order on disk within a 101 // given series 102 writesBySeries := map[string]seriesWritesAndReadPosition{} 103 for _, write := range writes { 104 seriesWrites := writesBySeries[write.series.ID.String()] 105 if seriesWrites.writes == nil { 106 seriesWrites.writes = []generatedWrite{} 107 } 108 seriesWrites.writes = append(seriesWrites.writes, write) 109 writesBySeries[write.series.ID.String()] = seriesWrites 110 } 111 112 for ; iter.Next(); i++ { 113 logEntry := iter.Current() 114 var ( 115 series = logEntry.Series 116 datapoint = logEntry.Datapoint 117 ) 118 119 require.NoError(t, iter.Err()) 120 seriesWrites := writesBySeries[series.ID.String()] 121 write := seriesWrites.writes[seriesWrites.readPosition] 122 123 require.Equal(t, write.series.ID.String(), series.ID.String()) 124 require.True(t, bytes.Equal(write.series.EncodedTags, series.EncodedTags)) 125 require.Equal(t, write.series.Namespace.String(), series.Namespace.String()) 126 require.Equal(t, write.series.Shard, series.Shard) 127 require.Equal(t, write.datapoint.Value, datapoint.Value) 128 require.Equal(t, write.datapoint.TimestampNanos, datapoint.TimestampNanos) 129 130 seriesWrites.readPosition++ 131 writesBySeries[series.ID.String()] = seriesWrites 132 } 133 require.Equal(t, len(writes), i) 134 } 135 136 // TestCommitLogPropTest property tests the commitlog by performing various 137 // operations (Open, Write, Close) in various orders, and then ensuring that 138 // all the data can be read back. In addition, in some runs it will arbitrarily 139 // (based on a randomly generated probability) corrupt any bytes written to disk by 140 // the commitlog to ensure that the commitlog reader is resilient to arbitrarily 141 // corrupted files and will not deadlock / panic. 142 func TestCommitLogPropTest(t *testing.T) { 143 // Temporarily reduce size of buffered channels to increase chance of 144 // catching deadlock issues. 145 var ( 146 oldDecoderInBufChanSize = decoderInBufChanSize 147 oldDecoderOutBufChanSize = decoderOutBufChanSize 148 ) 149 defer func() { 150 decoderInBufChanSize = oldDecoderInBufChanSize 151 decoderOutBufChanSize = oldDecoderOutBufChanSize 152 }() 153 decoderInBufChanSize = 0 154 decoderOutBufChanSize = 0 155 156 basePath, err := ioutil.TempDir("", "commit-log-tests") 157 require.NoError(t, err) 158 defer os.RemoveAll(basePath) 159 160 var ( 161 seed = time.Now().Unix() 162 parameters = gopter.DefaultTestParametersWithSeed(seed) 163 reporter = gopter.NewFormatedReporter(true, 160, os.Stdout) 164 ) 165 parameters.MinSuccessfulTests = 20 166 properties := gopter.NewProperties(parameters) 167 168 comms := clCommandFunctor(t, basePath, seed) 169 properties.Property("CommitLog System", commands.Prop(comms)) 170 if !properties.Run(reporter) { 171 t.Errorf("failed with initial seed: %d", seed) 172 } 173 } 174 175 // clCommandFunctor is a var which implements the command.Commands interface, 176 // i.e. is responsible for creating/destroying the system under test and generating 177 // commands and initial states (clState) 178 func clCommandFunctor(t *testing.T, basePath string, seed int64) *commands.ProtoCommands { 179 return &commands.ProtoCommands{ 180 NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest { 181 return initialState 182 }, 183 DestroySystemUnderTestFunc: func(sut commands.SystemUnderTest) { 184 state := sut.(*clState) 185 if cl := state.cLog; cl != nil { 186 cl.Close() 187 } 188 os.RemoveAll(state.opts.FilesystemOptions().FilePathPrefix()) 189 }, 190 InitialStateGen: genState(t, basePath, seed), 191 InitialPreConditionFunc: func(state commands.State) bool { 192 if state == nil { 193 return false 194 } 195 _, ok := state.(*clState) 196 return ok 197 }, 198 GenCommandFunc: func(state commands.State) gopter.Gen { 199 return gen.OneGenOf( 200 genOpenCommand, 201 genCloseCommand, 202 genWriteBehindCommand, 203 genActiveLogsCommand, 204 genRotateLogsCommand, 205 ) 206 }, 207 } 208 } 209 210 // operations on *clState 211 212 var genOpenCommand = gen.Const(&commands.ProtoCommand{ 213 Name: "Open", 214 PreConditionFunc: func(state commands.State) bool { 215 return !state.(*clState).open 216 }, 217 RunFunc: func(q commands.SystemUnderTest) commands.Result { 218 var err error 219 s := q.(*clState) 220 s.cLog, err = NewCommitLog(s.opts) 221 if err != nil { 222 return err 223 } 224 225 if s.shouldCorrupt { 226 cLog := s.cLog.(*commitLog) 227 cLog.newCommitLogWriterFn = func(flushFn flushFn, opts Options) commitLogWriter { 228 wIface := newCommitLogWriter(flushFn, opts) 229 w := wIface.(*writer) 230 w.chunkWriter = newCorruptingChunkWriter( 231 w.chunkWriter.(*fsChunkWriter), 232 s.corruptionProbability, 233 s.seed, 234 ) 235 return w 236 } 237 } 238 err = s.cLog.Open() 239 if err != nil { 240 return err 241 } 242 s.open = true 243 return nil 244 }, 245 NextStateFunc: func(state commands.State) commands.State { 246 s := state.(*clState) 247 s.open = true 248 return s 249 }, 250 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 251 if result == nil { 252 return &gopter.PropResult{Status: gopter.PropTrue} 253 } 254 return &gopter.PropResult{ 255 Status: gopter.PropError, 256 Error: result.(error), 257 } 258 }, 259 }) 260 261 var genCloseCommand = gen.Const(&commands.ProtoCommand{ 262 Name: "Close", 263 PreConditionFunc: func(state commands.State) bool { 264 return state.(*clState).open 265 }, 266 RunFunc: func(q commands.SystemUnderTest) commands.Result { 267 s := q.(*clState) 268 err := s.cLog.Close() 269 if err != nil { 270 return err 271 } 272 s.open = false 273 return nil 274 }, 275 NextStateFunc: func(state commands.State) commands.State { 276 s := state.(*clState) 277 s.open = false 278 return s 279 }, 280 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 281 if result != nil { 282 return &gopter.PropResult{ 283 Status: gopter.PropError, 284 Error: result.(error), 285 } 286 } 287 s := state.(*clState) 288 err := s.writesArePresent(s.pendingWrites...) 289 if err != nil { 290 return &gopter.PropResult{ 291 Status: gopter.PropError, 292 Error: err.(error), 293 } 294 } 295 return &gopter.PropResult{Status: gopter.PropTrue} 296 }, 297 }) 298 299 var genWriteBehindCommand = gen.SliceOfN(10, genWrite()). 300 Map(func(writes []generatedWrite) commands.Command { 301 return &commands.ProtoCommand{ 302 Name: "WriteBehind", 303 PreConditionFunc: func(state commands.State) bool { 304 return state.(*clState).open 305 }, 306 RunFunc: func(q commands.SystemUnderTest) commands.Result { 307 s := q.(*clState) 308 ctx := context.NewBackground() 309 defer ctx.Close() 310 311 for _, w := range writes { 312 err := s.cLog.Write(ctx, w.series, w.datapoint, w.unit, w.annotation) 313 if err != nil { 314 return err 315 } 316 } 317 318 return nil 319 }, 320 NextStateFunc: func(state commands.State) commands.State { 321 s := state.(*clState) 322 s.pendingWrites = append(s.pendingWrites, writes...) 323 return s 324 }, 325 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 326 if result == nil { 327 return &gopter.PropResult{Status: gopter.PropTrue} 328 } 329 return &gopter.PropResult{ 330 Status: gopter.PropError, 331 Error: result.(error), 332 } 333 }, 334 } 335 }) 336 337 var genActiveLogsCommand = gen.Const(&commands.ProtoCommand{ 338 Name: "ActiveLogs", 339 PreConditionFunc: func(state commands.State) bool { 340 return true 341 }, 342 RunFunc: func(q commands.SystemUnderTest) commands.Result { 343 s := q.(*clState) 344 345 if s.cLog == nil { 346 return nil 347 } 348 349 logs, err := s.cLog.ActiveLogs() 350 if !s.open { 351 if err != errCommitLogClosed { 352 return errors.New("did not receive commit log closed error") 353 } 354 return nil 355 } 356 357 if err != nil { 358 return err 359 } 360 361 if len(logs) != 2 { 362 return fmt.Errorf("ActiveLogs did not return exactly two log files: %v", logs) 363 } 364 365 return nil 366 }, 367 NextStateFunc: func(state commands.State) commands.State { 368 s := state.(*clState) 369 return s 370 }, 371 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 372 if result == nil { 373 return &gopter.PropResult{Status: gopter.PropTrue} 374 } 375 return &gopter.PropResult{ 376 Status: gopter.PropError, 377 Error: result.(error), 378 } 379 }, 380 }) 381 382 var genRotateLogsCommand = gen.Const(&commands.ProtoCommand{ 383 Name: "RotateLogs", 384 PreConditionFunc: func(state commands.State) bool { 385 return true 386 }, 387 RunFunc: func(q commands.SystemUnderTest) commands.Result { 388 s := q.(*clState) 389 390 if s.cLog == nil { 391 return nil 392 } 393 394 _, err := s.cLog.RotateLogs() 395 if !s.open { 396 if err != errCommitLogClosed { 397 return errors.New("did not receive commit log closed error") 398 } 399 return nil 400 } 401 402 if err != nil { 403 return err 404 } 405 406 return nil 407 }, 408 NextStateFunc: func(state commands.State) commands.State { 409 s := state.(*clState) 410 return s 411 }, 412 PostConditionFunc: func(state commands.State, result commands.Result) *gopter.PropResult { 413 if result == nil { 414 return &gopter.PropResult{Status: gopter.PropTrue} 415 } 416 return &gopter.PropResult{ 417 Status: gopter.PropError, 418 Error: result.(error), 419 } 420 }, 421 }) 422 423 // clState holds the expected state (i.e. its the commands.State), and we use it as the SystemUnderTest 424 type clState struct { 425 basePath string 426 opts Options 427 open bool 428 cLog CommitLog 429 pendingWrites []generatedWrite 430 // Whether the test should corrupt the commit log. 431 shouldCorrupt bool 432 // If the test should corrupt the commit log, what is the probability of 433 // corruption for any given write. 434 corruptionProbability float64 435 // Seed for use with all RNGs so that runs are reproducible. 436 seed int64 437 } 438 439 // generator for commit log write 440 func genState(t *testing.T, basePath string, seed int64) gopter.Gen { 441 return gopter.CombineGens(gen.Identifier(), gen.Bool(), gen.Float64Range(0, 1)). 442 MapResult(func(r *gopter.GenResult) *gopter.GenResult { 443 iface, ok := r.Retrieve() 444 if !ok { 445 return gopter.NewEmptyResult(reflect.PtrTo(reflect.TypeOf(clState{}))) 446 } 447 p, ok := iface.([]interface{}) 448 if !ok { 449 return gopter.NewEmptyResult(reflect.PtrTo(reflect.TypeOf(clState{}))) 450 } 451 452 var ( 453 initPath = path.Join(basePath, p[0].(string)) 454 shouldCorrupt = p[1].(bool) 455 corruptionProbability = p[2].(float64) 456 result = newInitState( 457 t, initPath, shouldCorrupt, corruptionProbability, seed) 458 ) 459 return gopter.NewGenResult(result, gopter.NoShrinker) 460 }) 461 } 462 463 func newInitState( 464 t *testing.T, 465 dir string, 466 shouldCorrupt bool, 467 corruptionProbability float64, 468 seed int64, 469 ) *clState { 470 opts := testOpts. 471 SetStrategy(StrategyWriteBehind). 472 SetFlushInterval(defaultTestFlushInterval). 473 // Need to set this to a relatively low value otherwise the test will 474 // time out because its allocating so much memory for the byte pools 475 // in the commit log reader. 476 SetFlushSize(1024) 477 fsOpts := opts.FilesystemOptions().SetFilePathPrefix(dir) 478 opts = opts.SetFilesystemOptions(fsOpts) 479 return &clState{ 480 basePath: dir, 481 opts: opts, 482 shouldCorrupt: shouldCorrupt, 483 corruptionProbability: corruptionProbability, 484 seed: seed, 485 } 486 } 487 488 func (s *clState) writesArePresent(writes ...generatedWrite) error { 489 writesOnDisk := make(map[string]map[xtime.UnixNano]generatedWrite) 490 iterOpts := IteratorOpts{ 491 CommitLogOptions: s.opts, 492 FileFilterPredicate: ReadAllPredicate(), 493 } 494 // Based on the corruption type this could return some corrupt files 495 // or it could not, so we don't check it. 496 iter, _, err := NewIterator(iterOpts) 497 if err != nil { 498 if s.shouldCorrupt { 499 return nil 500 } 501 return err 502 } 503 504 defer iter.Close() 505 506 count := 0 507 for iter.Next() { 508 logEntry := iter.Current() 509 var ( 510 series = logEntry.Series 511 datapoint = logEntry.Datapoint 512 unit = logEntry.Unit 513 annotation = logEntry.Annotation 514 idString = series.ID.String() 515 ) 516 517 seriesMap, ok := writesOnDisk[idString] 518 if !ok { 519 seriesMap = make(map[xtime.UnixNano]generatedWrite) 520 writesOnDisk[idString] = seriesMap 521 } 522 seriesMap[datapoint.TimestampNanos] = generatedWrite{ 523 series: series, 524 datapoint: datapoint, 525 unit: unit, 526 annotation: annotation, 527 } 528 count++ 529 } 530 531 if s.shouldCorrupt { 532 return nil 533 } 534 535 if err := iter.Err(); err != nil { 536 return fmt.Errorf("failed after reading %d datapoints: %v", count, err) 537 } 538 539 missingErr := fmt.Errorf("writesOnDisk: %+v, writes: %+v", writesOnDisk, writes) 540 for _, w := range writes { 541 idString := w.series.ID.String() 542 seriesMap, ok := writesOnDisk[idString] 543 if !ok { 544 return missingErr 545 } 546 gw, ok := seriesMap[w.datapoint.TimestampNanos] 547 if !ok { 548 return missingErr 549 } 550 if !gw.series.Namespace.Equal(w.series.Namespace) || 551 gw.series.Shard != w.series.Shard || 552 gw.datapoint.Value != w.datapoint.Value { 553 return missingErr 554 } 555 } 556 return nil 557 } 558 559 type generatedWrite struct { 560 series ts.Series 561 datapoint ts.Datapoint 562 unit xtime.Unit 563 annotation ts.Annotation 564 } 565 566 func (w generatedWrite) String() string { 567 return fmt.Sprintf("ID = %v, Datapoint = %+v", w.series.ID.String(), w.datapoint) 568 } 569 570 // generator for commit log write 571 func genWrite() gopter.Gen { 572 testTagEncodingPool := serialize.NewTagEncoderPool(serialize.NewTagEncoderOptions(), 573 pool.NewObjectPoolOptions().SetSize(1)) 574 testTagEncodingPool.Init() 575 576 return gopter.CombineGens( 577 gen.Identifier(), 578 gen.TimeRange(time.Now(), 15*time.Minute), 579 gen.Float64(), 580 gen.Identifier(), 581 gen.UInt32(), 582 gen.Identifier(), 583 gen.Identifier(), 584 gen.Identifier(), 585 gen.Identifier(), 586 gen.Bool(), 587 ).Map(func(val []interface{}) generatedWrite { 588 id := val[0].(string) 589 t := val[1].(time.Time) 590 v := val[2].(float64) 591 ns := val[3].(string) 592 shard := val[4].(uint32) 593 tags := map[string]string{ 594 val[5].(string): val[6].(string), 595 val[7].(string): val[8].(string), 596 } 597 encodeTags := val[9].(bool) 598 599 var ( 600 seriesTags ident.Tags 601 seriesEncodedTags []byte 602 ) 603 for k, v := range tags { 604 seriesTags.Append(ident.Tag{ 605 Name: ident.StringID(k), 606 Value: ident.StringID(v), 607 }) 608 } 609 610 if encodeTags { 611 encoder := testTagEncodingPool.Get() 612 if err := encoder.Encode(ident.NewTagsIterator(seriesTags)); err != nil { 613 panic(err) 614 } 615 data, ok := encoder.Data() 616 if !ok { 617 panic("could not encode tags") 618 } 619 620 // Set encoded tags so the "fast" path is activated. 621 seriesEncodedTags = data.Bytes() 622 } 623 624 return generatedWrite{ 625 series: ts.Series{ 626 ID: ident.StringID(id), 627 EncodedTags: seriesEncodedTags, 628 Namespace: ident.StringID(ns), 629 Shard: shard, 630 UniqueIndex: uniqueID(ns, id), 631 }, 632 datapoint: ts.Datapoint{ 633 TimestampNanos: xtime.ToUnixNano(t), 634 Value: v, 635 }, 636 unit: xtime.Nanosecond, 637 } 638 }) 639 } 640 641 type globalMetricIdx struct { 642 sync.Mutex 643 644 idx uint64 645 // NB(prateek): we use a map from ns -> series id (string) -> unique uint64, to 646 // ensure we assign a unique value to each series/ns combination. Further, we 647 // also ensure the value is consistent. i.e. repeated generations of the same 648 // id/ns retrieve the same uint64 as earlier. 649 idToIdx map[string]map[string]uint64 650 } 651 652 var metricIdx = globalMetricIdx{ 653 idToIdx: make(map[string]map[string]uint64), 654 } 655 656 func uniqueID(ns, s string) uint64 { 657 metricIdx.Lock() 658 defer metricIdx.Unlock() 659 660 nsMap, ok := metricIdx.idToIdx[ns] 661 if !ok { 662 nsMap = make(map[string]uint64) 663 metricIdx.idToIdx[ns] = nsMap 664 } 665 666 i, ok := nsMap[s] 667 if ok { 668 return i 669 } 670 671 idx := metricIdx.idx 672 metricIdx.idx++ 673 674 nsMap[s] = idx 675 return idx 676 } 677 678 // corruptingChunkWriter implements the chunkWriter interface and can corrupt all writes issued 679 // to it based on a configurable probability. 680 type corruptingChunkWriter struct { 681 chunkWriter *fsChunkWriter 682 corruptionProbability float64 683 seed int64 684 } 685 686 func newCorruptingChunkWriter( 687 chunkWriter *fsChunkWriter, 688 corruptionProbability float64, 689 seed int64, 690 ) chunkWriter { 691 return &corruptingChunkWriter{ 692 chunkWriter: chunkWriter, 693 corruptionProbability: corruptionProbability, 694 seed: seed, 695 } 696 } 697 698 func (c *corruptingChunkWriter) reset(f xos.File) { 699 c.chunkWriter.fd = xtest.NewCorruptingFile( 700 f, c.corruptionProbability, c.seed) 701 } 702 703 func (c *corruptingChunkWriter) Write(p []byte) (int, error) { 704 return c.chunkWriter.Write(p) 705 } 706 707 func (c *corruptingChunkWriter) close() error { 708 return c.chunkWriter.close() 709 } 710 711 func (c *corruptingChunkWriter) isOpen() bool { 712 return c.chunkWriter.isOpen() 713 } 714 715 func (c *corruptingChunkWriter) sync() error { 716 return c.chunkWriter.sync() 717 } 718 719 func (c *corruptingChunkWriter) setOnFlush(f func(err error)) { 720 c.chunkWriter.setOnFlush(f) 721 }