Repository: petermattis/pebble (go)

Issue:
cache: need to add a per-DB uniquifier to the cache key
RocksDB allows a cache to be shared amongst multiple instances, but doing so with Pebble does not currently work. The problem is that the cache keys are simply `(file-num,offset)`. That key is unique within a DB instance, but not across multiple instances. 

Pull request description:
cache: add a dbNum argument to cache methods
Allow a cache to be shared by multiple Pebble instances by adding a
`dbNum` argument to the cache methods which take a `fileNum`. Plumb this
through everywhere, which is the majority of changes.

Provide a global `dbNum` allocator. This "uniquifier" only needs to
provide uniqueness within a single process.

Change the default options to create an 8 MB cache rather than not
caching at all. This matches the RocksDB behavior.

Fixes #206

Patch:
diff --git a/cache/clockpro.go b/cache/clockpro.go
index 642da49..bef51bc 100644
--- a/cache/clockpro.go
+++ b/cache/clockpro.go
@@ -46,9 +46,14 @@ func (p entryType) String() string {
 	return "unknown"
 }
 
-type key struct {
+type fileKey struct {
+	dbNum   uint64
 	fileNum uint64
-	offset  uint64
+}
+
+type key struct {
+	fileKey
+	offset uint64
 }
 
 type value struct {
@@ -213,8 +218,8 @@ type shard struct {
 
 	maxSize  int64
 	coldSize int64
-	blocks   map[key]*entry    // fileNum+offset -> block
-	files    map[uint64]*entry // fileNum -> list of blocks
+	blocks   map[key]*entry     // fileNum+offset -> block
+	files    map[fileKey]*entry // fileNum -> list of blocks
 
 	handHot  *entry
 	handCold *entry
@@ -225,9 +230,9 @@ type shard struct {
 	countTest int64
 }
 
-func (c *shard) Get(fileNum, offset uint64) Handle {
+func (c *shard) Get(dbNum, fileNum, offset uint64) Handle {
 	c.mu.RLock()
-	e := c.blocks[key{fileNum: fileNum, offset: offset}]
+	e := c.blocks[key{fileKey{dbNum, fileNum}, offset}]
 	var value *value
 	if e != nil {
 		value = e.getValue()
@@ -242,11 +247,11 @@ func (c *shard) Get(fileNum, offset uint64) Handle {
 	return Handle{value: value, free: c.free}
 }
 
-func (c *shard) Set(fileNum, offset uint64, value []byte) Handle {
+func (c *shard) Set(dbNum, fileNum, offset uint64, value []byte) Handle {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	k := key{fileNum: fileNum, offset: offset}
+	k := key{fileKey{dbNum, fileNum}, offset}
 	e := c.blocks[k]
 	v := newValue(value)
 
@@ -291,11 +296,11 @@ func (c *shard) Set(fileNum, offset uint64, value []byte) Handle {
 }
 
 // EvictFile evicts all of the cache values for the specified file.
-func (c *shard) EvictFile(fileNum uint64) {
+func (c *shard) EvictFile(dbNum, fileNum uint64) {
 	c.mu.Lock()
 	defer c.mu.Unlock()
 
-	blocks := c.files[fileNum]
+	blocks := c.files[fileKey{dbNum, fileNum}]
 	if blocks == nil {
 		return
 	}
@@ -342,8 +347,8 @@ func (c *shard) metaAdd(key key, e *entry) {
 		c.handCold = c.handCold.prev()
 	}
 
-	if fileBlocks := c.files[key.fileNum]; fileBlocks == nil {
-		c.files[key.fileNum] = e
+	if fileBlocks := c.files[key.fileKey]; fileBlocks == nil {
+		c.files[key.fileKey] = e
 	} else {
 		fileBlocks.linkFile(e)
 	}
@@ -370,9 +375,9 @@ func (c *shard) metaDel(e *entry) {
 	}
 
 	if next := e.unlinkFile(); e == next {
-		delete(c.files, e.key.fileNum)
+		delete(c.files, e.key.fileKey)
 	} else {
-		c.files[e.key.fileNum] = next
+		c.files[e.key.fileKey] = next
 	}
 }
 
@@ -488,18 +493,23 @@ func newShards(size int64, shards int) *Cache {
 			maxSize:  size / int64(len(c.shards)),
 			coldSize: size / int64(len(c.shards)),
 			blocks:   make(map[key]*entry),
-			files:    make(map[uint64]*entry),
+			files:    make(map[fileKey]*entry),
 		}
 	}
 	return c
 }
 
-func (c *Cache) getShard(fileNum, offset uint64) *shard {
+func (c *Cache) getShard(dbNum, fileNum, offset uint64) *shard {
 	// Inlined version of fnv.New64 + Write.
 	const offset64 = 14695981039346656037
 	const prime64 = 1099511628211
 
 	h := uint64(offset64)
+	for i := 0; i < 8; i++ {
+		h *= prime64
+		h ^= uint64(dbNum & 0xff)
+		dbNum >>= 8
+	}
 	for i := 0; i < 8; i++ {
 		h *= prime64
 		h ^= uint64(fileNum & 0xff)
@@ -516,47 +526,32 @@ func (c *Cache) getShard(fileNum, offset uint64) *shard {
 
 // Get retrieves the cache value for the specified file and offset, returning
 // nil if no value is present.
-func (c *Cache) Get(fileNum, offset uint64) Handle {
-	if c == nil {
-		return Handle{}
-	}
-	return c.getShard(fileNum, offset).Get(fileNum, offset)
+func (c *Cache) Get(dbNum, fileNum, offset uint64) Handle {
+	return c.getShard(dbNum, fileNum, offset).Get(dbNum, fileNum, offset)
 }
 
 // Set sets the cache value for the specified file and offset, overwriting an
 // existing value if present. A Handle is returned which provides faster
 // retrieval of the cached value than Get (lock-free and avoidance of the map
 // lookup).
-func (c *Cache) Set(fileNum, offset uint64, value []byte) Handle {
-	if c == nil {
-		return Handle{value: newValue(value)}
-	}
-	return c.getShard(fileNum, offset).Set(fileNum, offset, value)
+func (c *Cache) Set(dbNum, fileNum, offset uint64, value []byte) Handle {
+	return c.getShard(dbNum, fileNum, offset).Set(dbNum, fileNum, offset, value)
 }
 
 // EvictFile evicts all of the cache values for the specified file.
-func (c *Cache) EvictFile(fileNum uint64) {
-	if c == nil {
-		return
-	}
+func (c *Cache) EvictFile(dbNum, fileNum uint64) {
 	for i := range c.shards {
-		c.shards[i].EvictFile(fileNum)
+		c.shards[i].EvictFile(dbNum, fileNum)
 	}
 }
 
 // MaxSize returns the max size of the cache.
 func (c *Cache) MaxSize() int64 {
-	if c == nil {
-		return 0
-	}
 	return c.maxSize
 }
 
 // Size returns the current space used by the cache.
 func (c *Cache) Size() int64 {
-	if c == nil {
-		return 0
-	}
 	var size int64
 	for i := range c.shards {
 		size += c.shards[i].Size()
@@ -567,9 +562,6 @@ func (c *Cache) Size() int64 {
 // Alloc allocates a byte slice of the specified size, possibly reusing
 // previously allocated but unused memory.
 func (c *Cache) Alloc(n int) []byte {
-	if c == nil {
-		return make([]byte, n)
-	}
 	a := c.allocPool.Get().(*allocCache)
 	b := a.alloc(n)
 	c.allocPool.Put(a)
@@ -579,9 +571,6 @@ func (c *Cache) Alloc(n int) []byte {
 // Free frees the specified slice of memory. The buffer will possibly be
 // reused, making it invalid to use the buffer after calling Free.
 func (c *Cache) Free(b []byte) {
-	if c == nil {
-		return
-	}
 	a := c.allocPool.Get().(*allocCache)
 	a.free(b)
 	c.allocPool.Put(a)
diff --git a/db.go b/db.go
index 103f5c3..12b3607 100644
--- a/db.go
+++ b/db.go
@@ -140,6 +140,7 @@ type Writer interface {
 //		Comparer: myComparer,
 //	})
 type DB struct {
+	dbNum          uint64
 	dirname        string
 	walDirname     string
 	opts           *Options
diff --git a/ingest.go b/ingest.go
index 888b724..a4759f2 100644
--- a/ingest.go
+++ b/ingest.go
@@ -28,7 +28,7 @@ func sstableKeyCompare(userCmp Compare, a, b InternalKey) int {
 	return 0
 }
 
-func ingestLoad1(opts *Options, path string, fileNum uint64) (*fileMetadata, error) {
+func ingestLoad1(opts *Options, path string, dbNum, fileNum uint64) (*fileMetadata, error) {
 	stat, err := opts.FS.Stat(path)
 	if err != nil {
 		return nil, err
@@ -39,7 +39,7 @@ func ingestLoad1(opts *Options, path string, fileNum uint64) (*fileMetadata, err
 		return nil, err
 	}
 
-	r := sstable.NewReader(f, fileNum, opts)
+	r := sstable.NewReader(f, dbNum, fileNum, opts)
 	defer r.Close()
 
 	meta := &fileMetadata{}
@@ -85,11 +85,13 @@ func ingestLoad1(opts *Options, path string, fileNum uint64) (*fileMetadata, err
 	return meta, nil
 }
 
-func ingestLoad(opts *Options, paths []string, pending []uint64) ([]*fileMetadata, error) {
+func ingestLoad(
+	opts *Options, paths []string, dbNum uint64, pending []uint64,
+) ([]*fileMetadata, error) {
 	meta := make([]*fileMetadata, len(paths))
 	for i := range paths {
 		var err error
-		meta[i], err = ingestLoad1(opts, paths[i], pending[i])
+		meta[i], err = ingestLoad1(opts, paths[i], dbNum, pending[i])
 		if err != nil {
 			return nil, err
 		}
@@ -277,7 +279,7 @@ func (d *DB) Ingest(paths []string) error {
 	}()
 
 	// Load the metadata for all of the files being ingested.
-	meta, err := ingestLoad(d.opts, paths, pendingOutputs)
+	meta, err := ingestLoad(d.opts, paths, d.dbNum, pendingOutputs)
 	if err != nil {
 		return err
 	}
diff --git a/internal/base/options.go b/internal/base/options.go
index 6e249dc..0d6087c 100644
--- a/internal/base/options.go
+++ b/internal/base/options.go
@@ -213,7 +213,9 @@ type Options struct {
 	// The default value is 512KB.
 	BytesPerSync int
 
-	// TODO(peter): provide a cache interface.
+	// Cache is used to cache uncompressed blocks from sstables.
+	//
+	// The default cache size is 8 MB.
 	Cache *cache.Cache
 
 	// Comparer defines a total ordering over the space of []byte keys: a 'less
@@ -223,6 +225,14 @@ type Options struct {
 	// The default value uses the same ordering as bytes.Compare.
 	Comparer *Comparer
 
+	// Comparers is a map from comparer name to comparer. It is used for
+	// debugging tools which may be used on multiple databases configured with
+	// different comparers. It is not necessary to populate this comparers map
+	// during normal usage of a DB.
+	//
+	// TODO(peter): unimplemented.
+	Comparers map[string]*Comparer
+
 	// Disable the write-ahead log (WAL). Disabling the write-ahead log prohibits
 	// crash recovery, but can improve performance if crash recovery is not
 	// needed (e.g. when only temporary state is being stored in the database).
@@ -239,6 +249,14 @@ type Options struct {
 	// flushes, compactions, and table deletion.
 	EventListener EventListener
 
+	// Filters is a map from filter policy name to filter policy. It is used for
+	// debugging tools which may be used on multiple databases configured with
+	// different filter policies. It is not necessary to populate this filters
+	// map during normal usage of a DB.
+	//
+	// TODO(peter): unimplemented.
+	Filters map[string]FilterPolicy
+
 	// FS provides the interface for persistent file storage.
 	//
 	// The default value uses the underlying operating system's file system.
@@ -296,6 +314,14 @@ type Options struct {
 	// The default merger concatenates values.
 	Merger *Merger
 
+	// Mergers is a map from merger name to merger. It is used for debugging
+	// tools which may be used on multiple databases configured with different
+	// mergers. It is not necessary to populate this mergers map during normal
+	// usage of a DB.
+	//
+	// TODO(peter): unimplemented.
+	Mergers map[string]*Merger
+
 	// MinCompactionRate sets the minimum rate at which compactions occur. The
 	// default is 4 MB/s.
 	MinCompactionRate int
@@ -332,7 +358,10 @@ func (o *Options) EnsureDefaults() *Options {
 		o = &Options{}
 	}
 	if o.BytesPerSync <= 0 {
-		o.BytesPerSync = 512 << 10
+		o.BytesPerSync = 512 << 10 // 512 KB
+	}
+	if o.Cache == nil {
+		o.Cache = cache.New(8 << 20) // 8 MB
 	}
 	if o.Comparer == nil {
 		o.Comparer = DefaultComparer
diff --git a/open.go b/open.go
index 97e67d6..2c96b47 100644
--- a/open.go
+++ b/open.go
@@ -12,6 +12,7 @@ import (
 	"os"
 	"path/filepath"
 	"sort"
+	"sync"
 
 	"github.com/petermattis/pebble/internal/arenaskl"
 	"github.com/petermattis/pebble/internal/rate"
@@ -19,6 +20,19 @@ import (
 	"github.com/petermattis/pebble/vfs"
 )
 
+var dbNumAlloc = struct {
+	sync.Mutex
+	seq uint64
+}{seq: 1}
+
+func allocDBNum() uint64 {
+	dbNumAlloc.Lock()
+	num := dbNumAlloc.seq
+	dbNumAlloc.seq++
+	dbNumAlloc.Unlock()
+	return num
+}
+
 func createDB(dirname string, opts *Options) (retErr error) {
 	const manifestFileNum = 1
 	ve := versionEdit{
@@ -57,6 +71,7 @@ func createDB(dirname string, opts *Options) (retErr error) {
 func Open(dirname string, opts *Options) (*DB, error) {
 	opts = opts.EnsureDefaults()
 	d := &DB{
+		dbNum:          allocDBNum(),
 		dirname:        dirname,
 		walDirname:     opts.WALDir,
 		opts:           opts,
@@ -74,7 +89,7 @@ func Open(dirname string, opts *Options) (*DB, error) {
 	if tableCacheSize < minTableCacheSize {
 		tableCacheSize = minTableCacheSize
 	}
-	d.tableCache.init(dirname, opts.FS, d.opts, tableCacheSize, defaultTableCacheHitBuffer)
+	d.tableCache.init(d.dbNum, dirname, opts.FS, d.opts, tableCacheSize, defaultTableCacheHitBuffer)
 	d.newIters = d.tableCache.newIters
 	d.commit = newCommitPipeline(commitEnv{
 		logSeqNum:     &d.mu.versions.logSeqNum,
diff --git a/ptable/reader.go b/ptable/reader.go
index c99c36b..7b555c8 100644
--- a/ptable/reader.go
+++ b/ptable/reader.go
@@ -125,7 +125,8 @@ func (i *Iter) loadBlock() {
 // Reader ...
 type Reader struct {
 	file    vfs.File
-	fileNum uint64 // TODO(peter): needed for block cache
+	dbNum   uint64
+	fileNum uint64
 	err     error
 	index   []byte
 	cache   *cache.Cache
@@ -137,6 +138,7 @@ func NewReader(f vfs.File, fileNum uint64, o *base.Options) *Reader {
 	o = o.EnsureDefaults()
 	r := &Reader{
 		file:    f,
+		dbNum:   0, // TODO(peter): needed for block cache
 		fileNum: fileNum,
 		cache:   o.Cache,
 		cmp:     o.Comparer.Compare,
@@ -249,7 +251,7 @@ func (r *Reader) NewIter() *Iter {
 
 // readBlock reads and decompresses a block from disk into memory.
 func (r *Reader) readBlock(bh blockHandle) (cache.Handle, error) {
-	if h := r.cache.Get(r.fileNum, bh.offset); h.Get() != nil {
+	if h := r.cache.Get(r.dbNum, r.fileNum, bh.offset); h.Get() != nil {
 		return h, nil
 	}
 
@@ -265,14 +267,14 @@ func (r *Reader) readBlock(bh blockHandle) (cache.Handle, error) {
 	switch b[bh.length] {
 	case noCompressionBlockType:
 		b = b[:bh.length]
-		h := r.cache.Set(r.fileNum, bh.offset, b)
+		h := r.cache.Set(r.dbNum, r.fileNum, bh.offset, b)
 		return h, nil
 	case snappyCompressionBlockType:
 		b, err := snappy.Decode(nil, b[:bh.length])
 		if err != nil {
 			return cache.Handle{}, err
 		}
-		h := r.cache.Set(r.fileNum, bh.offset, b)
+		h := r.cache.Set(r.dbNum, r.fileNum, bh.offset, b)
 		return h, nil
 	}
 	return cache.Handle{}, fmt.Errorf("pebble/table: unknown block compression: %d", b[bh.length])
diff --git a/sstable/reader.go b/sstable/reader.go
index 89b92a7..6204330 100644
--- a/sstable/reader.go
+++ b/sstable/reader.go
@@ -578,6 +578,7 @@ type blockTransform func([]byte) ([]byte, error)
 // Reader is a table reader.
 type Reader struct {
 	file              vfs.File
+	dbNum             uint64
 	fileNum           uint64
 	err               error
 	index             weakCachedBlock
@@ -738,7 +739,7 @@ func (r *Reader) readWeakCachedBlock(
 func (r *Reader) readBlock(
 	bh blockHandle, transform blockTransform,
 ) (cache.Handle, error) {
-	if h := r.cache.Get(r.fileNum, bh.offset); h.Get() != nil {
+	if h := r.cache.Get(r.dbNum, r.fileNum, bh.offset); h.Get() != nil {
 		return h, nil
 	}
 
@@ -784,7 +785,7 @@ func (r *Reader) readBlock(
 		}
 	}
 
-	h := r.cache.Set(r.fileNum, bh.offset, b)
+	h := r.cache.Set(r.dbNum, r.fileNum, bh.offset, b)
 	return h, nil
 }
 
@@ -909,10 +910,11 @@ func (r *Reader) readMetaindex(metaindexBH blockHandle, o *Options) error {
 
 // NewReader returns a new table reader for the file. Closing the reader will
 // close the file.
-func NewReader(f vfs.File, fileNum uint64, o *Options) *Reader {
+func NewReader(f vfs.File, dbNum, fileNum uint64, o *Options) *Reader {
 	o = o.EnsureDefaults()
 	r := &Reader{
 		file:    f,
+		dbNum:   dbNum,
 		fileNum: fileNum,
 		opts:    o,
 		cache:   o.Cache,
diff --git a/table_cache.go b/table_cache.go
index 99b77f3..602e15e 100644
--- a/table_cache.go
+++ b/table_cache.go
@@ -25,10 +25,12 @@ type tableCache struct {
 	shards []tableCacheShard
 }
 
-func (c *tableCache) init(dirname string, fs vfs.FS, opts *Options, size, hitBuffer int) {
+func (c *tableCache) init(
+	dbNum uint64, dirname string, fs vfs.FS, opts *Options, size, hitBuffer int,
+) {
 	c.shards = make([]tableCacheShard, runtime.NumCPU())
 	for i := range c.shards {
-		c.shards[i].init(dirname, fs, opts, size/len(c.shards), hitBuffer)
+		c.shards[i].init(dbNum, dirname, fs, opts, size/len(c.shards), hitBuffer)
 	}
 }
 
@@ -57,6 +59,7 @@ func (c *tableCache) Close() error {
 }
 
 type tableCacheShard struct {
+	dbNum   uint64
 	dirname string
 	fs      vfs.FS
 	opts    *Options
@@ -75,7 +78,10 @@ type tableCacheShard struct {
 	hitsPool  *sync.Pool
 }
 
-func (c *tableCacheShard) init(dirname string, fs vfs.FS, opts *Options, size, hitBuffer int) {
+func (c *tableCacheShard) init(
+	dbNum uint64, dirname string, fs vfs.FS, opts *Options, size, hitBuffer int,
+) {
+	c.dbNum = dbNum
 	c.dirname = dirname
 	c.fs = fs
 	c.opts = opts
@@ -260,7 +266,7 @@ func (c *tableCacheShard) evict(fileNum uint64) {
 	}
 	c.mu.Unlock()
 
-	c.opts.Cache.EvictFile(fileNum)
+	c.opts.Cache.EvictFile(c.dbNum, fileNum)
 }
 
 func (c *tableCacheShard) recordHits(hits []*tableCacheNode) {
@@ -338,7 +344,7 @@ func (n *tableCacheNode) load(c *tableCacheShard) {
 		close(n.loaded)
 		return
 	}
-	r := sstable.NewReader(f, n.meta.fileNum, c.opts)
+	r := sstable.NewReader(f, c.dbNum, n.meta.fileNum, c.opts)
 	if n.meta.smallestSeqNum == n.meta.largestSeqNum {
 		r.Properties.GlobalSeqNum = n.meta.largestSeqNum
 	}


Test patch:
diff --git a/cache/clockpro_test.go b/cache/clockpro_test.go
index 200d390..924a7d5 100644
--- a/cache/clockpro_test.go
+++ b/cache/clockpro_test.go
@@ -32,9 +32,9 @@ func TestCache(t *testing.T) {
 		wantHit := fields[1][0] == 'h'
 
 		var hit bool
-		h := cache.Get(uint64(key), 0)
+		h := cache.Get(0, uint64(key), 0)
 		if v := h.Get(); v == nil {
-			cache.Set(uint64(key), 0, append([]byte(nil), fields[0][0]))
+			cache.Set(0, uint64(key), 0, append([]byte(nil), fields[0][0]))
 		} else {
 			hit = true
 			if !bytes.Equal(v, fields[0][:1]) {
@@ -51,8 +51,8 @@ func TestCache(t *testing.T) {
 
 func TestWeakHandle(t *testing.T) {
 	cache := newShards(5, 1)
-	cache.Set(1, 0, bytes.Repeat([]byte("a"), 5))
-	h := cache.Set(0, 0, bytes.Repeat([]byte("b"), 5))
+	cache.Set(0, 1, 0, bytes.Repeat([]byte("a"), 5))
+	h := cache.Set(0, 0, 0, bytes.Repeat([]byte("b"), 5))
 	if v := h.Get(); string(v) != "bbbbb" {
 		t.Fatalf("expected bbbbb, but found %v", v)
 	}
@@ -61,7 +61,7 @@ func TestWeakHandle(t *testing.T) {
 	if v := w.Get(); string(v) != "bbbbb" {
 		t.Fatalf("expected bbbbb, but found %v", v)
 	}
-	cache.Set(2, 0, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 2, 0, bytes.Repeat([]byte("a"), 5))
 	if v := w.Get(); v != nil {
 		t.Fatalf("expected nil, but found %s", v)
 	}
@@ -69,24 +69,45 @@ func TestWeakHandle(t *testing.T) {
 
 func TestEvictFile(t *testing.T) {
 	cache := newShards(100, 1)
-	cache.Set(0, 0, bytes.Repeat([]byte("a"), 5))
-	cache.Set(1, 0, bytes.Repeat([]byte("a"), 5))
-	cache.Set(2, 0, bytes.Repeat([]byte("a"), 5))
-	cache.Set(2, 1, bytes.Repeat([]byte("a"), 5))
-	cache.Set(2, 2, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 0, 0, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 1, 0, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 2, 0, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 2, 1, bytes.Repeat([]byte("a"), 5))
+	cache.Set(0, 2, 2, bytes.Repeat([]byte("a"), 5))
 	if expected, size := int64(25), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
-	cache.EvictFile(0)
+	cache.EvictFile(0, 0)
 	if expected, size := int64(20), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
-	cache.EvictFile(1)
+	cache.EvictFile(0, 1)
 	if expected, size := int64(15), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
-	cache.EvictFile(2)
+	cache.EvictFile(0, 2)
 	if expected, size := int64(0), cache.Size(); expected != size {
 		t.Fatalf("expected cache size %d, but found %d", expected, size)
 	}
 }
+
+func TestMultipleDBs(t *testing.T) {
+	cache := newShards(100, 1)
+	cache.Set(0, 0, 0, bytes.Repeat([]byte("a"), 5))
+	cache.Set(1, 0, 0, bytes.Repeat([]byte("b"), 5))
+	if expected, size := int64(10), cache.Size(); expected != size {
+		t.Fatalf("expected cache size %d, but found %d", expected, size)
+	}
+	cache.EvictFile(0, 0)
+	if expected, size := int64(5), cache.Size(); expected != size {
+		t.Fatalf("expected cache size %d, but found %d", expected, size)
+	}
+	h := cache.Get(0, 0, 0)
+	if v := h.Get(); v != nil {
+		t.Fatalf("expected not present, but found %s", v)
+	}
+	h = cache.Get(1, 0, 0)
+	if v := h.Get(); string(v) != "bbbbb" {
+		t.Fatalf("expected bbbbb, but found %v", v)
+	}
+}
diff --git a/compaction_test.go b/compaction_test.go
index fcefba1..40ab941 100644
--- a/compaction_test.go
+++ b/compaction_test.go
@@ -624,7 +624,7 @@ func TestCompaction(t *testing.T) {
 					return "", "", fmt.Errorf("Open: %v", err)
 				}
 				defer f.Close()
-				r := sstable.NewReader(f, meta.fileNum, nil)
+				r := sstable.NewReader(f, 0, meta.fileNum, nil)
 				defer r.Close()
 				ss = append(ss, get1(r.NewIter(nil /* lower */, nil /* upper */))+".")
 			}
diff --git a/ingest_test.go b/ingest_test.go
index 8755028..d16be1a 100644
--- a/ingest_test.go
+++ b/ingest_test.go
@@ -50,7 +50,7 @@ func TestIngestLoad(t *testing.T) {
 				Comparer: DefaultComparer,
 				FS:       mem,
 			}
-			meta, err := ingestLoad(opts, []string{"ext"}, []uint64{1})
+			meta, err := ingestLoad(opts, []string{"ext"}, 0, []uint64{1})
 			if err != nil {
 				return err.Error()
 			}
@@ -130,7 +130,7 @@ func TestIngestLoadRand(t *testing.T) {
 		Comparer: DefaultComparer,
 		FS:       mem,
 	}
-	meta, err := ingestLoad(opts, paths, pending)
+	meta, err := ingestLoad(opts, paths, 0, pending)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -144,7 +144,7 @@ func TestIngestLoadNonExistent(t *testing.T) {
 		Comparer: DefaultComparer,
 		FS:       vfs.NewMem(),
 	}
-	if _, err := ingestLoad(opts, []string{"non-existent"}, []uint64{1}); err == nil {
+	if _, err := ingestLoad(opts, []string{"non-existent"}, 0, []uint64{1}); err == nil {
 		t.Fatalf("expected error, but found success")
 	}
 }
@@ -161,7 +161,7 @@ func TestIngestLoadEmpty(t *testing.T) {
 		Comparer: DefaultComparer,
 		FS:       mem,
 	}
-	if _, err := ingestLoad(opts, []string{"empty"}, []uint64{1}); err == nil {
+	if _, err := ingestLoad(opts, []string{"empty"}, 0, []uint64{1}); err == nil {
 		t.Fatalf("expected error, but found success")
 	}
 }
diff --git a/internal/base/options_test.go b/internal/base/options_test.go
index 94c8405..0f1c000 100644
--- a/internal/base/options_test.go
+++ b/internal/base/options_test.go
@@ -41,7 +41,7 @@ func TestOptionsString(t *testing.T) {
 
 [Options]
   bytes_per_sync=524288
-  cache_size=0
+  cache_size=8388608
   comparer=leveldb.BytewiseComparator
   disable_wal=false
   l0_compaction_threshold=4
diff --git a/level_iter_test.go b/level_iter_test.go
index f9a948d..b3d6e88 100644
--- a/level_iter_test.go
+++ b/level_iter_test.go
@@ -187,7 +187,7 @@ func TestLevelIterBoundaries(t *testing.T) {
 			if err != nil {
 				return err.Error()
 			}
-			readers = append(readers, sstable.NewReader(f1, 0, nil))
+			readers = append(readers, sstable.NewReader(f1, 0, 0, nil))
 			files = append(files, fileMetadata{
 				fileNum:  fileNum,
 				smallest: meta.Smallest(cmp),
@@ -258,7 +258,7 @@ func buildLevelIterTables(
 		if err != nil {
 			b.Fatal(err)
 		}
-		readers[i] = sstable.NewReader(f, uint64(i), &Options{
+		readers[i] = sstable.NewReader(f, 0, uint64(i), &Options{
 			Cache: cache,
 		})
 	}
diff --git a/merging_iter_test.go b/merging_iter_test.go
index 7a0c2b9..2c5bdd1 100644
--- a/merging_iter_test.go
+++ b/merging_iter_test.go
@@ -180,7 +180,7 @@ func buildMergingIterTables(
 		if err != nil {
 			b.Fatal(err)
 		}
-		readers[i] = sstable.NewReader(f, uint64(i), &Options{
+		readers[i] = sstable.NewReader(f, 0, uint64(i), &Options{
 			Cache: cache,
 		})
 	}
diff --git a/sstable/properties_test.go b/sstable/properties_test.go
index 2e2d482..44c881a 100644
--- a/sstable/properties_test.go
+++ b/sstable/properties_test.go
@@ -78,7 +78,7 @@ func TestPropertiesLoad(t *testing.T) {
 			t.Fatal(err)
 		}
 		defer f.Close()
-		r := NewReader(f, 0, nil)
+		r := NewReader(f, 0, 0, nil)
 
 		if diff := pretty.Diff(expected, r.Properties); diff != nil {
 			t.Fatalf("%s", strings.Join(diff, "\n"))
diff --git a/sstable/reader_test.go b/sstable/reader_test.go
index 5b52880..0674946 100644
--- a/sstable/reader_test.go
+++ b/sstable/reader_test.go
@@ -137,6 +137,7 @@ func runTestReader(t *testing.T, o Options, dir string) {
 
 	mem := vfs.NewMem()
 	var r *Reader
+	var dbNum uint64
 
 	datadriven.Walk(t, dir, func(t *testing.T, path string) {
 		datadriven.RunTest(t, path, func(d *datadriven.TestData) string {
@@ -162,7 +163,8 @@ func runTestReader(t *testing.T, o Options, dir string) {
 				if err != nil {
 					return err.Error()
 				}
-				r = NewReader(f, 0, &o)
+				r = NewReader(f, dbNum, 0, &o)
+				dbNum++
 				return ""
 
 			case "iter":
@@ -263,11 +265,12 @@ func TestBytesIteratedCompressed(t *testing.T) {
 			r := buildTestTable(t, numEntries, blockSize, SnappyCompression)
 			var bytesIterated uint64
 			citer := r.NewCompactionIter(&bytesIterated)
-			for citer.First(); citer.Valid(); citer.Next() {}
+			for citer.First(); citer.Valid(); citer.Next() {
+			}
 
 			expected := r.Properties.DataSize
 			// There is some inaccuracy due to compression estimation.
-			if bytesIterated < expected * 99/100 || bytesIterated > expected * 101/100 {
+			if bytesIterated < expected*99/100 || bytesIterated > expected*101/100 {
 				t.Fatalf("bytesIterated: got %d, want %d", bytesIterated, expected)
 			}
 		}
@@ -280,7 +283,8 @@ func TestBytesIteratedUncompressed(t *testing.T) {
 			r := buildTestTable(t, numEntries, blockSize, NoCompression)
 			var bytesIterated uint64
 			citer := r.NewCompactionIter(&bytesIterated)
-			for citer.First(); citer.Valid(); citer.Next() {}
+			for citer.First(); citer.Valid(); citer.Next() {
+			}
 
 			expected := r.Properties.DataSize
 			if bytesIterated != expected {
@@ -306,8 +310,8 @@ func buildTestTable(t *testing.T, numEntries uint64, blockSize int, compression
 
 	var ikey InternalKey
 	for i := uint64(0); i < numEntries; i++ {
-		key := make([]byte, 8 + i%3)
-		value := make([]byte, 7 + i%5)
+		key := make([]byte, 8+i%3)
+		value := make([]byte, 7+i%5)
 		binary.BigEndian.PutUint64(key, i)
 		ikey.UserKey = key
 		w.Add(ikey, value)
@@ -322,7 +326,7 @@ func buildTestTable(t *testing.T, numEntries uint64, blockSize int, compression
 	if err != nil {
 		t.Fatal(err)
 	}
-	return NewReader(f1, 0, &Options{
+	return NewReader(f1, 0, 0, &Options{
 		Cache: cache.New(128 << 20),
 	})
 }
@@ -360,7 +364,7 @@ func buildBenchmarkTable(b *testing.B, blockSize, restartInterval int) (*Reader,
 	if err != nil {
 		b.Fatal(err)
 	}
-	return NewReader(f1, 0, &Options{
+	return NewReader(f1, 0, 0, &Options{
 		Cache: cache.New(128 << 20),
 	}), keys
 }
diff --git a/sstable/table_test.go b/sstable/table_test.go
index 2e70f33..baf057f 100644
--- a/sstable/table_test.go
+++ b/sstable/table_test.go
@@ -114,7 +114,7 @@ func init() {
 }
 
 func check(f vfs.File, comparer *Comparer, fp FilterPolicy) error {
-	r := NewReader(f, 0, &Options{
+	r := NewReader(f, 0, 0, &Options{
 		Comparer: comparer,
 		Levels: []TableOptions{{
 			FilterPolicy: fp,
@@ -463,7 +463,7 @@ func TestBloomFilterFalsePositiveRate(t *testing.T) {
 	c := &countingFilterPolicy{
 		FilterPolicy: bloom.FilterPolicy(1),
 	}
-	r := NewReader(f, 0, &Options{
+	r := NewReader(f, 0, 0, &Options{
 		Levels: []TableOptions{{
 			FilterPolicy: c,
 		}},
@@ -592,7 +592,7 @@ func TestFinalBlockIsWritten(t *testing.T) {
 				t.Errorf("nk=%d, vLen=%d: memFS open: %v", nk, vLen, err)
 				continue
 			}
-			r := NewReader(rf, 0, nil)
+			r := NewReader(rf, 0, 0, nil)
 			i := iterAdapter{r.NewIter(nil /* lower */, nil /* upper */)}
 			for valid := i.First(); valid; valid = i.Next() {
 				got++
@@ -619,7 +619,7 @@ func TestReaderGlobalSeqNum(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	r := NewReader(f, 0, nil)
+	r := NewReader(f, 0, 0, nil)
 	defer r.Close()
 
 	const globalSeqNum = 42
diff --git a/sstable/writer_test.go b/sstable/writer_test.go
index fc9172d..869bf71 100644
--- a/sstable/writer_test.go
+++ b/sstable/writer_test.go
@@ -83,7 +83,7 @@ func TestWriter(t *testing.T) {
 			if err != nil {
 				return err.Error()
 			}
-			r = NewReader(f1, 0, nil)
+			r = NewReader(f1, 0, 0, nil)
 			return fmt.Sprintf("point:   [%s,%s]\nrange:   [%s,%s]\nseqnums: [%d,%d]\n",
 				meta.SmallestPoint, meta.LargestPoint,
 				meta.SmallestRange, meta.LargestRange,
@@ -130,7 +130,7 @@ func TestWriter(t *testing.T) {
 			if err != nil {
 				return err.Error()
 			}
-			r = NewReader(f1, 0, nil)
+			r = NewReader(f1, 0, 0, nil)
 			return fmt.Sprintf("point:   [%s,%s]\nrange:   [%s,%s]\nseqnums: [%d,%d]\n",
 				meta.SmallestPoint, meta.LargestPoint,
 				meta.SmallestRange, meta.LargestRange,
diff --git a/table_cache_test.go b/table_cache_test.go
index aea36d9..1a22c23 100644
--- a/table_cache_test.go
+++ b/table_cache_test.go
@@ -150,7 +150,7 @@ func newTableCache() (*tableCache, *tableCacheTestFS, error) {
 	opts := &Options{}
 	opts.EnsureDefaults()
 	c := &tableCache{}
-	c.init("", fs, opts, tableCacheTestCacheSize, tableCacheTestHitBufferSize)
+	c.init(0, "", fs, opts, tableCacheTestCacheSize, tableCacheTestHitBufferSize)
 	return c, fs, nil
 }
 
@@ -172,7 +172,7 @@ func testTableCacheRandomAccess(t *testing.T, concurrent bool) {
 			rngMu.Unlock()
 			iter, _, err := c.newIters(
 				&fileMetadata{fileNum: uint64(fileNum)},
-				nil /* iter options */,
+				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
 				errc <- fmt.Errorf("i=%d, fileNum=%d: find: %v", i, fileNum, err)
@@ -234,7 +234,7 @@ func TestTableCacheFrequentlyUsed(t *testing.T) {
 		for _, j := range [...]int{pinned0, i % tableCacheTestNumTables, pinned1} {
 			iter, _, err := c.newIters(
 				&fileMetadata{fileNum: uint64(j)},
-				nil /* iter options */,
+				nil, /* iter options */
 				nil /* bytes iterated */)
 			if err != nil {
 				t.Fatalf("i=%d, j=%d: find: %v", i, j, err)
@@ -272,7 +272,7 @@ func TestTableCacheEvictions(t *testing.T) {
 		j := rng.Intn(tableCacheTestNumTables)
 		iter, _, err := c.newIters(
 			&fileMetadata{fileNum: uint64(j)},
-			nil /* iter options */,
+			nil, /* iter options */
 			nil /* bytes iterated */)
 		if err != nil {
 			t.Fatalf("i=%d, j=%d: find: %v", i, j, err)
@@ -315,7 +315,7 @@ func TestTableCacheIterLeak(t *testing.T) {
 	}
 	if _, _, err := c.newIters(
 		&fileMetadata{fileNum: 0},
-		nil /* iter options */,
+		nil, /* iter options */
 		nil /* bytes iterated */); err != nil {
 		t.Fatal(err)
 	}