github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/integration/index_active_block_rotate_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 "sync" 30 "testing" 31 "time" 32 33 "github.com/m3db/m3/src/dbnode/integration/generate" 34 "github.com/m3db/m3/src/dbnode/namespace" 35 "github.com/m3db/m3/src/dbnode/retention" 36 "github.com/m3db/m3/src/dbnode/storage/index/compaction" 37 xclock "github.com/m3db/m3/src/x/clock" 38 "github.com/m3db/m3/src/x/ident" 39 xtime "github.com/m3db/m3/src/x/time" 40 41 "github.com/stretchr/testify/assert" 42 "github.com/stretchr/testify/require" 43 "github.com/uber-go/tally" 44 "go.uber.org/atomic" 45 "go.uber.org/zap" 46 "go.uber.org/zap/zapcore" 47 "go.uber.org/zap/zaptest/observer" 48 ) 49 50 func TestIndexActiveBlockRotate(t *testing.T) { 51 var ( 52 testNsID = ident.StringID("testns") 53 numWrites = 50 54 numTags = 10 55 blockSize = 2 * time.Hour 56 indexBlockSize = blockSize * 2 57 retentionPeriod = 12 * blockSize 58 bufferPast = 10 * time.Minute 59 rOpts = retention.NewOptions(). 60 SetRetentionPeriod(retentionPeriod). 61 SetBlockSize(blockSize). 62 SetBufferPast(bufferPast) 63 64 idxOpts = namespace.NewIndexOptions().SetEnabled(true).SetBlockSize(indexBlockSize) 65 nsOpts = namespace.NewOptions(). 66 SetRetentionOptions(rOpts). 67 SetIndexOptions(idxOpts). 68 SetColdWritesEnabled(true) 69 70 defaultTimeout = time.Minute 71 // verifyTimeout = time.Minute 72 ) 73 ns, err := namespace.NewMetadata(testNsID, nsOpts) 74 require.NoError(t, err) 75 76 // Set time to next warm flushable block transition 77 // (i.e. align by block + bufferPast - time.Second) 78 currTime := time.Now().UTC() 79 progressTime := false 80 progressTimeDelta := time.Duration(0) 81 lockTime := sync.RWMutex{} 82 setTime := func(t time.Time) { 83 lockTime.Lock() 84 defer lockTime.Unlock() 85 progressTime = false 86 currTime = t.UTC() 87 } 88 setProgressTime := func() { 89 lockTime.Lock() 90 defer lockTime.Unlock() 91 progressTime = true 92 actualNow := time.Now().UTC() 93 progressTimeDelta = currTime.Sub(actualNow) 94 } 95 nowFn := func() time.Time { 96 lockTime.RLock() 97 at := currTime 98 progress := progressTime 99 progressDelta := progressTimeDelta 100 lockTime.RUnlock() 101 if progress { 102 return time.Now().UTC().Add(progressDelta) 103 } 104 return at 105 } 106 107 testOpts := NewTestOptions(t). 108 SetNamespaces([]namespace.Metadata{ns}). 109 SetWriteNewSeriesAsync(true). 110 SetNowFn(nowFn) 111 112 testSetup, err := NewTestSetup(t, testOpts, nil) 113 require.NoError(t, err) 114 defer testSetup.Close() 115 116 // Write test data to disk so that there's some blocks on disk to simulate 117 // some index blocks already having on disk segments already in them. 118 require.NoError(t, testSetup.InitializeBootstrappers(InitializeBootstrappersOptions{ 119 WithFileSystem: true, 120 })) 121 now := testSetup.NowFn()() 122 fooSeries := generate.Series{ 123 ID: ident.StringID("foo"), 124 Tags: ident.NewTags(ident.StringTag("city", "new_york")), 125 } 126 barSeries := generate.Series{ 127 ID: ident.StringID("bar"), 128 Tags: ident.NewTags(ident.StringTag("city", "new_jersey")), 129 } 130 seriesMaps := generate.BlocksByStart([]generate.BlockConfig{ 131 { 132 IDs: []string{fooSeries.ID.String()}, 133 Tags: fooSeries.Tags, 134 NumPoints: 100, 135 Start: now.Add(-3 * blockSize), 136 }, 137 { 138 IDs: []string{barSeries.ID.String()}, 139 Tags: barSeries.Tags, 140 NumPoints: 100, 141 Start: now.Add(-3 * blockSize), 142 }, 143 }) 144 require.NoError(t, writeTestDataToDisk(ns, testSetup, seriesMaps, 0)) 145 146 // Set foreground compaction planner options to force index compaction. 147 minCompactSize := 10 148 foregroundCompactionOpts := compaction.DefaultOptions 149 foregroundCompactionOpts.Levels = []compaction.Level{ 150 { 151 MinSizeInclusive: 0, 152 MaxSizeExclusive: int64(minCompactSize), 153 }, 154 } 155 indexOpts := testSetup.StorageOpts().IndexOptions(). 156 SetForegroundCompactionPlannerOptions(foregroundCompactionOpts) 157 testSetup.SetStorageOpts(testSetup.StorageOpts().SetIndexOptions(indexOpts)) 158 159 // Configure log capture 160 log := testSetup.StorageOpts().InstrumentOptions().Logger() 161 captureCore, logs := observer.New(zapcore.ErrorLevel) 162 zapOpt := zap.WrapCore(func(existingCore zapcore.Core) zapcore.Core { 163 return zapcore.NewTee(existingCore, captureCore) 164 }) 165 log = log.WithOptions(zapOpt) 166 167 // Wire up logger. 168 instrumentOpts := testSetup.StorageOpts().InstrumentOptions(). 169 SetLogger(log) 170 testSetup.SetStorageOpts(testSetup.StorageOpts().SetInstrumentOptions(instrumentOpts)) 171 scope := testSetup.Scope() 172 173 // Start the server. 174 require.NoError(t, testSetup.StartServer()) 175 176 // Stop the server. 177 defer func() { 178 require.NoError(t, testSetup.StopServer()) 179 log.Debug("server is now down") 180 }() 181 182 // Write test data. 183 session, err := testSetup.M3DBClient().DefaultSession() 184 require.NoError(t, err) 185 186 var ( 187 metricGCSeries = "index.block.active-block.gc-series+namespace=" + testNsID.String() 188 metricFlushIndex = "database.flushIndex.success+namespace=" + testNsID.String() 189 ) 190 prevWarmFlushes := counterValue(t, scope, metricFlushIndex) 191 prevNumGCSeries := 0 192 numGCSeries := counterValue(t, scope, metricGCSeries) 193 require.Equal(t, 0, numGCSeries) 194 195 prevLog := log 196 for i := 0; i < 4; i++ { 197 log = prevLog.With(zap.Int("checkIteration", i)) 198 199 // Progress to next time just before a flush and freeze (using setTime). 200 prevTime := nowFn() 201 newTime := prevTime. 202 Truncate(indexBlockSize). 203 Add(2 * indexBlockSize) 204 setTime(newTime) 205 log.Info("progressing time to before next block edge", 206 zap.Stringer("prevTime", prevTime), 207 zap.Stringer("newTime", newTime)) 208 209 start := time.Now() 210 log.Info("writing test data") 211 212 t0 := xtime.ToUnixNano(newTime.Add(-1 * (bufferPast / 2))) 213 t1 := xtime.ToUnixNano(newTime) 214 writesPeriodIter := GenerateTestIndexWrite(i, numWrites, numTags, t0, t1) 215 writesPeriodIter.Write(t, testNsID, session) 216 log.Info("test data written", zap.Duration("took", time.Since(start))) 217 218 log.Info("waiting till data is indexed") 219 indexed := xclock.WaitUntil(func() bool { 220 indexedPeriod := writesPeriodIter.NumIndexed(t, testNsID, session) 221 return indexedPeriod == len(writesPeriodIter) 222 }, 15*time.Second) 223 require.True(t, indexed, 224 fmt.Sprintf("unexpected data indexed: actual=%d, expected=%d", 225 writesPeriodIter.NumIndexedWithOptions(t, testNsID, session, NumIndexedOptions{Logger: log}), 226 len(writesPeriodIter))) 227 log.Info("verified data is indexed", zap.Duration("took", time.Since(start))) 228 229 newTime = prevTime. 230 Truncate(indexBlockSize). 231 Add(2 * indexBlockSize). 232 Add(bufferPast). 233 Add(-100 * time.Millisecond) 234 setTime(newTime) 235 log.Info("progressing time to before next flush", 236 zap.Stringer("prevTime", prevTime), 237 zap.Stringer("newTime", newTime)) 238 239 log.Info("waiting till warm flush occurs") 240 241 // Resume time progressing by wall clock. 242 setProgressTime() 243 244 // Start checks to ensure metrics are visible the whole time. 245 checkFailed := atomic.NewUint64(0) 246 checkIndexable := func() { 247 numGCSeriesBefore := counterValue(t, scope, metricGCSeries) 248 indexedPeriod := writesPeriodIter.NumIndexed(t, testNsID, session) 249 numGCSeriesAfter := counterValue(t, scope, metricGCSeries) 250 if len(writesPeriodIter) != indexedPeriod { 251 assert.Equal(t, len(writesPeriodIter), indexedPeriod, 252 fmt.Sprintf("some metrics not indexed/visible: actual=%d, expected=%d, numGCBefore=%d, numGCAfter=%d", 253 writesPeriodIter.NumIndexedWithOptions(t, testNsID, session, NumIndexedOptions{Logger: log}), 254 len(writesPeriodIter), 255 numGCSeriesBefore, 256 numGCSeriesAfter)) 257 checkFailed.Inc() 258 } 259 } 260 261 ticker := time.NewTicker(10 * time.Millisecond) 262 stopTickCh := make(chan struct{}) 263 closedTickCh := make(chan struct{}) 264 go func() { 265 defer func() { 266 ticker.Stop() 267 close(closedTickCh) 268 }() 269 270 for { 271 select { 272 case <-ticker.C: 273 checkIndexable() 274 case <-stopTickCh: 275 return 276 } 277 } 278 }() 279 280 start = time.Now() 281 warmFlushed := xclock.WaitUntil(func() bool { 282 return counterValue(t, scope, metricFlushIndex)-prevWarmFlushes > 0 283 }, defaultTimeout) 284 counter := counterValue(t, scope, metricFlushIndex) 285 require.True(t, warmFlushed, 286 fmt.Sprintf("warm flush stats: current=%d, previous=%d", counter, prevWarmFlushes)) 287 log.Info("verified data has been warm flushed", zap.Duration("took", time.Since(start))) 288 prevWarmFlushes = counter 289 290 start = time.Now() 291 log.Info("waiting for GC of series") 292 293 expectedNumGCSeries := prevNumGCSeries + numWrites - minCompactSize 294 gcSeries := xclock.WaitUntil(func() bool { 295 // Run background compaction path to check that this path correctly 296 // identifies these series as "empty" post the warm flush above. 297 // Note: typically this path gets called from just WriteBatch calls but 298 // for this test we just explcitly invoke the background compact path. 299 for _, ns := range testSetup.DB().Namespaces() { 300 idx, err := ns.Index() 301 require.NoError(t, err) 302 303 idx.BackgroundCompact() 304 } 305 306 numGCSeries := counterValue(t, scope, metricGCSeries) 307 return numGCSeries >= expectedNumGCSeries 308 }, defaultTimeout) 309 numGCSeries := counterValue(t, scope, metricGCSeries) 310 require.True(t, gcSeries, 311 fmt.Sprintf("unexpected num gc series: actual=%d, expected=%d", 312 numGCSeries, expectedNumGCSeries)) 313 require.True(t, numGCSeries >= expectedNumGCSeries) 314 log.Info("verified series have been GC'd", zap.Duration("took", time.Since(start))) 315 prevNumGCSeries = numGCSeries 316 317 require.Equal(t, 0, logs.Len(), "errors found in logs during flush/indexing") 318 319 // Keep running indexable check for a few seconds, then progress next iter. 320 time.Sleep(5 * time.Second) 321 close(stopTickCh) 322 <-closedTickCh 323 324 // Ensure check did not fail. 325 require.True(t, checkFailed.Load() == 0, 326 fmt.Sprintf("check indexable errors: %d", checkFailed.Load())) 327 } 328 329 log.Info("checks passed") 330 } 331 332 func counterValue(t *testing.T, r tally.TestScope, key string) int { 333 v, ok := r.Snapshot().Counters()[key] 334 require.True(t, ok) 335 return int(v.Value()) 336 }