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