github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/shard_race_prop_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 storage
    22  
    23  import (
    24  	"fmt"
    25  	"math/rand"
    26  	"os"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"github.com/m3db/m3/src/dbnode/namespace"
    32  	"github.com/m3db/m3/src/dbnode/runtime"
    33  	"github.com/m3db/m3/src/dbnode/storage/block"
    34  	"github.com/m3db/m3/src/dbnode/storage/bootstrap/result"
    35  	"github.com/m3db/m3/src/dbnode/storage/series"
    36  	"github.com/m3db/m3/src/x/context"
    37  	"github.com/m3db/m3/src/x/ident"
    38  	xtime "github.com/m3db/m3/src/x/time"
    39  
    40  	"github.com/leanovate/gopter"
    41  	"github.com/leanovate/gopter/gen"
    42  	"github.com/leanovate/gopter/prop"
    43  	"github.com/stretchr/testify/assert"
    44  	"github.com/stretchr/testify/require"
    45  )
    46  
    47  func TestShardTickReadFnRace(t *testing.T) {
    48  	parameters := gopter.DefaultTestParameters()
    49  	seed := time.Now().UnixNano()
    50  	parameters.MinSuccessfulTests = 200
    51  	parameters.MaxSize = 40
    52  	parameters.Rng = rand.New(rand.NewSource(seed))
    53  	properties := gopter.NewProperties(parameters)
    54  
    55  	properties.Property("Concurrent Tick and Shard Fn doesn't panic", prop.ForAll(
    56  		func(ids []ident.ID, tickBatchSize uint8, fn testShardReadFn) bool {
    57  			testShardTickReadFnRace(t, ids, int(tickBatchSize), fn)
    58  			return true
    59  		},
    60  		anyIDs().WithLabel("ids"),
    61  		gen.UInt8().WithLabel("tickBatchSize").SuchThat(func(x uint8) bool { return x > 0 }),
    62  		gen.OneConstOf(fetchBlocksMetadataV2ShardFn),
    63  	))
    64  
    65  	reporter := gopter.NewFormatedReporter(true, 160, os.Stdout)
    66  	if !properties.Run(reporter) {
    67  		t.Errorf("failed with initial seed: %d", seed)
    68  	}
    69  }
    70  
    71  func testShardTickReadFnRace(t *testing.T, ids []ident.ID, tickBatchSize int, fn testShardReadFn) {
    72  	shard, opts := propTestDatabaseShard(t, tickBatchSize)
    73  	defer func() {
    74  		shard.Close()
    75  		opts.RuntimeOptionsManager().Close()
    76  	}()
    77  
    78  	for _, id := range ids {
    79  		addTestSeries(shard, id)
    80  	}
    81  	var wg sync.WaitGroup
    82  
    83  	wg.Add(2)
    84  	go func() {
    85  		_, err := shard.Tick(context.NewNoOpCanncellable(), xtime.Now(), namespace.Context{})
    86  		require.NoError(t, err)
    87  		wg.Done()
    88  	}()
    89  
    90  	go func() {
    91  		fn(shard)
    92  		wg.Done()
    93  	}()
    94  
    95  	wg.Wait()
    96  }
    97  
    98  type testShardReadFn func(shard *dbShard)
    99  
   100  var fetchBlocksMetadataV2ShardFn testShardReadFn = func(shard *dbShard) {
   101  	ctx := context.NewBackground()
   102  	start := xtime.UnixNano(0)
   103  	end := xtime.Now()
   104  	shard.FetchBlocksMetadataV2(ctx, start, end, 100, nil, block.FetchBlocksMetadataOptions{
   105  		IncludeChecksums: true,
   106  		IncludeLastRead:  true,
   107  		IncludeSizes:     true,
   108  	})
   109  	ctx.BlockingClose()
   110  }
   111  
   112  func propTestDatabaseShard(t *testing.T, tickBatchSize int) (*dbShard, Options) {
   113  	opts := DefaultTestOptions().SetRuntimeOptionsManager(runtime.NewOptionsManager())
   114  	shard := testDatabaseShard(t, opts)
   115  	// This sleep duration needs to be at the microsecond level because tests
   116  	// can have a high number of iterations, using a high number of series in
   117  	// combination with a small batch size, causing frequent timeouts during
   118  	// execution.
   119  	shard.currRuntimeOptions.tickSleepPerSeries = time.Microsecond
   120  	shard.currRuntimeOptions.tickSleepSeriesBatchSize = tickBatchSize
   121  	return shard, opts
   122  }
   123  
   124  func anyIDs() gopter.Gen {
   125  	return gen.IntRange(0, 20).
   126  		Map(func(n int) []ident.ID {
   127  			ids := make([]ident.ID, 0, n)
   128  			for i := 0; i < n; i++ {
   129  				ids = append(ids, ident.StringID(fmt.Sprintf("foo.%d", i)))
   130  			}
   131  			return ids
   132  		})
   133  }
   134  
   135  func TestShardTickWriteRace(t *testing.T) {
   136  	parameters := gopter.DefaultTestParameters()
   137  	seed := time.Now().UnixNano()
   138  	parameters.MinSuccessfulTests = 200
   139  	parameters.MaxSize = 10
   140  	parameters.Rng = rand.New(rand.NewSource(seed))
   141  	properties := gopter.NewProperties(parameters)
   142  
   143  	properties.Property("Concurrent Tick and Write doesn't deadlock", prop.ForAll(
   144  		func(tickBatchSize, numSeries int) bool {
   145  			testShardTickWriteRace(t, tickBatchSize, numSeries)
   146  			return true
   147  		},
   148  		gen.IntRange(1, 100).WithLabel("tickBatchSize"),
   149  		gen.IntRange(1, 100).WithLabel("numSeries"),
   150  	))
   151  
   152  	reporter := gopter.NewFormatedReporter(true, 160, os.Stdout)
   153  	if !properties.Run(reporter) {
   154  		t.Errorf("failed with initial seed: %d", seed)
   155  	}
   156  }
   157  
   158  func testShardTickWriteRace(t *testing.T, tickBatchSize, numSeries int) {
   159  	shard, opts := propTestDatabaseShard(t, tickBatchSize)
   160  	defer func() {
   161  		shard.Close()
   162  		opts.RuntimeOptionsManager().Close()
   163  	}()
   164  
   165  	ids := []ident.ID{}
   166  	for i := 0; i < numSeries; i++ {
   167  		ids = append(ids, ident.StringID(fmt.Sprintf("foo.%d", i)))
   168  	}
   169  
   170  	var (
   171  		numRoutines = 1 + /* Fetch */ +1 /* Tick */ + len(ids) /* Write(s) */
   172  		barrier     = make(chan struct{}, numRoutines)
   173  		wg          sync.WaitGroup
   174  	)
   175  
   176  	wg.Add(numRoutines)
   177  
   178  	doneFn := func() {
   179  		if r := recover(); r != nil {
   180  			assert.Fail(t, "unexpected panic: %v", r)
   181  		}
   182  		wg.Done()
   183  	}
   184  
   185  	for _, id := range ids {
   186  		id := id
   187  		go func() {
   188  			defer doneFn()
   189  			<-barrier
   190  			ctx := context.NewBackground()
   191  			now := xtime.Now()
   192  			seriesWrite, err := shard.Write(ctx, id, now, 1.0, xtime.Second, nil, series.WriteOptions{})
   193  			assert.NoError(t, err)
   194  			assert.True(t, seriesWrite.WasWritten)
   195  			ctx.BlockingClose()
   196  		}()
   197  	}
   198  
   199  	go func() {
   200  		defer doneFn()
   201  		<-barrier
   202  		fetchBlocksMetadataV2ShardFn(shard)
   203  	}()
   204  
   205  	go func() {
   206  		defer doneFn()
   207  		<-barrier
   208  		_, err := shard.Tick(context.NewNoOpCanncellable(), xtime.Now(), namespace.Context{})
   209  		assert.NoError(t, err)
   210  	}()
   211  
   212  	for i := 0; i < numRoutines; i++ {
   213  		barrier <- struct{}{}
   214  	}
   215  
   216  	wg.Wait()
   217  }
   218  
   219  func TestShardTickBootstrapWriteRace(t *testing.T) {
   220  	shard, opts := propTestDatabaseShard(t, 10)
   221  	defer func() {
   222  		if r := recover(); r != nil {
   223  			assert.Fail(t, "unexpected panic: %v", r)
   224  		}
   225  		shard.Close()
   226  		opts.RuntimeOptionsManager().Close()
   227  	}()
   228  
   229  	// distribute ids into 3 categories
   230  	// (1) existing in the shard prior to bootstrap (for w/e reason)
   231  	// (2) actively being written to by Write()
   232  	// (3) inserted via Bootstrap()
   233  	// further, we ensure there's pairwise overlaps between each pair of categories.
   234  
   235  	// total ids = 30, splitting id space into following
   236  	// (1) - existingIDs - [0, 20)
   237  	// (2) - writeIDs - [10, 30)
   238  	// (3) - bootstrapIDs - [0, 10) U [] [20, 30)
   239  
   240  	var writeIDs []ident.ID
   241  	bootstrapResult := result.NewMap(result.MapOptions{})
   242  
   243  	for i := 0; i < 30; i++ {
   244  		id := ident.StringID(fmt.Sprintf("foo.%d", i))
   245  		// existing ids
   246  		if i < 20 {
   247  			addTestSeriesWithCount(shard, id, 0)
   248  		}
   249  		// write ids
   250  		if i >= 10 {
   251  			writeIDs = append(writeIDs, id)
   252  		}
   253  		// botstrap ids
   254  		if i < 10 || i >= 20 {
   255  			bootstrapResult.Set(id, result.DatabaseSeriesBlocks{
   256  				ID:     id,
   257  				Tags:   ident.NewTags(),
   258  				Blocks: block.NewDatabaseSeriesBlocks(3),
   259  			})
   260  		}
   261  	}
   262  
   263  	var (
   264  		numRoutines = 1 + /* Bootstrap */ +1 /* Tick */ + len(writeIDs) /* Write(s) */
   265  		barrier     = make(chan struct{}, numRoutines)
   266  		wg          sync.WaitGroup
   267  	)
   268  
   269  	wg.Add(numRoutines)
   270  
   271  	doneFn := func() {
   272  		if r := recover(); r != nil {
   273  			assert.Fail(t, "unexpected panic: %v", r)
   274  		}
   275  		wg.Done()
   276  	}
   277  
   278  	ctx := context.NewBackground()
   279  	defer ctx.Close()
   280  
   281  	assert.NoError(t, shard.Bootstrap(ctx, namespace.Context{ID: ident.StringID("foo")}))
   282  	for _, id := range writeIDs {
   283  		id := id
   284  		go func() {
   285  			defer doneFn()
   286  			<-barrier
   287  			ctx := context.NewBackground()
   288  			now := xtime.Now()
   289  			seriesWrite, err := shard.Write(ctx, id, now, 1.0, xtime.Second, nil, series.WriteOptions{})
   290  			assert.NoError(t, err)
   291  			assert.True(t, seriesWrite.WasWritten)
   292  			ctx.BlockingClose()
   293  		}()
   294  	}
   295  
   296  	go func() {
   297  		defer doneFn()
   298  		<-barrier
   299  		err := shard.LoadBlocks(bootstrapResult)
   300  		assert.NoError(t, err)
   301  	}()
   302  
   303  	go func() {
   304  		defer doneFn()
   305  		<-barrier
   306  		_, err := shard.Tick(context.NewNoOpCanncellable(), xtime.Now(), namespace.Context{})
   307  		assert.NoError(t, err)
   308  	}()
   309  
   310  	for i := 0; i < numRoutines; i++ {
   311  		barrier <- struct{}{}
   312  	}
   313  
   314  	wg.Wait()
   315  }