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