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  }