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 }