github.com/petermattis/pebble@v0.0.0-20190905164901-ab51a2166067/table_cache_test.go (about) 1 // Copyright 2013 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package pebble 6 7 import ( 8 "bytes" 9 "fmt" 10 "strings" 11 "sync" 12 "testing" 13 "time" 14 15 "github.com/petermattis/pebble/internal/base" 16 "github.com/petermattis/pebble/sstable" 17 "github.com/petermattis/pebble/vfs" 18 "golang.org/x/exp/rand" 19 ) 20 21 type tableCacheTestFile struct { 22 vfs.File 23 fs *tableCacheTestFS 24 name string 25 } 26 27 func (f *tableCacheTestFile) Close() error { 28 f.fs.mu.Lock() 29 if f.fs.closeCounts != nil { 30 f.fs.closeCounts[f.name]++ 31 } 32 f.fs.mu.Unlock() 33 return f.File.Close() 34 } 35 36 type tableCacheTestFS struct { 37 vfs.FS 38 39 mu sync.Mutex 40 openCounts map[string]int 41 closeCounts map[string]int 42 } 43 44 func (fs *tableCacheTestFS) Open(name string, opts ...vfs.OpenOption) (vfs.File, error) { 45 fs.mu.Lock() 46 if fs.openCounts != nil { 47 fs.openCounts[name]++ 48 } 49 fs.mu.Unlock() 50 f, err := fs.FS.Open(name, opts...) 51 if len(opts) < 1 || opts[0] != vfs.RandomReadsOption { 52 return nil, fmt.Errorf("sstable file %s not opened with random reads option", name) 53 } 54 if err != nil { 55 return nil, err 56 } 57 return &tableCacheTestFile{f, fs, name}, nil 58 } 59 60 func (fs *tableCacheTestFS) validate(t *testing.T, c *tableCache, f func(i, gotO, gotC int) error) { 61 if err := fs.validateOpenTables(f); err != nil { 62 t.Error(err) 63 return 64 } 65 c.Close() 66 if err := fs.validateNoneStillOpen(); err != nil { 67 t.Error(err) 68 return 69 } 70 } 71 72 // validateOpenTables validates that no tables in the cache are open twice, and 73 // the number still open is no greater than tableCacheTestCacheSize. 74 func (fs *tableCacheTestFS) validateOpenTables(f func(i, gotO, gotC int) error) error { 75 // try backs off to let any clean-up goroutines do their work. 76 return try(100*time.Microsecond, 20*time.Second, func() error { 77 fs.mu.Lock() 78 defer fs.mu.Unlock() 79 80 numStillOpen := 0 81 for i := 0; i < tableCacheTestNumTables; i++ { 82 filename := base.MakeFilename("", fileTypeTable, uint64(i)) 83 gotO, gotC := fs.openCounts[filename], fs.closeCounts[filename] 84 if gotO > gotC { 85 numStillOpen++ 86 } 87 if gotC != gotO && gotC != gotO-1 { 88 return fmt.Errorf("i=%d: table closed too many or too few times: opened %d times, closed %d times", 89 i, gotO, gotC) 90 } 91 if f != nil { 92 if err := f(i, gotO, gotC); err != nil { 93 return err 94 } 95 } 96 } 97 if numStillOpen > tableCacheTestCacheSize { 98 return fmt.Errorf("numStillOpen is %d, want <= %d", numStillOpen, tableCacheTestCacheSize) 99 } 100 return nil 101 }) 102 } 103 104 // validateNoneStillOpen validates that no tables in the cache are open. 105 func (fs *tableCacheTestFS) validateNoneStillOpen() error { 106 // try backs off to let any clean-up goroutines do their work. 107 return try(100*time.Microsecond, 20*time.Second, func() error { 108 fs.mu.Lock() 109 defer fs.mu.Unlock() 110 111 for i := 0; i < tableCacheTestNumTables; i++ { 112 filename := base.MakeFilename("", fileTypeTable, uint64(i)) 113 gotO, gotC := fs.openCounts[filename], fs.closeCounts[filename] 114 if gotO != gotC { 115 return fmt.Errorf("i=%d: opened %d times, closed %d times", i, gotO, gotC) 116 } 117 } 118 return nil 119 }) 120 } 121 122 const ( 123 tableCacheTestNumTables = 300 124 tableCacheTestCacheSize = 100 125 tableCacheTestHitBufferSize = 64 126 ) 127 128 func newTableCache() (*tableCache, *tableCacheTestFS, error) { 129 xxx := bytes.Repeat([]byte("x"), tableCacheTestNumTables) 130 fs := &tableCacheTestFS{ 131 FS: vfs.NewMem(), 132 } 133 for i := 0; i < tableCacheTestNumTables; i++ { 134 f, err := fs.Create(base.MakeFilename("", fileTypeTable, uint64(i))) 135 if err != nil { 136 return nil, nil, fmt.Errorf("fs.Create: %v", err) 137 } 138 tw := sstable.NewWriter(f, nil, LevelOptions{}) 139 ik := base.ParseInternalKey(fmt.Sprintf("k.SET.%d", i)) 140 if err := tw.Add(ik, xxx[:i]); err != nil { 141 return nil, nil, fmt.Errorf("tw.Set: %v", err) 142 } 143 if err := tw.Close(); err != nil { 144 return nil, nil, fmt.Errorf("tw.Close: %v", err) 145 } 146 } 147 148 fs.mu.Lock() 149 fs.openCounts = map[string]int{} 150 fs.closeCounts = map[string]int{} 151 fs.mu.Unlock() 152 153 opts := &Options{} 154 opts.EnsureDefaults() 155 c := &tableCache{} 156 c.init(0, "", fs, opts, tableCacheTestCacheSize, tableCacheTestHitBufferSize) 157 return c, fs, nil 158 } 159 160 func testTableCacheRandomAccess(t *testing.T, concurrent bool) { 161 const N = 2000 162 c, fs, err := newTableCache() 163 if err != nil { 164 t.Fatal(err) 165 } 166 167 rngMu := sync.Mutex{} 168 rng := rand.New(rand.NewSource(1)) 169 170 errc := make(chan error, N) 171 for i := 0; i < N; i++ { 172 go func(i int) { 173 rngMu.Lock() 174 fileNum, sleepTime := rng.Intn(tableCacheTestNumTables), rng.Intn(1000) 175 rngMu.Unlock() 176 iter, _, err := c.newIters( 177 &fileMetadata{FileNum: uint64(fileNum)}, 178 nil, /* iter options */ 179 nil /* bytes iterated */) 180 if err != nil { 181 errc <- fmt.Errorf("i=%d, fileNum=%d: find: %v", i, fileNum, err) 182 return 183 } 184 iter.SeekGE([]byte("k")) 185 if concurrent { 186 time.Sleep(time.Duration(sleepTime) * time.Microsecond) 187 } 188 if !iter.Valid() { 189 errc <- fmt.Errorf("i=%d, fileNum=%d: valid.0: got false, want true", i, fileNum) 190 return 191 } 192 if got := len(iter.Value()); got != fileNum { 193 errc <- fmt.Errorf("i=%d, fileNum=%d: value: got %d bytes, want %d", i, fileNum, got, fileNum) 194 return 195 } 196 if key, _ := iter.Next(); key != nil { 197 errc <- fmt.Errorf("i=%d, fileNum=%d: next.1: got true, want false", i, fileNum) 198 return 199 } 200 if err := iter.Close(); err != nil { 201 errc <- fmt.Errorf("i=%d, fileNum=%d: close: %v", i, fileNum, err) 202 return 203 } 204 errc <- nil 205 }(i) 206 if !concurrent { 207 if err := <-errc; err != nil { 208 t.Fatal(err) 209 } 210 } 211 } 212 if concurrent { 213 for i := 0; i < N; i++ { 214 if err := <-errc; err != nil { 215 t.Fatal(err) 216 } 217 } 218 } 219 fs.validate(t, c, nil) 220 } 221 222 func TestTableCacheRandomAccessSequential(t *testing.T) { testTableCacheRandomAccess(t, false) } 223 func TestTableCacheRandomAccessConcurrent(t *testing.T) { testTableCacheRandomAccess(t, true) } 224 225 func TestTableCacheFrequentlyUsed(t *testing.T) { 226 const ( 227 N = 1000 228 pinned0 = 7 229 pinned1 = 11 230 ) 231 c, fs, err := newTableCache() 232 if err != nil { 233 t.Fatal(err) 234 } 235 236 for i := 0; i < N; i++ { 237 for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} { 238 iter, _, err := c.newIters( 239 &fileMetadata{FileNum: uint64(j)}, 240 nil, /* iter options */ 241 nil /* bytes iterated */) 242 if err != nil { 243 t.Fatalf("i=%d, j=%d: find: %v", i, j, err) 244 } 245 if err := iter.Close(); err != nil { 246 t.Fatalf("i=%d, j=%d: close: %v", i, j, err) 247 } 248 } 249 } 250 251 fs.validate(t, c, func(i, gotO, gotC int) error { 252 if i == pinned0 || i == pinned1 { 253 if gotO != 1 || gotC != 0 { 254 return fmt.Errorf("i=%d: pinned table: got %d, %d, want %d, %d", i, gotO, gotC, 1, 0) 255 } 256 } else if gotO == 1 { 257 return fmt.Errorf("i=%d: table only opened once", i) 258 } 259 return nil 260 }) 261 } 262 263 func TestTableCacheEvictions(t *testing.T) { 264 const ( 265 N = 1000 266 lo, hi = 10, 20 267 ) 268 c, fs, err := newTableCache() 269 if err != nil { 270 t.Fatal(err) 271 } 272 273 rng := rand.New(rand.NewSource(2)) 274 for i := 0; i < N; i++ { 275 j := rng.Intn(tableCacheTestNumTables) 276 iter, _, err := c.newIters( 277 &fileMetadata{FileNum: uint64(j)}, 278 nil, /* iter options */ 279 nil /* bytes iterated */) 280 if err != nil { 281 t.Fatalf("i=%d, j=%d: find: %v", i, j, err) 282 } 283 if err := iter.Close(); err != nil { 284 t.Fatalf("i=%d, j=%d: close: %v", i, j, err) 285 } 286 287 c.evict(uint64(lo + rng.Intn(hi-lo))) 288 } 289 290 sumEvicted, nEvicted := 0, 0 291 sumSafe, nSafe := 0, 0 292 fs.validate(t, c, func(i, gotO, gotC int) error { 293 if lo <= i && i < hi { 294 sumEvicted += gotO 295 nEvicted++ 296 } else { 297 sumSafe += gotO 298 nSafe++ 299 } 300 return nil 301 }) 302 fEvicted := float64(sumEvicted) / float64(nEvicted) 303 fSafe := float64(sumSafe) / float64(nSafe) 304 // The magic 1.25 number isn't derived from formal modeling. It's just a guess. For 305 // (lo, hi, tableCacheTestCacheSize, tableCacheTestNumTables) = (10, 20, 100, 300), 306 // the ratio seems to converge on roughly 1.5 for large N, compared to 1.0 if we do 307 // not evict any cache entries. 308 if ratio := fEvicted / fSafe; ratio < 1.25 { 309 t.Errorf("evicted tables were opened %.3f times on average, safe tables %.3f, ratio %.3f < 1.250", 310 fEvicted, fSafe, ratio) 311 } 312 } 313 314 func TestTableCacheIterLeak(t *testing.T) { 315 c, _, err := newTableCache() 316 if err != nil { 317 t.Fatal(err) 318 } 319 if _, _, err := c.newIters( 320 &fileMetadata{FileNum: 0}, 321 nil, /* iter options */ 322 nil /* bytes iterated */); err != nil { 323 t.Fatal(err) 324 } 325 if err := c.Close(); err == nil { 326 t.Fatalf("expected failure, but found success") 327 } else if !strings.HasPrefix(err.Error(), "leaked iterators:") { 328 t.Fatalf("expected leaked iterators, but found %+v", err) 329 } else { 330 t.Log(err.Error()) 331 } 332 }