Upgrade versions of vendored libs
This commit is contained in:
@@ -16,9 +16,9 @@ const (
|
||||
// RecordCacheHitOp happens when a read is satisfied from the block cache. See
|
||||
// objstorage.ReadHandle.RecordCacheHit().
|
||||
RecordCacheHitOp
|
||||
// MaxReadaheadOp is a "meta operation" that configures a read handle for
|
||||
// large sequential reads. See objstorage.ReadHandle.MaxReadahead().
|
||||
MaxReadaheadOp
|
||||
// SetupForCompactionOp is a "meta operation" that configures a read handle
|
||||
// for large sequential reads. See objstorage.ReadHandle.SetupForCompaction().
|
||||
SetupForCompactionOp
|
||||
)
|
||||
|
||||
// Reason indicates the higher-level context of the operation.
|
||||
|
||||
@@ -209,14 +209,14 @@ func (rh *readHandle) Close() error {
|
||||
return rh.rh.Close()
|
||||
}
|
||||
|
||||
// MaxReadahead is part of the objstorage.ReadHandle interface.
|
||||
func (rh *readHandle) MaxReadahead() {
|
||||
// SetupForCompaction is part of the objstorage.ReadHandle interface.
|
||||
func (rh *readHandle) SetupForCompaction() {
|
||||
rh.g.add(context.Background(), Event{
|
||||
Op: MaxReadaheadOp,
|
||||
Op: SetupForCompactionOp,
|
||||
FileNum: rh.fileNum,
|
||||
HandleID: rh.handleID,
|
||||
})
|
||||
rh.rh.MaxReadahead()
|
||||
rh.rh.SetupForCompaction()
|
||||
}
|
||||
|
||||
// RecordCacheHit is part of the objstorage.ReadHandle interface.
|
||||
|
||||
15
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go
generated
vendored
15
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go
generated
vendored
@@ -93,6 +93,21 @@ type Settings struct {
|
||||
// (experimental).
|
||||
Shared struct {
|
||||
Storage shared.Storage
|
||||
|
||||
// CacheSizeBytes is the size of the on-disk block cache for objects
|
||||
// on shared storage. If it is 0, no cache is used.
|
||||
CacheSizeBytes int64
|
||||
|
||||
// CacheBlockSize is the block size of the cache; if 0, the default of 32KB is used.
|
||||
CacheBlockSize int
|
||||
|
||||
// The number of independent shards the cache leverages. Each shard is the same size,
|
||||
// and a hash of filenum & offset map a read to a certain shard. If set to 0,
|
||||
// 2*runtime.GOMAXPROCS is used as the shard count.
|
||||
CacheShardCount int
|
||||
|
||||
// TODO(radu): allow the cache to live on another FS/location (e.g. to use
|
||||
// instance-local SSD).
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
47
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go
generated
vendored
47
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go
generated
vendored
@@ -11,15 +11,15 @@ const (
|
||||
minFileReadsForReadahead = 2
|
||||
// TODO(bilal): Have the initial size value be a factor of the block size,
|
||||
// as opposed to a hardcoded value.
|
||||
initialReadaheadSize = 64 << 10 /* 64KB */
|
||||
maxReadaheadSize = 256 << 10 /* 256KB */
|
||||
initialReadaheadSize = 64 << 10 /* 64KB */
|
||||
)
|
||||
|
||||
// readaheadState contains state variables related to readahead. Updated on
|
||||
// file reads.
|
||||
type readaheadState struct {
|
||||
// Number of sequential reads.
|
||||
numReads int64
|
||||
numReads int64
|
||||
maxReadaheadSize int64
|
||||
// Size issued to the next call to Prefetch. Starts at or above
|
||||
// initialReadaheadSize and grows exponentially until maxReadaheadSize.
|
||||
size int64
|
||||
@@ -32,22 +32,23 @@ type readaheadState struct {
|
||||
limit int64
|
||||
}
|
||||
|
||||
func makeReadaheadState() readaheadState {
|
||||
func makeReadaheadState(maxReadaheadSize int64) readaheadState {
|
||||
return readaheadState{
|
||||
size: initialReadaheadSize,
|
||||
size: initialReadaheadSize,
|
||||
maxReadaheadSize: maxReadaheadSize,
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
|
||||
currentReadEnd := offset + blockLength
|
||||
if rs.numReads >= minFileReadsForReadahead {
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
|
||||
// This is a read that would have resulted in a readahead, had it
|
||||
// not been a cache hit.
|
||||
rs.limit = currentReadEnd
|
||||
return
|
||||
}
|
||||
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize {
|
||||
// We read too far away from rs.limit to benefit from readahead in
|
||||
// any scenario. Reset all variables.
|
||||
rs.numReads = 1
|
||||
@@ -61,7 +62,7 @@ func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
|
||||
// readahead.
|
||||
return
|
||||
}
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
|
||||
// Blocks are being read sequentially and would benefit from readahead
|
||||
// down the line.
|
||||
rs.numReads++
|
||||
@@ -89,28 +90,28 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
// as well as the interval where a read would benefit from read ahead:
|
||||
// [rs.limit, rs.limit + rs.size]
|
||||
// We increase the latter interval to
|
||||
// [rs.limit, rs.limit + maxReadaheadSize] to account for cases where
|
||||
// [rs.limit, rs.limit + rs.maxReadaheadSize] to account for cases where
|
||||
// readahead may not be beneficial with a small readahead size, but over
|
||||
// time the readahead size would increase exponentially to make it
|
||||
// beneficial.
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
|
||||
// We are doing a read in the interval ahead of
|
||||
// the last readahead range. In the diagrams below, ++++ is the last
|
||||
// readahead range, ==== is the range represented by
|
||||
// [rs.limit, rs.limit + maxReadaheadSize], and ---- is the range
|
||||
// [rs.limit, rs.limit + rs.maxReadaheadSize], and ---- is the range
|
||||
// being read.
|
||||
//
|
||||
// rs.limit rs.limit + maxReadaheadSize
|
||||
// rs.limit rs.limit + rs.maxReadaheadSize
|
||||
// ++++++++++|===========================|
|
||||
//
|
||||
// |-------------|
|
||||
// offset currentReadEnd
|
||||
//
|
||||
// This case is also possible, as are all cases with an overlap
|
||||
// between [rs.limit, rs.limit + maxReadaheadSize] and [offset,
|
||||
// between [rs.limit, rs.limit + rs.maxReadaheadSize] and [offset,
|
||||
// currentReadEnd]:
|
||||
//
|
||||
// rs.limit rs.limit + maxReadaheadSize
|
||||
// rs.limit rs.limit + rs.maxReadaheadSize
|
||||
// ++++++++++|===========================|
|
||||
//
|
||||
// |-------------|
|
||||
@@ -122,19 +123,19 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
rs.prevSize = rs.size
|
||||
// Increase rs.size for the next read.
|
||||
rs.size *= 2
|
||||
if rs.size > maxReadaheadSize {
|
||||
rs.size = maxReadaheadSize
|
||||
if rs.size > rs.maxReadaheadSize {
|
||||
rs.size = rs.maxReadaheadSize
|
||||
}
|
||||
return rs.prevSize
|
||||
}
|
||||
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize {
|
||||
// The above conditional has rs.limit > rs.prevSize to confirm that
|
||||
// rs.limit - rs.prevSize would not underflow.
|
||||
// We read too far away from rs.limit to benefit from readahead in
|
||||
// any scenario. Reset all variables.
|
||||
// The case where we read too far ahead:
|
||||
//
|
||||
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize)
|
||||
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
|
||||
// |+++++++++++++|=============|
|
||||
//
|
||||
// |-------------|
|
||||
@@ -142,7 +143,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
//
|
||||
// Or too far behind:
|
||||
//
|
||||
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize)
|
||||
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
|
||||
// |+++++++++++++|=============|
|
||||
//
|
||||
// |-------------|
|
||||
@@ -160,7 +161,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
// readahead, but there's no reason to issue a readahead call at the
|
||||
// moment.
|
||||
//
|
||||
// (rs.limit - rs.prevSize) (rs.limit + maxReadaheadSize)
|
||||
// (rs.limit - rs.prevSize) (rs.limit + rs.maxReadaheadSize)
|
||||
// |+++++++++++++|===============|
|
||||
// (rs.limit)
|
||||
//
|
||||
@@ -170,11 +171,11 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
rs.numReads++
|
||||
return 0
|
||||
}
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
|
||||
if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize {
|
||||
// Blocks are being read sequentially and would benefit from readahead
|
||||
// down the line.
|
||||
//
|
||||
// (rs.limit) (rs.limit + maxReadaheadSize)
|
||||
// (rs.limit) (rs.limit + rs.maxReadaheadSize)
|
||||
// |=============|
|
||||
//
|
||||
// |-------|
|
||||
@@ -186,7 +187,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
|
||||
// We read too far ahead of the last read, or before it. This indicates
|
||||
// a random read, where readahead is not desirable. Reset all variables.
|
||||
//
|
||||
// (rs.limit - maxReadaheadSize) (rs.limit) (rs.limit + maxReadaheadSize)
|
||||
// (rs.limit - rs.maxReadaheadSize) (rs.limit) (rs.limit + rs.maxReadaheadSize)
|
||||
// |+++++++++++++|=============|
|
||||
//
|
||||
// |-------|
|
||||
|
||||
31
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared.go
generated
vendored
31
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared.go
generated
vendored
@@ -6,6 +6,7 @@ package objstorageprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/internal/invariants"
|
||||
"github.com/cockroachdb/pebble/objstorage"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedobjcat"
|
||||
"github.com/cockroachdb/pebble/objstorage/shared"
|
||||
)
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
// All fields remain unset if shared storage is not configured.
|
||||
type sharedSubsystem struct {
|
||||
catalog *sharedobjcat.Catalog
|
||||
cache *sharedcache.Cache
|
||||
|
||||
// checkRefsOnOpen controls whether we check the ref marker file when opening
|
||||
// an object. Normally this is true when invariants are enabled (but the provider
|
||||
@@ -65,6 +68,24 @@ func (p *provider) sharedInit() error {
|
||||
p.st.Logger.Infof("shared storage configured; no creatorID yet")
|
||||
}
|
||||
|
||||
if p.st.Shared.CacheSizeBytes > 0 {
|
||||
const defaultBlockSize = 32 * 1024
|
||||
blockSize := p.st.Shared.CacheBlockSize
|
||||
if blockSize == 0 {
|
||||
blockSize = defaultBlockSize
|
||||
}
|
||||
|
||||
numShards := p.st.Shared.CacheShardCount
|
||||
if numShards == 0 {
|
||||
numShards = 2 * runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
p.shared.cache, err = sharedcache.Open(p.st.FS, p.st.Logger, p.st.FSDirName, blockSize, p.st.Shared.CacheSizeBytes, numShards)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "pebble: could not open shared object cache")
|
||||
}
|
||||
}
|
||||
|
||||
for _, meta := range contents.Objects {
|
||||
o := objstorage.ObjectMetadata{
|
||||
DiskFileNum: meta.FileNum,
|
||||
@@ -95,6 +116,14 @@ func (p *provider) SetCreatorID(creatorID objstorage.CreatorID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsForeign is part of the objstorage.Provider interface.
|
||||
func (p *provider) IsForeign(meta objstorage.ObjectMetadata) bool {
|
||||
if !p.shared.initialized.Load() {
|
||||
return false
|
||||
}
|
||||
return meta.IsShared() && p.shared.creatorID != meta.Shared.CreatorID
|
||||
}
|
||||
|
||||
func (p *provider) sharedCheckInitialized() error {
|
||||
if p.sharedStorage() == nil {
|
||||
return errors.Errorf("shared object support not configured")
|
||||
@@ -211,7 +240,7 @@ func (p *provider) sharedOpenForReading(
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return newSharedReadable(reader, size), nil
|
||||
return p.newSharedReadable(reader, size, meta.DiskFileNum), nil
|
||||
}
|
||||
|
||||
func (p *provider) sharedSize(meta objstorage.ObjectMetadata) (int64, error) {
|
||||
|
||||
109
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_readable.go
generated
vendored
109
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_readable.go
generated
vendored
@@ -6,28 +6,55 @@ package objstorageprovider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/objstorage"
|
||||
"github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache"
|
||||
"github.com/cockroachdb/pebble/objstorage/shared"
|
||||
)
|
||||
|
||||
const sharedMaxReadaheadSize = 1024 * 1024 /* 1MB */
|
||||
|
||||
// sharedReadable is a very simple implementation of Readable on top of the
|
||||
// ReadCloser returned by shared.Storage.CreateObject.
|
||||
type sharedReadable struct {
|
||||
objReader shared.ObjectReader
|
||||
size int64
|
||||
fileNum base.DiskFileNum
|
||||
provider *provider
|
||||
}
|
||||
|
||||
var _ objstorage.Readable = (*sharedReadable)(nil)
|
||||
|
||||
func newSharedReadable(objReader shared.ObjectReader, size int64) *sharedReadable {
|
||||
func (p *provider) newSharedReadable(
|
||||
objReader shared.ObjectReader, size int64, fileNum base.DiskFileNum,
|
||||
) *sharedReadable {
|
||||
return &sharedReadable{
|
||||
objReader: objReader,
|
||||
size: size,
|
||||
fileNum: fileNum,
|
||||
provider: p,
|
||||
}
|
||||
}
|
||||
|
||||
// ReadAt is part of the objstorage.Readable interface.
|
||||
func (r *sharedReadable) ReadAt(ctx context.Context, p []byte, offset int64) error {
|
||||
return r.readInternal(ctx, p, offset, false /* forCompaction */)
|
||||
}
|
||||
|
||||
// readInternal performs a read for the object, using the cache when
|
||||
// appropriate.
|
||||
func (r *sharedReadable) readInternal(
|
||||
ctx context.Context, p []byte, offset int64, forCompaction bool,
|
||||
) error {
|
||||
if cache := r.provider.shared.cache; cache != nil {
|
||||
flags := sharedcache.ReadFlags{
|
||||
// Don't add data to the cache if this read is for a compaction.
|
||||
ReadOnly: forCompaction,
|
||||
}
|
||||
return r.provider.shared.cache.ReadAt(ctx, r.fileNum, p, offset, r.objReader, r.size, flags)
|
||||
}
|
||||
return r.objReader.ReadAt(ctx, p, offset)
|
||||
}
|
||||
|
||||
@@ -42,24 +69,94 @@ func (r *sharedReadable) Size() int64 {
|
||||
|
||||
func (r *sharedReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
|
||||
// TODO(radu): use a pool.
|
||||
return &sharedReadHandle{readable: r}
|
||||
rh := &sharedReadHandle{readable: r}
|
||||
rh.readahead.state = makeReadaheadState(sharedMaxReadaheadSize)
|
||||
return rh
|
||||
}
|
||||
|
||||
type sharedReadHandle struct {
|
||||
readable *sharedReadable
|
||||
readable *sharedReadable
|
||||
readahead struct {
|
||||
state readaheadState
|
||||
data []byte
|
||||
offset int64
|
||||
}
|
||||
forCompaction bool
|
||||
}
|
||||
|
||||
var _ objstorage.ReadHandle = (*sharedReadHandle)(nil)
|
||||
|
||||
// ReadAt is part of the objstorage.ReadHandle interface.
|
||||
func (r *sharedReadHandle) ReadAt(ctx context.Context, p []byte, offset int64) error {
|
||||
return r.readable.ReadAt(ctx, p, offset)
|
||||
readaheadSize := r.maybeReadahead(offset, len(p))
|
||||
|
||||
// Check if we already have the data from a previous read-ahead.
|
||||
if rhSize := int64(len(r.readahead.data)); rhSize > 0 {
|
||||
if r.readahead.offset <= offset && r.readahead.offset+rhSize > offset {
|
||||
n := copy(p, r.readahead.data[offset-r.readahead.offset:])
|
||||
if n == len(p) {
|
||||
// All data was available.
|
||||
return nil
|
||||
}
|
||||
// Use the data that we had and do a shorter read.
|
||||
offset += int64(n)
|
||||
p = p[n:]
|
||||
readaheadSize -= n
|
||||
}
|
||||
}
|
||||
|
||||
if readaheadSize > len(p) {
|
||||
// Don't try to read past EOF.
|
||||
if offset+int64(readaheadSize) > r.readable.size {
|
||||
readaheadSize = int(r.readable.size - offset)
|
||||
if readaheadSize <= 0 {
|
||||
// This shouldn't happen in practice (Pebble should never try to read
|
||||
// past EOF).
|
||||
return io.EOF
|
||||
}
|
||||
}
|
||||
r.readahead.offset = offset
|
||||
// TODO(radu): we need to somehow account for this memory.
|
||||
if cap(r.readahead.data) >= readaheadSize {
|
||||
r.readahead.data = r.readahead.data[:readaheadSize]
|
||||
} else {
|
||||
r.readahead.data = make([]byte, readaheadSize)
|
||||
}
|
||||
|
||||
if err := r.readable.readInternal(ctx, r.readahead.data, offset, r.forCompaction); err != nil {
|
||||
// Make sure we don't treat the data as valid next time.
|
||||
r.readahead.data = r.readahead.data[:0]
|
||||
return err
|
||||
}
|
||||
copy(p, r.readahead.data)
|
||||
return nil
|
||||
}
|
||||
|
||||
return r.readable.readInternal(ctx, p, offset, r.forCompaction)
|
||||
}
|
||||
|
||||
func (r *sharedReadHandle) maybeReadahead(offset int64, len int) int {
|
||||
if r.forCompaction {
|
||||
return sharedMaxReadaheadSize
|
||||
}
|
||||
return int(r.readahead.state.maybeReadahead(offset, int64(len)))
|
||||
}
|
||||
|
||||
// Close is part of the objstorage.ReadHandle interface.
|
||||
func (r *sharedReadHandle) Close() error {
|
||||
r.readable = nil
|
||||
r.readahead.data = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *sharedReadHandle) MaxReadahead() {}
|
||||
// SetupForCompaction is part of the objstorage.ReadHandle interface.
|
||||
func (r *sharedReadHandle) SetupForCompaction() {
|
||||
r.forCompaction = true
|
||||
}
|
||||
|
||||
func (r *sharedReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {}
|
||||
// RecordCacheHit is part of the objstorage.ReadHandle interface.
|
||||
func (r *sharedReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {
|
||||
if !r.forCompaction {
|
||||
r.readahead.state.recordCacheHit(offset, size)
|
||||
}
|
||||
}
|
||||
|
||||
570
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go
generated
vendored
Normal file
570
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go
generated
vendored
Normal file
@@ -0,0 +1,570 @@
|
||||
// Copyright 2023 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 sharedcache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/cockroachdb/pebble/internal/base"
|
||||
"github.com/cockroachdb/pebble/internal/invariants"
|
||||
"github.com/cockroachdb/pebble/objstorage/shared"
|
||||
"github.com/cockroachdb/pebble/vfs"
|
||||
)
|
||||
|
||||
const (
|
||||
shardingBlockSize = 1024 * 1024
|
||||
)
|
||||
|
||||
// Cache is a persistent cache backed by a local filesystem. It is intended
|
||||
// to cache data that is in slower shared storage (e.g. S3), hence the
|
||||
// package name 'sharedcache'.
|
||||
type Cache struct {
|
||||
shards []shard
|
||||
logger base.Logger
|
||||
|
||||
blockSize int
|
||||
|
||||
writeBackWaitGroup sync.WaitGroup
|
||||
// TODO(josh): Have a dedicated metrics struct. Right now, this
|
||||
// is just for testing.
|
||||
misses atomic.Int32
|
||||
}
|
||||
|
||||
// Open opens a cache. If there is no existing cache at fsDir, a new one
|
||||
// is created.
|
||||
func Open(
|
||||
fs vfs.FS, logger base.Logger, fsDir string, blockSize int, sizeBytes int64, numShards int,
|
||||
) (*Cache, error) {
|
||||
min := shardingBlockSize * int64(numShards)
|
||||
if sizeBytes < min {
|
||||
return nil, errors.Errorf("cache size %d lower than min %d", sizeBytes, min)
|
||||
}
|
||||
|
||||
sc := &Cache{
|
||||
logger: logger,
|
||||
blockSize: blockSize,
|
||||
}
|
||||
sc.shards = make([]shard, numShards)
|
||||
blocksPerShard := sizeBytes / int64(numShards) / int64(blockSize)
|
||||
for i := range sc.shards {
|
||||
if err := sc.shards[i].init(fs, fsDir, i, blocksPerShard, blockSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// Close closes the cache. Methods such as ReadAt should not be called after Close is
|
||||
// called.
|
||||
func (c *Cache) Close() error {
|
||||
c.writeBackWaitGroup.Wait()
|
||||
|
||||
var retErr error
|
||||
for i := range c.shards {
|
||||
if err := c.shards[i].close(); err != nil && retErr == nil {
|
||||
retErr = err
|
||||
}
|
||||
}
|
||||
c.shards = nil
|
||||
return retErr
|
||||
}
|
||||
|
||||
// ReadFlags contains options for Cache.ReadAt.
|
||||
type ReadFlags struct {
|
||||
// ReadOnly instructs ReadAt to not write any new data into the cache; it is
|
||||
// used when the data is unlikely to be used again.
|
||||
ReadOnly bool
|
||||
}
|
||||
|
||||
// ReadAt performs a read form an object, attempting to use cached data when
|
||||
// possible.
|
||||
func (c *Cache) ReadAt(
|
||||
ctx context.Context,
|
||||
fileNum base.DiskFileNum,
|
||||
p []byte,
|
||||
ofs int64,
|
||||
objReader shared.ObjectReader,
|
||||
objSize int64,
|
||||
flags ReadFlags,
|
||||
) error {
|
||||
if ofs >= objSize {
|
||||
if invariants.Enabled {
|
||||
panic("invalid ReadAt offset")
|
||||
}
|
||||
return io.EOF
|
||||
}
|
||||
// TODO(radu): for compaction reads, we may not want to read from the cache at
|
||||
// all.
|
||||
{
|
||||
n, err := c.get(fileNum, p, ofs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == len(p) {
|
||||
// Everything was in cache!
|
||||
return nil
|
||||
}
|
||||
|
||||
// Note this. The below code does not need the original ofs, as with the earlier
|
||||
// reading from the cache done, the relevant offset is ofs + int64(n). Same with p.
|
||||
ofs += int64(n)
|
||||
p = p[n:]
|
||||
|
||||
if invariants.Enabled {
|
||||
if n != 0 && ofs%int64(c.blockSize) != 0 {
|
||||
panic(fmt.Sprintf("after non-zero read from cache, ofs is not block-aligned: %v %v", ofs, n))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.misses.Add(1)
|
||||
|
||||
if flags.ReadOnly {
|
||||
return objReader.ReadAt(ctx, p, ofs)
|
||||
}
|
||||
|
||||
// We must do reads with offset & size that are multiples of the block size. Else
|
||||
// later cache hits may return incorrect zeroed results from the cache.
|
||||
firstBlockInd := ofs / int64(c.blockSize)
|
||||
adjustedOfs := firstBlockInd * int64(c.blockSize)
|
||||
|
||||
// Take the length of what is left to read plus the length of the adjustment of
|
||||
// the offset plus the size of a block minus one and divide by the size of a block
|
||||
// to get the number of blocks to read from the object.
|
||||
sizeOfOffAdjustment := int(ofs - adjustedOfs)
|
||||
numBlocksToRead := ((len(p) + sizeOfOffAdjustment) + (c.blockSize - 1)) / c.blockSize
|
||||
adjustedLen := numBlocksToRead * c.blockSize
|
||||
adjustedP := make([]byte, adjustedLen)
|
||||
|
||||
// Read the rest from the object. We may need to cap the length to avoid past EOF reads.
|
||||
eofCap := int64(adjustedLen)
|
||||
if adjustedOfs+eofCap > objSize {
|
||||
eofCap = objSize - adjustedOfs
|
||||
}
|
||||
if err := objReader.ReadAt(ctx, adjustedP[:eofCap], adjustedOfs); err != nil {
|
||||
return err
|
||||
}
|
||||
copy(p, adjustedP[sizeOfOffAdjustment:])
|
||||
|
||||
// TODO(josh): For writing back to the cache, we may want a concurrency-limited approach
|
||||
// that does batching, instead of what is below. I think it's reasonable though to
|
||||
// run a production experiment with what is below, before making adjustments. Note that
|
||||
// the writes being done are in multiples of the filesystem block size. The filesystem
|
||||
// might do okay with them.
|
||||
c.writeBackWaitGroup.Add(1)
|
||||
go func() {
|
||||
defer c.writeBackWaitGroup.Done()
|
||||
if err := c.set(fileNum, adjustedP, adjustedOfs); err != nil {
|
||||
// TODO(josh): Would like to log at error severity, but base.Logger doesn't
|
||||
// have error severity.
|
||||
c.logger.Infof("writing back to cache after miss failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// get attempts to read the requested data from the cache, if it is already
|
||||
// there.
|
||||
//
|
||||
// If all data is available, returns n = len(p).
|
||||
//
|
||||
// If data is partially available, a prefix of the data is read; returns n < len(p)
|
||||
// and no error. If no prefix is available, returns n = 0 and no error.
|
||||
func (c *Cache) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
|
||||
// The data extent might cross shard boundaries, hence the loop. In the hot
|
||||
// path, max two iterations of this loop will be executed, since reads are sized
|
||||
// in units of sstable block size.
|
||||
for {
|
||||
shard := c.getShard(fileNum, ofs+int64(n))
|
||||
cappedLen := len(p[n:])
|
||||
if toBoundary := int(shardingBlockSize - ((ofs + int64(n)) % shardingBlockSize)); cappedLen > toBoundary {
|
||||
cappedLen = toBoundary
|
||||
}
|
||||
numRead, err := shard.get(fileNum, p[n:n+cappedLen], ofs+int64(n))
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
n += numRead
|
||||
if numRead < cappedLen {
|
||||
// We only read a prefix from this shard.
|
||||
return n, nil
|
||||
}
|
||||
if n == len(p) {
|
||||
// We are done.
|
||||
return n, nil
|
||||
}
|
||||
// Data extent crosses shard boundary, continue with next shard.
|
||||
}
|
||||
}
|
||||
|
||||
// set attempts to write the requested data to the cache. Both ofs & len(p) must
|
||||
// be multiples of the block size.
|
||||
//
|
||||
// If all of p is not written to the shard, set returns a non-nil error.
|
||||
func (c *Cache) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
|
||||
if invariants.Enabled {
|
||||
if ofs%int64(c.blockSize) != 0 || len(p)%c.blockSize != 0 {
|
||||
panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
|
||||
}
|
||||
}
|
||||
|
||||
// The data extent might cross shard boundaries, hence the loop. In the hot
|
||||
// path, max two iterations of this loop will be executed, since reads are sized
|
||||
// in units of sstable block size.
|
||||
n := 0
|
||||
for {
|
||||
shard := c.getShard(fileNum, ofs+int64(n))
|
||||
cappedLen := len(p[n:])
|
||||
if toBoundary := int(shardingBlockSize - ((ofs + int64(n)) % shardingBlockSize)); cappedLen > toBoundary {
|
||||
cappedLen = toBoundary
|
||||
}
|
||||
err := shard.set(fileNum, p[n:n+cappedLen], ofs+int64(n))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// set returns an error if cappedLen bytes aren't written to the shard.
|
||||
n += cappedLen
|
||||
if n == len(p) {
|
||||
// We are done.
|
||||
return nil
|
||||
}
|
||||
// Data extent crosses shard boundary, continue with next shard.
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) getShard(fileNum base.DiskFileNum, ofs int64) *shard {
|
||||
const prime64 = 1099511628211
|
||||
hash := uint64(fileNum.FileNum())*prime64 + uint64(ofs)/shardingBlockSize
|
||||
// TODO(josh): Instance change ops are often run in production. Such an operation
|
||||
// updates len(c.shards); see openSharedCache. As a result, the behavior of this
|
||||
// function changes, and the cache empties out at restart time. We may want a better
|
||||
// story here eventually.
|
||||
return &c.shards[hash%uint64(len(c.shards))]
|
||||
}
|
||||
|
||||
type shard struct {
|
||||
file vfs.File
|
||||
sizeInBlocks int64
|
||||
blockSize int
|
||||
mu struct {
|
||||
sync.Mutex
|
||||
// TODO(josh): None of these datastructures are space-efficient.
|
||||
// Focusing on correctness to start.
|
||||
where whereMap
|
||||
locks []lockState
|
||||
free []cacheBlockIndex
|
||||
}
|
||||
}
|
||||
|
||||
// Maps a logical block in an SST to an index of the cache block with the
|
||||
// file contents (to the "cache block index").
|
||||
type whereMap map[logicalBlockID]cacheBlockIndex
|
||||
|
||||
type logicalBlockID struct {
|
||||
filenum base.DiskFileNum
|
||||
// Offset into the file, in units of shard.BlockSize.
|
||||
offsetInUnitsOfBlocks int64
|
||||
}
|
||||
|
||||
type cacheBlockIndex int64
|
||||
|
||||
type lockState int64
|
||||
|
||||
const (
|
||||
unlocked lockState = 0
|
||||
// >0 lockState tracks the number of distinct readers of some cache block / logical block
|
||||
// which is in the secondary cache. It is used to ensure that a cache block is not evicted
|
||||
// and overwritten, while there are active readers.
|
||||
readLockTakenInc = 1
|
||||
// -1 lockState indicates that some cache block is currently being populated with data from
|
||||
// blob storage. It is used to ensure that a cache block is not read or evicted again, while
|
||||
// it is being populated.
|
||||
writeLockTaken = -1
|
||||
)
|
||||
|
||||
func (s *shard) init(
|
||||
fs vfs.FS, fsDir string, shardIdx int, sizeInBlocks int64, blockSize int,
|
||||
) error {
|
||||
*s = shard{
|
||||
sizeInBlocks: sizeInBlocks,
|
||||
}
|
||||
if blockSize < 1024 || shardingBlockSize%blockSize != 0 {
|
||||
return errors.Newf("invalid block size %d (must divide %d)", blockSize, shardingBlockSize)
|
||||
}
|
||||
s.blockSize = blockSize
|
||||
file, err := fs.OpenReadWrite(fs.PathJoin(fsDir, fmt.Sprintf("SHARED-CACHE-%03d", shardIdx)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(radu): truncate file if necessary (especially important if we restart
|
||||
// with more shards).
|
||||
if err := file.Preallocate(0, int64(blockSize)*sizeInBlocks); err != nil {
|
||||
return err
|
||||
}
|
||||
s.file = file
|
||||
|
||||
// TODO(josh): Right now, the secondary cache is not persistent. All existing
|
||||
// cache contents will be over-written, since all metadata is only stored in
|
||||
// memory.
|
||||
s.mu.where = make(whereMap)
|
||||
for i := int64(0); i < sizeInBlocks; i++ {
|
||||
s.mu.locks = append(s.mu.locks, unlocked)
|
||||
s.mu.free = append(s.mu.free, cacheBlockIndex(i))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *shard) close() error {
|
||||
defer func() {
|
||||
s.file = nil
|
||||
}()
|
||||
return s.file.Close()
|
||||
}
|
||||
|
||||
// get attempts to read the requested data from the shard. The data must not
|
||||
// cross a shard boundary.
|
||||
//
|
||||
// If all data is available, returns n = len(p).
|
||||
//
|
||||
// If data is partially available, a prefix of the data is read; returns n < len(p)
|
||||
// and no error. If no prefix is available, returns n = 0 and no error.
|
||||
//
|
||||
// TODO(josh): Today, if there are two cache blocks needed to satisfy a read, and the
|
||||
// first block is not in the cache and the second one is, we will read both from
|
||||
// blob storage. We should fix this. This is not an unlikely scenario if we are doing
|
||||
// a reverse scan, since those iterate over sstable blocks in reverse order and due to
|
||||
// cache block aligned reads will have read the suffix of the sstable block that will
|
||||
// be needed next.
|
||||
func (s *shard) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) {
|
||||
if invariants.Enabled {
|
||||
if ofs/shardingBlockSize != (ofs+int64(len(p))-1)/shardingBlockSize {
|
||||
panic(fmt.Sprintf("get crosses shard boundary: %v %v", ofs, len(p)))
|
||||
}
|
||||
s.assertShardStateIsConsistent()
|
||||
}
|
||||
|
||||
// The data extent might cross cache block boundaries, hence the loop. In the hot
|
||||
// path, max two iterations of this loop will be executed, since reads are sized
|
||||
// in units of sstable block size.
|
||||
for {
|
||||
k := logicalBlockID{
|
||||
filenum: fileNum,
|
||||
offsetInUnitsOfBlocks: (ofs + int64(n)) / int64(s.blockSize),
|
||||
}
|
||||
s.mu.Lock()
|
||||
cacheBlockInd, ok := s.mu.where[k]
|
||||
// TODO(josh): Multiple reads within the same few milliseconds (anything that is smaller
|
||||
// than blob storage read latency) that miss on the same logical block ID will not necessarily
|
||||
// be rare. We may want to do only one read, with the later readers blocking on the first read
|
||||
// completing. This could be implemented either here or in the primary block cache. See
|
||||
// https://github.com/cockroachdb/pebble/pull/2586 for additional discussion.
|
||||
if !ok {
|
||||
s.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
if s.mu.locks[cacheBlockInd] == writeLockTaken {
|
||||
// In practice, if we have two reads of the same SST block in close succession, we
|
||||
// would expect the second to hit in the in-memory block cache. So it's not worth
|
||||
// optimizing this case here.
|
||||
s.mu.Unlock()
|
||||
return n, nil
|
||||
}
|
||||
s.mu.locks[cacheBlockInd] += readLockTakenInc
|
||||
s.mu.Unlock()
|
||||
|
||||
readAt := int64(cacheBlockInd) * int64(s.blockSize)
|
||||
if n == 0 { // if first read
|
||||
readAt += ofs % int64(s.blockSize)
|
||||
}
|
||||
readSize := s.blockSize
|
||||
if n == 0 { // if first read
|
||||
// Cast to int safe since ofs is modded by block size.
|
||||
readSize -= int(ofs % int64(s.blockSize))
|
||||
}
|
||||
|
||||
if len(p[n:]) <= readSize {
|
||||
numRead, err := s.file.ReadAt(p[n:], readAt)
|
||||
s.dropReadLock(cacheBlockInd)
|
||||
return n + numRead, err
|
||||
}
|
||||
numRead, err := s.file.ReadAt(p[n:n+readSize], readAt)
|
||||
s.dropReadLock(cacheBlockInd)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Note that numRead == readSize, since we checked for an error above.
|
||||
n += numRead
|
||||
}
|
||||
}
|
||||
|
||||
// set attempts to write the requested data to the shard. The data must not
|
||||
// cross a shard boundary, and both ofs & len(p) must be multiples of the
|
||||
// block size.
|
||||
//
|
||||
// If all of p is not written to the shard, set returns a non-nil error.
|
||||
func (s *shard) set(fileNum base.DiskFileNum, p []byte, ofs int64) error {
|
||||
if invariants.Enabled {
|
||||
if ofs/shardingBlockSize != (ofs+int64(len(p))-1)/shardingBlockSize {
|
||||
panic(fmt.Sprintf("set crosses shard boundary: %v %v", ofs, len(p)))
|
||||
}
|
||||
if ofs%int64(s.blockSize) != 0 || len(p)%s.blockSize != 0 {
|
||||
panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p)))
|
||||
}
|
||||
s.assertShardStateIsConsistent()
|
||||
}
|
||||
|
||||
// The data extent might cross cache block boundaries, hence the loop. In the hot
|
||||
// path, max two iterations of this loop will be executed, since reads are sized
|
||||
// in units of sstable block size.
|
||||
n := 0
|
||||
for {
|
||||
if n == len(p) {
|
||||
return nil
|
||||
}
|
||||
if invariants.Enabled {
|
||||
if n > len(p) {
|
||||
panic(fmt.Sprintf("set with n greater than len(p): %v %v", n, len(p)))
|
||||
}
|
||||
}
|
||||
|
||||
// If the logical block is already in the cache, we should skip doing a set.
|
||||
k := logicalBlockID{
|
||||
filenum: fileNum,
|
||||
offsetInUnitsOfBlocks: (ofs + int64(n)) / int64(s.blockSize),
|
||||
}
|
||||
s.mu.Lock()
|
||||
if _, ok := s.mu.where[k]; ok {
|
||||
s.mu.Unlock()
|
||||
n += s.blockSize
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine cache block index by looking at the free list and randomly evicting if
|
||||
// it is empty.
|
||||
var cacheBlockInd cacheBlockIndex
|
||||
if len(s.mu.free) == 0 {
|
||||
// TODO(josh): Right now, we do random eviction. Eventually, we will do something
|
||||
// more sophisticated, e.g. leverage ClockPro.
|
||||
foundBlockToEvict := false
|
||||
var k logicalBlockID
|
||||
for k1, v := range s.mu.where {
|
||||
// This implies that hot blocks will not be evicted as often, meaning the
|
||||
// eviction scheme is not quite random. I think that's fine for now, since we plan
|
||||
// to leverage a more sophisticated eviction scheme eventually anyway.
|
||||
lock := s.mu.locks[v]
|
||||
if lock >= readLockTakenInc || lock == writeLockTaken {
|
||||
continue
|
||||
}
|
||||
foundBlockToEvict = true
|
||||
cacheBlockInd = v
|
||||
k = k1
|
||||
break
|
||||
}
|
||||
// TODO(josh): We may want to block until a block frees up, instead of returning
|
||||
// an error here. But I think we can do that later on, e.g. after running some production
|
||||
// experiments.
|
||||
if !foundBlockToEvict {
|
||||
s.mu.Unlock()
|
||||
return errors.New("no block to evict so skipping write to cache")
|
||||
}
|
||||
delete(s.mu.where, k)
|
||||
} else {
|
||||
cacheBlockInd = s.mu.free[len(s.mu.free)-1]
|
||||
s.mu.free = s.mu.free[:len(s.mu.free)-1]
|
||||
}
|
||||
|
||||
s.mu.where[k] = cacheBlockInd
|
||||
s.mu.locks[cacheBlockInd] = writeLockTaken
|
||||
s.mu.Unlock()
|
||||
|
||||
writeAt := int64(cacheBlockInd) * int64(s.blockSize)
|
||||
|
||||
writeSize := s.blockSize
|
||||
if len(p[n:]) <= writeSize {
|
||||
writeSize = len(p[n:])
|
||||
}
|
||||
|
||||
_, err := s.file.WriteAt(p[n:n+writeSize], writeAt)
|
||||
if err != nil {
|
||||
s.freeBlock(k, cacheBlockInd)
|
||||
return err
|
||||
}
|
||||
s.dropWriteLock(cacheBlockInd)
|
||||
|
||||
n += writeSize
|
||||
}
|
||||
}
|
||||
|
||||
// Doesn't inline currently. This might be okay, but something to keep in mind.
|
||||
func (s *shard) dropReadLock(cacheBlockInd cacheBlockIndex) {
|
||||
s.mu.Lock()
|
||||
s.mu.locks[cacheBlockInd] -= readLockTakenInc
|
||||
if invariants.Enabled {
|
||||
if s.mu.locks[cacheBlockInd] < 0 {
|
||||
panic(fmt.Sprintf("unexpected lock state %v in dropReadLock: %v", s.mu.locks[cacheBlockInd], s.mu.locks))
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Doesn't inline currently. This might be okay, but something to keep in mind.
|
||||
func (s *shard) dropWriteLock(cacheBlockInd cacheBlockIndex) {
|
||||
s.mu.Lock()
|
||||
if invariants.Enabled {
|
||||
if s.mu.locks[cacheBlockInd] != writeLockTaken {
|
||||
panic(fmt.Sprintf("unexpected lock state %v in dropWriteLock: %v", s.mu.locks[cacheBlockInd], s.mu.locks))
|
||||
}
|
||||
}
|
||||
s.mu.locks[cacheBlockInd] = unlocked
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Doesn't inline currently. This might be okay, but something to keep in mind.
|
||||
func (s *shard) freeBlock(k logicalBlockID, cacheBlockInd cacheBlockIndex) {
|
||||
s.mu.Lock()
|
||||
delete(s.mu.where, k)
|
||||
s.mu.locks[cacheBlockInd] = unlocked
|
||||
s.mu.free = append(s.mu.free, cacheBlockInd)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
func (s *shard) assertShardStateIsConsistent() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
cacheBlockInds := make(map[cacheBlockIndex]bool)
|
||||
for _, v := range s.mu.where {
|
||||
if cacheBlockInds[v] {
|
||||
panic(fmt.Sprintf("repeated cache block index (where): %v %v %v %v", v, cacheBlockInds, s.mu.where, s.mu.free))
|
||||
}
|
||||
cacheBlockInds[v] = true
|
||||
}
|
||||
for _, v := range s.mu.free {
|
||||
if cacheBlockInds[v] {
|
||||
panic(fmt.Sprintf("repeated cache block index (free): %v %v %v %v", v, cacheBlockInds, s.mu.where, s.mu.free))
|
||||
}
|
||||
cacheBlockInds[v] = true
|
||||
}
|
||||
for i := int64(0); i < s.sizeInBlocks; i++ {
|
||||
if !cacheBlockInds[cacheBlockIndex(i)] {
|
||||
panic(fmt.Sprintf("missing cache block index: %v %v %v %v", i, cacheBlockInds, s.mu.where, s.mu.free))
|
||||
}
|
||||
}
|
||||
if int64(len(s.mu.locks)) != s.sizeInBlocks {
|
||||
panic(fmt.Sprintf("lock table isn't correct size: %v %v", len(s.mu.locks), s.sizeInBlocks))
|
||||
}
|
||||
for _, ls := range s.mu.locks {
|
||||
if ls < writeLockTaken {
|
||||
panic(fmt.Sprintf("lock state %v is not allowed: %v", ls, s.mu.locks))
|
||||
}
|
||||
}
|
||||
}
|
||||
16
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go
generated
vendored
16
vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go
generated
vendored
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/cockroachdb/pebble/vfs"
|
||||
)
|
||||
|
||||
const fileMaxReadaheadSize = 256 * 1024 /* 256KB */
|
||||
|
||||
// fileReadable implements objstorage.Readable on top of a vfs.File.
|
||||
//
|
||||
// The implementation might use Prealloc and might reopen the file with
|
||||
@@ -75,7 +77,7 @@ func (r *fileReadable) Size() int64 {
|
||||
func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle {
|
||||
rh := readHandlePool.Get().(*vfsReadHandle)
|
||||
rh.r = r
|
||||
rh.rs = makeReadaheadState()
|
||||
rh.rs = makeReadaheadState(fileMaxReadaheadSize)
|
||||
return rh
|
||||
}
|
||||
|
||||
@@ -126,10 +128,10 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
|
||||
n, err = rh.sequentialFile.ReadAt(p, offset)
|
||||
} else {
|
||||
if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
|
||||
if readaheadSize >= maxReadaheadSize {
|
||||
if readaheadSize >= fileMaxReadaheadSize {
|
||||
// We've reached the maximum readahead size. Beyond this point, rely on
|
||||
// OS-level readahead.
|
||||
rh.MaxReadahead()
|
||||
rh.switchToOSReadahead()
|
||||
} else {
|
||||
_ = rh.r.file.Prefetch(offset, readaheadSize)
|
||||
}
|
||||
@@ -142,8 +144,12 @@ func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error
|
||||
return err
|
||||
}
|
||||
|
||||
// MaxReadahead is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) MaxReadahead() {
|
||||
// SetupForCompaction is part of the objstorage.ReadHandle interface.
|
||||
func (rh *vfsReadHandle) SetupForCompaction() {
|
||||
rh.switchToOSReadahead()
|
||||
}
|
||||
|
||||
func (rh *vfsReadHandle) switchToOSReadahead() {
|
||||
if rh.sequentialFile != nil {
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user