Initial commit
This commit is contained in:
955
vendor/github.com/cockroachdb/pebble/table_cache.go
generated
vendored
Normal file
955
vendor/github.com/cockroachdb/pebble/table_cache.go
generated
vendored
Normal file
@@ -0,0 +1,955 @@
|
||||
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
|
||||
// of this source code is governed by a BSD-style license that can be found in
|
||||
// the LICENSE file.
|
||||
|
||||
package pebble
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"runtime/pprof"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/internal/invariants"
|
||||
"github.com/cockroachdb/pebble/internal/keyspan"
|
||||
"github.com/cockroachdb/pebble/internal/manifest"
|
||||
"github.com/cockroachdb/pebble/internal/private"
|
||||
"github.com/cockroachdb/pebble/sstable"
|
||||
"github.com/cockroachdb/pebble/vfs"
|
||||
)
|
||||
|
||||
var emptyIter = &errorIter{err: nil}
|
||||
var emptyKeyspanIter = &errorKeyspanIter{err: nil}
|
||||
|
||||
var tableCacheLabels = pprof.Labels("pebble", "table-cache")
|
||||
|
||||
// tableCacheOpts contains the db specific fields
|
||||
// of a table cache. This is stored in the tableCacheContainer
|
||||
// along with the table cache.
|
||||
// NB: It is important to make sure that the fields in this
|
||||
// struct are read-only. Since the fields here are shared
|
||||
// by every single tableCacheShard, if non read-only fields
|
||||
// are updated, we could have unnecessary evictions of those
|
||||
// fields, and the surrounding fields from the CPU caches.
|
||||
type tableCacheOpts struct {
|
||||
atomic struct {
|
||||
// iterCount in the tableCacheOpts keeps track of iterators
|
||||
// opened or closed by a DB. It's used to keep track of
|
||||
// leaked iterators on a per-db level.
|
||||
iterCount *int32
|
||||
}
|
||||
|
||||
logger Logger
|
||||
cacheID uint64
|
||||
dirname string
|
||||
fs vfs.FS
|
||||
opts sstable.ReaderOptions
|
||||
filterMetrics *FilterMetrics
|
||||
}
|
||||
|
||||
// tableCacheContainer contains the table cache and
|
||||
// fields which are unique to the DB.
|
||||
type tableCacheContainer struct {
|
||||
tableCache *TableCache
|
||||
|
||||
// dbOpts contains fields relevant to the table cache
|
||||
// which are unique to each DB.
|
||||
dbOpts tableCacheOpts
|
||||
}
|
||||
|
||||
// newTableCacheContainer will panic if the underlying cache in the table cache
|
||||
// doesn't match Options.Cache.
|
||||
func newTableCacheContainer(
|
||||
tc *TableCache, cacheID uint64, dirname string, fs vfs.FS, opts *Options, size int,
|
||||
) *tableCacheContainer {
|
||||
// We will release a ref to table cache acquired here when tableCacheContainer.close is called.
|
||||
if tc != nil {
|
||||
if tc.cache != opts.Cache {
|
||||
panic("pebble: underlying cache for the table cache and db are different")
|
||||
}
|
||||
tc.Ref()
|
||||
} else {
|
||||
// NewTableCache should create a ref to tc which the container should
|
||||
// drop whenever it is closed.
|
||||
tc = NewTableCache(opts.Cache, opts.Experimental.TableCacheShards, size)
|
||||
}
|
||||
|
||||
t := &tableCacheContainer{}
|
||||
t.tableCache = tc
|
||||
t.dbOpts.logger = opts.Logger
|
||||
t.dbOpts.cacheID = cacheID
|
||||
t.dbOpts.dirname = dirname
|
||||
t.dbOpts.fs = fs
|
||||
t.dbOpts.opts = opts.MakeReaderOptions()
|
||||
t.dbOpts.filterMetrics = &FilterMetrics{}
|
||||
t.dbOpts.atomic.iterCount = new(int32)
|
||||
return t
|
||||
}
|
||||
|
||||
// Before calling close, make sure that there will be no further need
|
||||
// to access any of the files associated with the store.
|
||||
func (c *tableCacheContainer) close() error {
|
||||
// We want to do some cleanup work here. Check for leaked iterators
|
||||
// by the DB using this container. Note that we'll still perform cleanup
|
||||
// below in the case that there are leaked iterators.
|
||||
var err error
|
||||
if v := atomic.LoadInt32(c.dbOpts.atomic.iterCount); v > 0 {
|
||||
err = errors.Errorf("leaked iterators: %d", errors.Safe(v))
|
||||
}
|
||||
|
||||
// Release nodes here.
|
||||
for _, shard := range c.tableCache.shards {
|
||||
if shard != nil {
|
||||
shard.removeDB(&c.dbOpts)
|
||||
}
|
||||
}
|
||||
return firstError(err, c.tableCache.Unref())
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) newIters(
|
||||
file *manifest.FileMetadata, opts *IterOptions, bytesIterated *uint64,
|
||||
) (internalIterator, keyspan.FragmentIterator, error) {
|
||||
return c.tableCache.getShard(file.FileNum).newIters(file, opts, bytesIterated, &c.dbOpts)
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) newRangeKeyIter(
|
||||
file *manifest.FileMetadata, opts *keyspan.SpanIterOptions,
|
||||
) (keyspan.FragmentIterator, error) {
|
||||
return c.tableCache.getShard(file.FileNum).newRangeKeyIter(file, opts, &c.dbOpts)
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) getTableProperties(file *fileMetadata) (*sstable.Properties, error) {
|
||||
return c.tableCache.getShard(file.FileNum).getTableProperties(file, &c.dbOpts)
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) evict(fileNum FileNum) {
|
||||
c.tableCache.getShard(fileNum).evict(fileNum, &c.dbOpts, false)
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) metrics() (CacheMetrics, FilterMetrics) {
|
||||
var m CacheMetrics
|
||||
for i := range c.tableCache.shards {
|
||||
s := c.tableCache.shards[i]
|
||||
s.mu.RLock()
|
||||
m.Count += int64(len(s.mu.nodes))
|
||||
s.mu.RUnlock()
|
||||
m.Hits += atomic.LoadInt64(&s.atomic.hits)
|
||||
m.Misses += atomic.LoadInt64(&s.atomic.misses)
|
||||
}
|
||||
m.Size = m.Count * int64(unsafe.Sizeof(sstable.Reader{}))
|
||||
f := FilterMetrics{
|
||||
Hits: atomic.LoadInt64(&c.dbOpts.filterMetrics.Hits),
|
||||
Misses: atomic.LoadInt64(&c.dbOpts.filterMetrics.Misses),
|
||||
}
|
||||
return m, f
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) withReader(meta *fileMetadata, fn func(*sstable.Reader) error) error {
|
||||
s := c.tableCache.getShard(meta.FileNum)
|
||||
v := s.findNode(meta, &c.dbOpts)
|
||||
defer s.unrefValue(v)
|
||||
if v.err != nil {
|
||||
base.MustExist(c.dbOpts.fs, v.filename, c.dbOpts.logger, v.err)
|
||||
return v.err
|
||||
}
|
||||
return fn(v.reader)
|
||||
}
|
||||
|
||||
func (c *tableCacheContainer) iterCount() int64 {
|
||||
return int64(atomic.LoadInt32(c.dbOpts.atomic.iterCount))
|
||||
}
|
||||
|
||||
// TableCache is a shareable cache for open sstables.
|
||||
type TableCache struct {
|
||||
// atomic contains fields which are accessed atomically. Go allocations
|
||||
// are guaranteed to be 64-bit aligned which we take advantage of by
|
||||
// placing the 64-bit fields which we access atomically at the beginning
|
||||
// of the TableCache struct. For more information, see
|
||||
// https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
|
||||
atomic struct {
|
||||
refs int64
|
||||
}
|
||||
|
||||
cache *Cache
|
||||
shards []*tableCacheShard
|
||||
}
|
||||
|
||||
// Ref adds a reference to the table cache. Once tableCache.init returns,
|
||||
// the table cache only remains valid if there is at least one reference
|
||||
// to it.
|
||||
func (c *TableCache) Ref() {
|
||||
v := atomic.AddInt64(&c.atomic.refs, 1)
|
||||
// We don't want the reference count to ever go from 0 -> 1,
|
||||
// cause a reference count of 0 implies that we've closed the cache.
|
||||
if v <= 1 {
|
||||
panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v))
|
||||
}
|
||||
}
|
||||
|
||||
// Unref removes a reference to the table cache.
|
||||
func (c *TableCache) Unref() error {
|
||||
v := atomic.AddInt64(&c.atomic.refs, -1)
|
||||
switch {
|
||||
case v < 0:
|
||||
panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v))
|
||||
case v == 0:
|
||||
var err error
|
||||
for i := range c.shards {
|
||||
// The cache shard is not allocated yet, nothing to close
|
||||
if c.shards[i] == nil {
|
||||
continue
|
||||
}
|
||||
err = firstError(err, c.shards[i].Close())
|
||||
}
|
||||
|
||||
// Unref the cache which we create a reference to when the tableCache
|
||||
// is first instantiated.
|
||||
c.cache.Unref()
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewTableCache will create a reference to the table cache. It is the callers responsibility
|
||||
// to call tableCache.Unref if they will no longer hold a reference to the table cache.
|
||||
func NewTableCache(cache *Cache, numShards int, size int) *TableCache {
|
||||
if size == 0 {
|
||||
panic("pebble: cannot create a table cache of size 0")
|
||||
} else if numShards == 0 {
|
||||
panic("pebble: cannot create a table cache with 0 shards")
|
||||
}
|
||||
|
||||
c := &TableCache{}
|
||||
c.cache = cache
|
||||
c.cache.Ref()
|
||||
|
||||
c.shards = make([]*tableCacheShard, numShards)
|
||||
for i := range c.shards {
|
||||
c.shards[i] = &tableCacheShard{}
|
||||
c.shards[i].init(size / len(c.shards))
|
||||
}
|
||||
|
||||
// Hold a ref to the cache here.
|
||||
c.atomic.refs = 1
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *TableCache) getShard(fileNum FileNum) *tableCacheShard {
|
||||
return c.shards[uint64(fileNum)%uint64(len(c.shards))]
|
||||
}
|
||||
|
||||
type tableCacheKey struct {
|
||||
cacheID uint64
|
||||
fileNum FileNum
|
||||
}
|
||||
|
||||
type tableCacheShard struct {
|
||||
// WARNING: The following struct `atomic` contains fields are accessed atomically.
|
||||
//
|
||||
// Go allocations are guaranteed to be 64-bit aligned which we take advantage
|
||||
// of by placing the 64-bit fields which we access atomically at the beginning
|
||||
// of the DB struct. For more information, see https://golang.org/pkg/sync/atomic/#pkg-note-BUG.
|
||||
atomic struct {
|
||||
hits int64
|
||||
misses int64
|
||||
iterCount int32
|
||||
}
|
||||
|
||||
size int
|
||||
|
||||
mu struct {
|
||||
sync.RWMutex
|
||||
nodes map[tableCacheKey]*tableCacheNode
|
||||
// The iters map is only created and populated in race builds.
|
||||
iters map[io.Closer][]byte
|
||||
|
||||
handHot *tableCacheNode
|
||||
handCold *tableCacheNode
|
||||
handTest *tableCacheNode
|
||||
|
||||
coldTarget int
|
||||
sizeHot int
|
||||
sizeCold int
|
||||
sizeTest int
|
||||
}
|
||||
releasing sync.WaitGroup
|
||||
releasingCh chan *tableCacheValue
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) init(size int) {
|
||||
c.size = size
|
||||
|
||||
c.mu.nodes = make(map[tableCacheKey]*tableCacheNode)
|
||||
c.mu.coldTarget = size
|
||||
c.releasingCh = make(chan *tableCacheValue, 100)
|
||||
go c.releaseLoop()
|
||||
|
||||
if invariants.RaceEnabled {
|
||||
c.mu.iters = make(map[io.Closer][]byte)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) releaseLoop() {
|
||||
pprof.Do(context.Background(), tableCacheLabels, func(context.Context) {
|
||||
for v := range c.releasingCh {
|
||||
v.release(c)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// checkAndIntersectFilters checks the specific table and block property filters
|
||||
// for intersection with any available table and block-level properties. Returns
|
||||
// true for ok if this table should be read by this iterator.
|
||||
func (c *tableCacheShard) checkAndIntersectFilters(
|
||||
v *tableCacheValue,
|
||||
tableFilter func(userProps map[string]string) bool,
|
||||
blockPropertyFilters []BlockPropertyFilter,
|
||||
) (ok bool, filterer *sstable.BlockPropertiesFilterer, err error) {
|
||||
if tableFilter != nil &&
|
||||
!tableFilter(v.reader.Properties.UserProperties) {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if len(blockPropertyFilters) > 0 {
|
||||
filterer = sstable.NewBlockPropertiesFilterer(blockPropertyFilters)
|
||||
intersects, err :=
|
||||
filterer.IntersectsUserPropsAndFinishInit(v.reader.Properties.UserProperties)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if !intersects {
|
||||
return false, nil, nil
|
||||
}
|
||||
}
|
||||
return true, filterer, nil
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) newIters(
|
||||
file *manifest.FileMetadata, opts *IterOptions, bytesIterated *uint64, dbOpts *tableCacheOpts,
|
||||
) (internalIterator, keyspan.FragmentIterator, error) {
|
||||
// Calling findNode gives us the responsibility of decrementing v's
|
||||
// refCount. If opening the underlying table resulted in error, then we
|
||||
// decrement this straight away. Otherwise, we pass that responsibility to
|
||||
// the sstable iterator, which decrements when it is closed.
|
||||
v := c.findNode(file, dbOpts)
|
||||
if v.err != nil {
|
||||
defer c.unrefValue(v)
|
||||
base.MustExist(dbOpts.fs, v.filename, dbOpts.logger, v.err)
|
||||
return nil, nil, v.err
|
||||
}
|
||||
|
||||
ok := true
|
||||
var filterer *sstable.BlockPropertiesFilterer
|
||||
var err error
|
||||
if opts != nil {
|
||||
ok, filterer, err = c.checkAndIntersectFilters(v, opts.TableFilter, opts.PointKeyFilters)
|
||||
}
|
||||
if err != nil {
|
||||
c.unrefValue(v)
|
||||
return nil, nil, err
|
||||
}
|
||||
if !ok {
|
||||
c.unrefValue(v)
|
||||
// Return the empty iterator. This iterator has no mutable state, so
|
||||
// using a singleton is fine.
|
||||
return emptyIter, nil, err
|
||||
}
|
||||
|
||||
var iter sstable.Iterator
|
||||
useFilter := true
|
||||
if opts != nil {
|
||||
useFilter = manifest.LevelToInt(opts.level) != 6 || opts.UseL6Filters
|
||||
}
|
||||
if bytesIterated != nil {
|
||||
iter, err = v.reader.NewCompactionIter(bytesIterated)
|
||||
} else {
|
||||
iter, err = v.reader.NewIterWithBlockPropertyFilters(
|
||||
opts.GetLowerBound(), opts.GetUpperBound(), filterer, useFilter)
|
||||
}
|
||||
if err != nil {
|
||||
c.unrefValue(v)
|
||||
return nil, nil, err
|
||||
}
|
||||
// NB: v.closeHook takes responsibility for calling unrefValue(v) here.
|
||||
iter.SetCloseHook(func(i sstable.Iterator) error {
|
||||
return v.closeHook(i)
|
||||
})
|
||||
|
||||
atomic.AddInt32(&c.atomic.iterCount, 1)
|
||||
atomic.AddInt32(dbOpts.atomic.iterCount, 1)
|
||||
if invariants.RaceEnabled {
|
||||
c.mu.Lock()
|
||||
c.mu.iters[iter] = debug.Stack()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// NB: range-del iterator does not maintain a reference to the table, nor
|
||||
// does it need to read from it after creation.
|
||||
rangeDelIter, err := v.reader.NewRawRangeDelIter()
|
||||
if err != nil {
|
||||
_ = iter.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
if rangeDelIter != nil {
|
||||
return iter, rangeDelIter, nil
|
||||
}
|
||||
// NB: Translate a nil range-del iterator into a nil interface.
|
||||
return iter, nil, nil
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) newRangeKeyIter(
|
||||
file *manifest.FileMetadata, opts *keyspan.SpanIterOptions, dbOpts *tableCacheOpts,
|
||||
) (keyspan.FragmentIterator, error) {
|
||||
// Calling findNode gives us the responsibility of decrementing v's
|
||||
// refCount. If opening the underlying table resulted in error, then we
|
||||
// decrement this straight away. Otherwise, we pass that responsibility to
|
||||
// the sstable iterator, which decrements when it is closed.
|
||||
v := c.findNode(file, dbOpts)
|
||||
if v.err != nil {
|
||||
defer c.unrefValue(v)
|
||||
base.MustExist(dbOpts.fs, v.filename, dbOpts.logger, v.err)
|
||||
return nil, v.err
|
||||
}
|
||||
|
||||
ok := true
|
||||
var err error
|
||||
if opts != nil {
|
||||
ok, _, err = c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters)
|
||||
}
|
||||
if err != nil {
|
||||
c.unrefValue(v)
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
c.unrefValue(v)
|
||||
// Return the empty iterator. This iterator has no mutable state, so
|
||||
// using a singleton is fine.
|
||||
return emptyKeyspanIter, err
|
||||
}
|
||||
|
||||
var iter sstable.FragmentIterator
|
||||
// TODO(bilal): We are currently passing through the raw blockIter for range
|
||||
// keys. This iter does not support bounds (eg. SetBounds will panic).
|
||||
// Any future users of the iter returned by this function need to make any
|
||||
// bounds-specific optimizations themselves.
|
||||
iter, err = v.reader.NewRawRangeKeyIter()
|
||||
if err != nil || iter == nil {
|
||||
c.unrefValue(v)
|
||||
return nil, err
|
||||
}
|
||||
// NB: v.closeHook takes responsibility for calling unrefValue(v) here.
|
||||
iter.SetCloseHook(func(i keyspan.FragmentIterator) error {
|
||||
return v.closeHook(i)
|
||||
})
|
||||
|
||||
atomic.AddInt32(&c.atomic.iterCount, 1)
|
||||
atomic.AddInt32(dbOpts.atomic.iterCount, 1)
|
||||
if invariants.RaceEnabled {
|
||||
c.mu.Lock()
|
||||
c.mu.iters[iter] = debug.Stack()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
return iter, nil
|
||||
}
|
||||
|
||||
// getTableProperties return sst table properties for target file
|
||||
func (c *tableCacheShard) getTableProperties(
|
||||
file *fileMetadata, dbOpts *tableCacheOpts,
|
||||
) (*sstable.Properties, error) {
|
||||
// Calling findNode gives us the responsibility of decrementing v's refCount here
|
||||
v := c.findNode(file, dbOpts)
|
||||
defer c.unrefValue(v)
|
||||
|
||||
if v.err != nil {
|
||||
return nil, v.err
|
||||
}
|
||||
return &v.reader.Properties, nil
|
||||
}
|
||||
|
||||
// releaseNode releases a node from the tableCacheShard.
|
||||
//
|
||||
// c.mu must be held when calling this.
|
||||
func (c *tableCacheShard) releaseNode(n *tableCacheNode) {
|
||||
c.unlinkNode(n)
|
||||
c.clearNode(n)
|
||||
}
|
||||
|
||||
// unlinkNode removes a node from the tableCacheShard, leaving the shard
|
||||
// reference in place.
|
||||
//
|
||||
// c.mu must be held when calling this.
|
||||
func (c *tableCacheShard) unlinkNode(n *tableCacheNode) {
|
||||
key := tableCacheKey{n.cacheID, n.meta.FileNum}
|
||||
delete(c.mu.nodes, key)
|
||||
|
||||
switch n.ptype {
|
||||
case tableCacheNodeHot:
|
||||
c.mu.sizeHot--
|
||||
case tableCacheNodeCold:
|
||||
c.mu.sizeCold--
|
||||
case tableCacheNodeTest:
|
||||
c.mu.sizeTest--
|
||||
}
|
||||
|
||||
if n == c.mu.handHot {
|
||||
c.mu.handHot = c.mu.handHot.prev()
|
||||
}
|
||||
if n == c.mu.handCold {
|
||||
c.mu.handCold = c.mu.handCold.prev()
|
||||
}
|
||||
if n == c.mu.handTest {
|
||||
c.mu.handTest = c.mu.handTest.prev()
|
||||
}
|
||||
|
||||
if n.unlink() == n {
|
||||
// This was the last entry in the cache.
|
||||
c.mu.handHot = nil
|
||||
c.mu.handCold = nil
|
||||
c.mu.handTest = nil
|
||||
}
|
||||
|
||||
n.links.prev = nil
|
||||
n.links.next = nil
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) clearNode(n *tableCacheNode) {
|
||||
if v := n.value; v != nil {
|
||||
n.value = nil
|
||||
c.unrefValue(v)
|
||||
}
|
||||
}
|
||||
|
||||
// unrefValue decrements the reference count for the specified value, releasing
|
||||
// it if the reference count fell to 0. Note that the value has a reference if
|
||||
// it is present in tableCacheShard.mu.nodes, so a reference count of 0 means
|
||||
// the node has already been removed from that map.
|
||||
func (c *tableCacheShard) unrefValue(v *tableCacheValue) {
|
||||
if atomic.AddInt32(&v.refCount, -1) == 0 {
|
||||
c.releasing.Add(1)
|
||||
c.releasingCh <- v
|
||||
}
|
||||
}
|
||||
|
||||
// findNode returns the node for the table with the given file number, creating
|
||||
// that node if it didn't already exist. The caller is responsible for
|
||||
// decrementing the returned node's refCount.
|
||||
func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) *tableCacheValue {
|
||||
// Fast-path for a hit in the cache.
|
||||
c.mu.RLock()
|
||||
key := tableCacheKey{dbOpts.cacheID, meta.FileNum}
|
||||
if n := c.mu.nodes[key]; n != nil && n.value != nil {
|
||||
// Fast-path hit.
|
||||
//
|
||||
// The caller is responsible for decrementing the refCount.
|
||||
v := n.value
|
||||
atomic.AddInt32(&v.refCount, 1)
|
||||
c.mu.RUnlock()
|
||||
atomic.StoreInt32(&n.referenced, 1)
|
||||
atomic.AddInt64(&c.atomic.hits, 1)
|
||||
<-v.loaded
|
||||
return v
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
n := c.mu.nodes[key]
|
||||
switch {
|
||||
case n == nil:
|
||||
// Slow-path miss of a non-existent node.
|
||||
n = &tableCacheNode{
|
||||
meta: meta,
|
||||
ptype: tableCacheNodeCold,
|
||||
}
|
||||
c.addNode(n, dbOpts)
|
||||
c.mu.sizeCold++
|
||||
|
||||
case n.value != nil:
|
||||
// Slow-path hit of a hot or cold node.
|
||||
//
|
||||
// The caller is responsible for decrementing the refCount.
|
||||
v := n.value
|
||||
atomic.AddInt32(&v.refCount, 1)
|
||||
atomic.StoreInt32(&n.referenced, 1)
|
||||
atomic.AddInt64(&c.atomic.hits, 1)
|
||||
c.mu.Unlock()
|
||||
<-v.loaded
|
||||
return v
|
||||
|
||||
default:
|
||||
// Slow-path miss of a test node.
|
||||
c.unlinkNode(n)
|
||||
c.mu.coldTarget++
|
||||
if c.mu.coldTarget > c.size {
|
||||
c.mu.coldTarget = c.size
|
||||
}
|
||||
|
||||
atomic.StoreInt32(&n.referenced, 0)
|
||||
n.ptype = tableCacheNodeHot
|
||||
c.addNode(n, dbOpts)
|
||||
c.mu.sizeHot++
|
||||
}
|
||||
|
||||
atomic.AddInt64(&c.atomic.misses, 1)
|
||||
|
||||
v := &tableCacheValue{
|
||||
loaded: make(chan struct{}),
|
||||
refCount: 2,
|
||||
}
|
||||
// Cache the closure invoked when an iterator is closed. This avoids an
|
||||
// allocation on every call to newIters.
|
||||
v.closeHook = func(i io.Closer) error {
|
||||
if invariants.RaceEnabled {
|
||||
c.mu.Lock()
|
||||
delete(c.mu.iters, i)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
c.unrefValue(v)
|
||||
atomic.AddInt32(&c.atomic.iterCount, -1)
|
||||
atomic.AddInt32(dbOpts.atomic.iterCount, -1)
|
||||
return nil
|
||||
}
|
||||
n.value = v
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
// Note adding to the cache lists must complete before we begin loading the
|
||||
// table as a failure during load will result in the node being unlinked.
|
||||
pprof.Do(context.Background(), tableCacheLabels, func(context.Context) {
|
||||
v.load(meta, c, dbOpts)
|
||||
})
|
||||
return v
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) addNode(n *tableCacheNode, dbOpts *tableCacheOpts) {
|
||||
c.evictNodes()
|
||||
n.cacheID = dbOpts.cacheID
|
||||
key := tableCacheKey{n.cacheID, n.meta.FileNum}
|
||||
c.mu.nodes[key] = n
|
||||
|
||||
n.links.next = n
|
||||
n.links.prev = n
|
||||
if c.mu.handHot == nil {
|
||||
// First element.
|
||||
c.mu.handHot = n
|
||||
c.mu.handCold = n
|
||||
c.mu.handTest = n
|
||||
} else {
|
||||
c.mu.handHot.link(n)
|
||||
}
|
||||
|
||||
if c.mu.handCold == c.mu.handHot {
|
||||
c.mu.handCold = c.mu.handCold.prev()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) evictNodes() {
|
||||
for c.size <= c.mu.sizeHot+c.mu.sizeCold && c.mu.handCold != nil {
|
||||
c.runHandCold()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) runHandCold() {
|
||||
n := c.mu.handCold
|
||||
if n.ptype == tableCacheNodeCold {
|
||||
if atomic.LoadInt32(&n.referenced) == 1 {
|
||||
atomic.StoreInt32(&n.referenced, 0)
|
||||
n.ptype = tableCacheNodeHot
|
||||
c.mu.sizeCold--
|
||||
c.mu.sizeHot++
|
||||
} else {
|
||||
c.clearNode(n)
|
||||
n.ptype = tableCacheNodeTest
|
||||
c.mu.sizeCold--
|
||||
c.mu.sizeTest++
|
||||
for c.size < c.mu.sizeTest && c.mu.handTest != nil {
|
||||
c.runHandTest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.handCold = c.mu.handCold.next()
|
||||
|
||||
for c.size-c.mu.coldTarget <= c.mu.sizeHot && c.mu.handHot != nil {
|
||||
c.runHandHot()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) runHandHot() {
|
||||
if c.mu.handHot == c.mu.handTest && c.mu.handTest != nil {
|
||||
c.runHandTest()
|
||||
if c.mu.handHot == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
n := c.mu.handHot
|
||||
if n.ptype == tableCacheNodeHot {
|
||||
if atomic.LoadInt32(&n.referenced) == 1 {
|
||||
atomic.StoreInt32(&n.referenced, 0)
|
||||
} else {
|
||||
n.ptype = tableCacheNodeCold
|
||||
c.mu.sizeHot--
|
||||
c.mu.sizeCold++
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.handHot = c.mu.handHot.next()
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) runHandTest() {
|
||||
if c.mu.sizeCold > 0 && c.mu.handTest == c.mu.handCold && c.mu.handCold != nil {
|
||||
c.runHandCold()
|
||||
if c.mu.handTest == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
n := c.mu.handTest
|
||||
if n.ptype == tableCacheNodeTest {
|
||||
c.mu.coldTarget--
|
||||
if c.mu.coldTarget < 0 {
|
||||
c.mu.coldTarget = 0
|
||||
}
|
||||
c.unlinkNode(n)
|
||||
c.clearNode(n)
|
||||
}
|
||||
|
||||
c.mu.handTest = c.mu.handTest.next()
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) evict(fileNum FileNum, dbOpts *tableCacheOpts, allowLeak bool) {
|
||||
c.mu.Lock()
|
||||
key := tableCacheKey{dbOpts.cacheID, fileNum}
|
||||
n := c.mu.nodes[key]
|
||||
var v *tableCacheValue
|
||||
if n != nil {
|
||||
// NB: This is equivalent to tableCacheShard.releaseNode(), but we perform
|
||||
// the tableCacheNode.release() call synchronously below to ensure the
|
||||
// sstable file descriptor is closed before returning. Note that
|
||||
// tableCacheShard.releasing needs to be incremented while holding
|
||||
// tableCacheShard.mu in order to avoid a race with Close()
|
||||
c.unlinkNode(n)
|
||||
v = n.value
|
||||
if v != nil {
|
||||
if !allowLeak {
|
||||
if t := atomic.AddInt32(&v.refCount, -1); t != 0 {
|
||||
dbOpts.logger.Fatalf("sstable %s: refcount is not zero: %d\n%s", fileNum, t, debug.Stack())
|
||||
}
|
||||
}
|
||||
c.releasing.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Unlock()
|
||||
|
||||
if v != nil {
|
||||
v.release(c)
|
||||
}
|
||||
|
||||
dbOpts.opts.Cache.EvictFile(dbOpts.cacheID, fileNum)
|
||||
}
|
||||
|
||||
// removeDB evicts any nodes which have a reference to the DB
|
||||
// associated with dbOpts.cacheID. Make sure that there will
|
||||
// be no more accesses to the files associated with the DB.
|
||||
func (c *tableCacheShard) removeDB(dbOpts *tableCacheOpts) {
|
||||
var fileNums []base.FileNum
|
||||
|
||||
c.mu.RLock()
|
||||
// Collect the fileNums which need to be cleaned.
|
||||
var firstNode *tableCacheNode
|
||||
node := c.mu.handHot
|
||||
for node != firstNode {
|
||||
if firstNode == nil {
|
||||
firstNode = node
|
||||
}
|
||||
|
||||
if node.cacheID == dbOpts.cacheID {
|
||||
fileNums = append(fileNums, node.meta.FileNum)
|
||||
}
|
||||
node = node.next()
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
// Evict all the nodes associated with the DB.
|
||||
// This should synchronously close all the files
|
||||
// associated with the DB.
|
||||
for _, fNum := range fileNums {
|
||||
c.evict(fNum, dbOpts, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *tableCacheShard) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Check for leaked iterators. Note that we'll still perform cleanup below in
|
||||
// the case that there are leaked iterators.
|
||||
var err error
|
||||
if v := atomic.LoadInt32(&c.atomic.iterCount); v > 0 {
|
||||
if !invariants.RaceEnabled {
|
||||
err = errors.Errorf("leaked iterators: %d", errors.Safe(v))
|
||||
} else {
|
||||
var buf bytes.Buffer
|
||||
for _, stack := range c.mu.iters {
|
||||
fmt.Fprintf(&buf, "%s\n", stack)
|
||||
}
|
||||
err = errors.Errorf("leaked iterators: %d\n%s", errors.Safe(v), buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
for c.mu.handHot != nil {
|
||||
n := c.mu.handHot
|
||||
if n.value != nil {
|
||||
if atomic.AddInt32(&n.value.refCount, -1) == 0 {
|
||||
c.releasing.Add(1)
|
||||
c.releasingCh <- n.value
|
||||
}
|
||||
}
|
||||
c.unlinkNode(n)
|
||||
}
|
||||
c.mu.nodes = nil
|
||||
c.mu.handHot = nil
|
||||
c.mu.handCold = nil
|
||||
c.mu.handTest = nil
|
||||
|
||||
// Only shutdown the releasing goroutine if there were no leaked
|
||||
// iterators. If there were leaked iterators, we leave the goroutine running
|
||||
// and the releasingCh open so that a subsequent iterator close can
|
||||
// complete. This behavior is used by iterator leak tests. Leaking the
|
||||
// goroutine for these tests is less bad not closing the iterator which
|
||||
// triggers other warnings about block cache handles not being released.
|
||||
if err == nil {
|
||||
close(c.releasingCh)
|
||||
}
|
||||
c.releasing.Wait()
|
||||
return err
|
||||
}
|
||||
|
||||
type tableCacheValue struct {
|
||||
closeHook func(i io.Closer) error
|
||||
reader *sstable.Reader
|
||||
filename string
|
||||
err error
|
||||
loaded chan struct{}
|
||||
// Reference count for the value. The reader is closed when the reference
|
||||
// count drops to zero.
|
||||
refCount int32
|
||||
}
|
||||
|
||||
func (v *tableCacheValue) load(meta *fileMetadata, c *tableCacheShard, dbOpts *tableCacheOpts) {
|
||||
// Try opening the fileTypeTable first.
|
||||
var f vfs.File
|
||||
v.filename = base.MakeFilepath(dbOpts.fs, dbOpts.dirname, fileTypeTable, meta.FileNum)
|
||||
f, v.err = dbOpts.fs.Open(v.filename, vfs.RandomReadsOption)
|
||||
if v.err == nil {
|
||||
cacheOpts := private.SSTableCacheOpts(dbOpts.cacheID, meta.FileNum).(sstable.ReaderOption)
|
||||
reopenOpt := sstable.FileReopenOpt{FS: dbOpts.fs, Filename: v.filename}
|
||||
v.reader, v.err = sstable.NewReader(f, dbOpts.opts, cacheOpts, dbOpts.filterMetrics, reopenOpt)
|
||||
}
|
||||
if v.err == nil {
|
||||
if meta.SmallestSeqNum == meta.LargestSeqNum {
|
||||
v.reader.Properties.GlobalSeqNum = meta.LargestSeqNum
|
||||
}
|
||||
}
|
||||
if v.err != nil {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
// Lookup the node in the cache again as it might have already been
|
||||
// removed.
|
||||
key := tableCacheKey{dbOpts.cacheID, meta.FileNum}
|
||||
n := c.mu.nodes[key]
|
||||
if n != nil && n.value == v {
|
||||
c.releaseNode(n)
|
||||
}
|
||||
}
|
||||
close(v.loaded)
|
||||
}
|
||||
|
||||
func (v *tableCacheValue) release(c *tableCacheShard) {
|
||||
<-v.loaded
|
||||
// Nothing to be done about an error at this point. Close the reader if it is
|
||||
// open.
|
||||
if v.reader != nil {
|
||||
_ = v.reader.Close()
|
||||
}
|
||||
c.releasing.Done()
|
||||
}
|
||||
|
||||
type tableCacheNodeType int8
|
||||
|
||||
const (
|
||||
tableCacheNodeTest tableCacheNodeType = iota
|
||||
tableCacheNodeCold
|
||||
tableCacheNodeHot
|
||||
)
|
||||
|
||||
func (p tableCacheNodeType) String() string {
|
||||
switch p {
|
||||
case tableCacheNodeTest:
|
||||
return "test"
|
||||
case tableCacheNodeCold:
|
||||
return "cold"
|
||||
case tableCacheNodeHot:
|
||||
return "hot"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
type tableCacheNode struct {
|
||||
meta *fileMetadata
|
||||
value *tableCacheValue
|
||||
|
||||
links struct {
|
||||
next *tableCacheNode
|
||||
prev *tableCacheNode
|
||||
}
|
||||
ptype tableCacheNodeType
|
||||
// referenced is atomically set to indicate that this entry has been accessed
|
||||
// since the last time one of the clock hands swept it.
|
||||
referenced int32
|
||||
|
||||
// Storing the cache id associated with the DB instance here
|
||||
// avoids the need to thread the dbOpts struct through many functions.
|
||||
cacheID uint64
|
||||
}
|
||||
|
||||
func (n *tableCacheNode) next() *tableCacheNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return n.links.next
|
||||
}
|
||||
|
||||
func (n *tableCacheNode) prev() *tableCacheNode {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
return n.links.prev
|
||||
}
|
||||
|
||||
func (n *tableCacheNode) link(s *tableCacheNode) {
|
||||
s.links.prev = n.links.prev
|
||||
s.links.prev.links.next = s
|
||||
s.links.next = n
|
||||
s.links.next.links.prev = s
|
||||
}
|
||||
|
||||
func (n *tableCacheNode) unlink() *tableCacheNode {
|
||||
next := n.links.next
|
||||
n.links.prev.links.next = n.links.next
|
||||
n.links.next.links.prev = n.links.prev
|
||||
n.links.prev = n
|
||||
n.links.next = n
|
||||
return next
|
||||
}
|
||||
Reference in New Issue
Block a user