github.com/m3db/m3@v1.5.0/src/dbnode/integration/index_block_orphaned_entry_test.go (about) 1 // +build integration 2 // 3 // Copyright (c) 2021 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 integration 24 25 import ( 26 "fmt" 27 "math/rand" 28 "runtime" 29 "strings" 30 "sync" 31 "testing" 32 "time" 33 34 "github.com/stretchr/testify/assert" 35 "github.com/stretchr/testify/require" 36 "go.uber.org/zap" 37 38 "github.com/m3db/m3/src/dbnode/client" 39 "github.com/m3db/m3/src/dbnode/namespace" 40 "github.com/m3db/m3/src/dbnode/persist/fs" 41 "github.com/m3db/m3/src/dbnode/storage" 42 "github.com/m3db/m3/src/dbnode/storage/index/compaction" 43 xclock "github.com/m3db/m3/src/x/clock" 44 "github.com/m3db/m3/src/x/ident" 45 xsync "github.com/m3db/m3/src/x/sync" 46 xtime "github.com/m3db/m3/src/x/time" 47 ) 48 49 const ( 50 numTestSeries = 5 51 concurrentWorkers = 25 52 writesPerWorker = 5 53 blockSize = 2 * time.Hour 54 ) 55 56 func TestIndexBlockOrphanedEntry(t *testing.T) { 57 nsOpts := namespace.NewOptions(). 58 SetRetentionOptions(DefaultIntegrationTestRetentionOpts). 59 SetIndexOptions(namespace.NewIndexOptions().SetEnabled(true)) 60 61 setup := generateTestSetup(t, nsOpts) 62 defer setup.Close() 63 64 // Start the server 65 log := setup.StorageOpts().InstrumentOptions().Logger() 66 require.NoError(t, setup.StartServer()) 67 68 // Stop the server 69 defer func() { 70 assert.NoError(t, setup.StopServer()) 71 log.Debug("server is now down") 72 }() 73 74 client := setup.M3DBClient() 75 session, err := client.DefaultSession() 76 require.NoError(t, err) 77 78 // Write concurrent metrics to generate multiple entries for the same series 79 ids := make([]ident.ID, 0, numTestSeries) 80 for i := 0; i < numTestSeries; i++ { 81 fooID := ident.StringID(fmt.Sprintf("foo.%v", i)) 82 ids = append(ids, fooID) 83 84 writeConcurrentMetrics(t, setup, session, fooID) 85 } 86 87 // Write metrics for a different series to push current foreground segment 88 // to the background. After this, all documents for foo.X exist in background segments 89 barID := ident.StringID("bar") 90 writeConcurrentMetrics(t, setup, session, barID) 91 92 // Fast-forward to a block rotation 93 newBlock := xtime.Now().Truncate(blockSize).Add(blockSize) 94 newCurrentTime := newBlock.Add(30 * time.Minute) // Add extra to account for buffer past 95 setup.SetNowFn(newCurrentTime) 96 97 // Wait for flush 98 log.Info("waiting for block rotation to complete") 99 nsID := setup.Namespaces()[0].ID() 100 found := xclock.WaitUntil(func() bool { 101 filesets, err := fs.IndexFileSetsAt(setup.FilePathPrefix(), nsID, newBlock.Add(-blockSize)) 102 require.NoError(t, err) 103 return len(filesets) == 1 104 }, 60*time.Second) 105 require.True(t, found) 106 107 // Do post-block rotation writes 108 for _, id := range ids { 109 writeMetric(t, session, nsID, id, newCurrentTime, 999.0) 110 } 111 writeMetric(t, session, nsID, barID, newCurrentTime, 999.0) 112 113 // Foreground segments should be in the background again which means updated index entry 114 // is now behind the orphaned entry so index reads should fail. 115 log.Info("waiting for metrics to be indexed") 116 var ( 117 missing string 118 ok bool 119 ) 120 found = xclock.WaitUntil(func() bool { 121 for _, id := range ids { 122 ok, err = isIndexedCheckedWithTime( 123 t, session, nsID, id, genTags(id), newCurrentTime, 124 ) 125 if !ok || err != nil { 126 missing = id.String() 127 return false 128 } 129 } 130 return true 131 }, 30*time.Second) 132 assert.True(t, found, fmt.Sprintf("series %s never indexed\n", missing)) 133 assert.NoError(t, err) 134 } 135 136 func writeConcurrentMetrics( 137 t *testing.T, 138 setup TestSetup, 139 session client.Session, 140 seriesID ident.ID, 141 ) { 142 var wg sync.WaitGroup 143 nowFn := setup.DB().Options().ClockOptions().NowFn() 144 145 workerPool := xsync.NewWorkerPool(concurrentWorkers) 146 workerPool.Init() 147 148 mdID := setup.Namespaces()[0].ID() 149 for i := 0; i < concurrentWorkers; i++ { 150 wg.Add(1) 151 go func() { 152 defer wg.Done() 153 154 for j := 0; j < writesPerWorker; j++ { 155 j := j 156 wg.Add(1) 157 workerPool.Go(func() { 158 defer wg.Done() 159 writeMetric(t, session, mdID, seriesID, xtime.ToUnixNano(nowFn()), float64(j)) 160 }) 161 } 162 }() 163 } 164 165 wg.Wait() 166 } 167 168 func genTags(seriesID ident.ID) ident.TagsIterator { 169 return ident.NewTagsIterator(ident.NewTags(ident.StringTag("tagName", seriesID.String()))) 170 } 171 172 func writeMetric( 173 t *testing.T, 174 session client.Session, 175 nsID ident.ID, 176 seriesID ident.ID, 177 timestamp xtime.UnixNano, 178 value float64, 179 ) { 180 err := session.WriteTagged(nsID, seriesID, genTags(seriesID), 181 timestamp, value, xtime.Second, nil) 182 require.NoError(t, err) 183 } 184 185 func generateTestSetup(t *testing.T, nsOpts namespace.Options) TestSetup { 186 md, err := namespace.NewMetadata(testNamespaces[0], nsOpts) 187 require.NoError(t, err) 188 189 testOpts := NewTestOptions(t). 190 SetNamespaces([]namespace.Metadata{md}). 191 SetWriteNewSeriesAsync(true) 192 testSetup, err := NewTestSetup(t, testOpts, nil, 193 func(s storage.Options) storage.Options { 194 s = s.SetCoreFn(func() int { 195 return rand.Intn(4) //nolint:gosec 196 }) 197 compactionOpts := s.IndexOptions().ForegroundCompactionPlannerOptions() 198 compactionOpts.Levels = []compaction.Level{ 199 { 200 MinSizeInclusive: 0, 201 MaxSizeExclusive: 1, 202 }, 203 } 204 return s.SetIndexOptions( 205 s.IndexOptions().SetForegroundCompactionPlannerOptions(compactionOpts)) 206 }) 207 require.NoError(t, err) 208 209 return testSetup 210 } 211 212 func TestIndexBlockOrphanedIndexValuesUpdatedAcrossTimes(t *testing.T) { 213 tests := []struct { 214 name string 215 numIDs int 216 interval time.Duration 217 }{ 218 { 219 name: "4 series every 100 nanos", 220 numIDs: 4, 221 interval: 100, 222 }, 223 { 224 name: "4 series every block", 225 numIDs: 4, 226 interval: blockSize, 227 }, 228 { 229 name: "12 series every 100 nanos", 230 numIDs: 12, 231 interval: 100, 232 }, 233 { 234 name: "12 series every block", 235 numIDs: 12, 236 interval: blockSize, 237 }, 238 { 239 name: "120 series every 100 nanos", 240 numIDs: 120, 241 interval: 100, 242 }, 243 { 244 name: "120 series every block", 245 numIDs: 120, 246 interval: blockSize, 247 }, 248 } 249 for _, tt := range tests { 250 t.Run(tt.name, func(t *testing.T) { 251 testIndexBlockOrphanedIndexValuesUpdatedAcrossTimes(t, tt.numIDs, tt.interval) 252 }) 253 } 254 } 255 256 func testIndexBlockOrphanedIndexValuesUpdatedAcrossTimes( 257 t *testing.T, numIDs int, writeInterval time.Duration, 258 ) { 259 // Write a metric concurrently for multiple index blocks to generate 260 // multiple entries for the same series 261 var ( 262 concurrentWriteMax = runtime.NumCPU() / 2 263 ids = make([]ident.ID, 0, numIDs) 264 writerCh = make(chan func(), concurrentWriteMax) 265 266 nsID = testNamespaces[0] 267 268 writesPerWorker = 5 269 writeTimes = make([]xtime.UnixNano, 0, writesPerWorker) 270 retention = blockSize * time.Duration(1+writesPerWorker) 271 272 seed = time.Now().UnixNano() 273 rng = rand.New(rand.NewSource(seed)) // nolint:gosec 274 ) 275 276 retOpts := DefaultIntegrationTestRetentionOpts.SetRetentionPeriod(retention) 277 nsOpts := namespace.NewOptions(). 278 SetRetentionOptions(retOpts). 279 SetIndexOptions(namespace.NewIndexOptions().SetEnabled(true)). 280 SetColdWritesEnabled(true) 281 282 setup := generateTestSetup(t, nsOpts) 283 defer setup.Close() 284 285 // Start the server 286 log := setup.StorageOpts().InstrumentOptions().Logger() 287 log.Info("running test with seed", zap.Int("seed", int(seed))) 288 require.NoError(t, setup.StartServer()) 289 290 // Stop the server 291 defer func() { 292 assert.NoError(t, setup.StopServer()) 293 log.Debug("server is now down") 294 }() 295 296 client := setup.M3DBClient() 297 session, err := client.DefaultSession() 298 require.NoError(t, err) 299 300 var ( 301 nowFn = setup.DB().Options().ClockOptions().NowFn() 302 // NB: write in the middle of a block to avoid block boundaries. 303 now = nowFn().Truncate(blockSize / 2) 304 ) 305 306 for i := 0; i < writesPerWorker; i++ { 307 writeTime := xtime.ToUnixNano(now.Add(time.Duration(i) * -writeInterval)) 308 writeTimes = append(writeTimes, writeTime) 309 } 310 311 fns := make([]func(), 0, numIDs*writesPerWorker) 312 for i := 0; i < numIDs; i++ { 313 fooID := ident.StringID(fmt.Sprintf("foo.%v", i)) 314 ids = append(ids, fooID) 315 fns = append(fns, writeConcurrentMetricsAcrossTime(t, setup, session, writeTimes, fooID)...) 316 } 317 318 rng.Shuffle(len(fns), func(i, j int) { fns[i], fns[j] = fns[j], fns[i] }) 319 var wg sync.WaitGroup 320 for i := 0; i < concurrentWriteMax; i++ { 321 wg.Add(1) 322 go func() { 323 for writeFn := range writerCh { 324 writeFn() 325 } 326 327 wg.Done() 328 }() 329 } 330 331 for _, fn := range fns { 332 writerCh <- fn 333 } 334 335 close(writerCh) 336 wg.Wait() 337 338 queryIDs := func() { 339 notFoundIds := make(notFoundIDs, 0, len(ids)*len(writeTimes)) 340 for _, id := range ids { 341 for _, writeTime := range writeTimes { 342 notFoundIds = append(notFoundIds, notFoundID{id: id, runAt: writeTime}) 343 } 344 } 345 346 found := xclock.WaitUntil(func() bool { 347 filteredIds := notFoundIds[:0] 348 for _, id := range notFoundIds { 349 ok, err := isIndexedCheckedWithTime( 350 t, session, nsID, id.id, genTags(id.id), id.runAt, 351 ) 352 if !ok || err != nil { 353 filteredIds = append(filteredIds, id) 354 } 355 } 356 357 if len(filteredIds) == 0 { 358 return true 359 } 360 361 notFoundIds = filteredIds 362 return false 363 }, time.Second*30) 364 365 require.True(t, found, fmt.Sprintf("series %s never indexed\n", notFoundIds)) 366 } 367 368 // Ensure all IDs are eventually queryable, even when only in the foreground 369 // segments. 370 queryIDs() 371 372 // Write metrics for a different series to push current foreground segment 373 // to the background. After this, all documents for foo.X exist in background segments 374 barID := ident.StringID("bar") 375 writeConcurrentMetrics(t, setup, session, barID) 376 377 queryIDs() 378 379 // Fast-forward to a block rotation 380 newBlock := xtime.Now().Truncate(blockSize).Add(blockSize) 381 newCurrentTime := newBlock.Add(30 * time.Minute) // Add extra to account for buffer past 382 setup.SetNowFn(newCurrentTime) 383 384 // Wait for flush 385 log.Info("waiting for block rotation to complete") 386 found := xclock.WaitUntil(func() bool { 387 filesets, err := fs.IndexFileSetsAt(setup.FilePathPrefix(), nsID, newBlock.Add(-blockSize)) 388 require.NoError(t, err) 389 return len(filesets) == 1 390 }, 30*time.Second) 391 require.True(t, found) 392 393 queryIDs() 394 } 395 396 type notFoundID struct { 397 id ident.ID 398 runAt xtime.UnixNano 399 } 400 401 func (i notFoundID) String() string { 402 return fmt.Sprintf("{%s: %s}", i.id.String(), i.runAt.String()) 403 } 404 405 type notFoundIDs []notFoundID 406 407 func (ids notFoundIDs) String() string { 408 strs := make([]string, 0, len(ids)) 409 for _, id := range ids { 410 strs = append(strs, id.String()) 411 } 412 413 return fmt.Sprintf("[%s]", strings.Join(strs, ", ")) 414 } 415 416 // writeConcurrentMetricsAcrossTime writes a datapoint for the given series at 417 // each `writeTime` simultaneously. 418 func writeConcurrentMetricsAcrossTime( 419 t *testing.T, 420 setup TestSetup, 421 session client.Session, 422 writeTimes []xtime.UnixNano, 423 seriesID ident.ID, 424 ) []func() { 425 workerPool := xsync.NewWorkerPool(concurrentWorkers) 426 workerPool.Init() 427 428 mdID := setup.Namespaces()[0].ID() 429 fns := make([]func(), 0, len(writeTimes)) 430 431 for j, writeTime := range writeTimes { 432 j, writeTime := j, writeTime 433 fns = append(fns, func() { 434 writeMetric(t, session, mdID, seriesID, writeTime, float64(j)) 435 }) 436 } 437 438 return fns 439 }