0

[WIP] Partial bot skeleton, non-functional though

This commit is contained in:
eleksir
2024-02-09 19:40:54 +03:00
parent ea4652cefa
commit 9eac899a5c
1460 changed files with 625051 additions and 492 deletions

View File

@@ -0,0 +1,29 @@
// 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 objstorage
import "io"
// NoopReadHandle can be used by Readable implementations that don't
// support read-ahead.
type NoopReadHandle struct {
io.ReaderAt
}
// MakeNoopReadHandle initializes a NoopReadHandle.
func MakeNoopReadHandle(r io.ReaderAt) NoopReadHandle {
return NoopReadHandle{ReaderAt: r}
}
var _ ReadHandle = (*NoopReadHandle)(nil)
// Close is part of the ReadHandle interface.
func (*NoopReadHandle) Close() error { return nil }
// MaxReadahead is part of the ReadHandle interface.
func (*NoopReadHandle) MaxReadahead() {}
// RecordCacheHit is part of the ReadHandle interface.
func (*NoopReadHandle) RecordCacheHit(offset, size int64) {}

View File

@@ -0,0 +1,446 @@
// 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 objstorage
import (
"io"
"os"
"sort"
"sync"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage/shared"
"github.com/cockroachdb/pebble/objstorage/sharedobjcat"
"github.com/cockroachdb/pebble/vfs"
)
// Provider is a singleton object used to access and manage objects.
//
// An object is conceptually like a large immutable file. The main use of
// objects is for storing sstables; in the future it could also be used for blob
// storage.
//
// The Provider can only manage objects that it knows about - either objects
// created by the provider, or existing objects the Provider was informed about
// via AddObjects.
//
// Objects are currently backed by a vfs.File.
type Provider struct {
st Settings
fsDir vfs.File
shared sharedSubsystem
mu struct {
sync.RWMutex
shared struct {
// catalogBatch accumulates shared object creations and deletions until
// Sync is called.
catalogBatch sharedobjcat.Batch
}
// localObjectsChanged is set if non-shared objects were created or deleted
// but Sync was not yet called.
localObjectsChanged bool
// knownObjects maintains information about objects that are known to the provider.
// It is initialized with the list of files in the manifest when we open a DB.
knownObjects map[base.FileNum]ObjectMetadata
}
}
// Readable is the handle for an object that is open for reading.
type Readable interface {
io.ReaderAt
io.Closer
// Size returns the size of the object.
Size() int64
// NewReadHandle creates a read handle for ReadAt requests that are related
// and can benefit from optimizations like read-ahead.
//
// The ReadHandle must be closed before the Readable is closed.
//
// Multiple separate ReadHandles can be used.
NewReadHandle() ReadHandle
}
// ReadHandle is used to perform reads that are related and might benefit from
// optimizations like read-ahead.
type ReadHandle interface {
io.ReaderAt
io.Closer
// MaxReadahead configures the implementation to expect large sequential
// reads. Used to skip any initial read-ahead ramp-up.
MaxReadahead()
// RecordCacheHit informs the implementation that we were able to retrieve a
// block from cache.
RecordCacheHit(offset, size int64)
}
// Writable is the handle for an object that is open for writing.
// Either Finish or Abort must be called.
type Writable interface {
// Write writes len(p) bytes from p to the underlying object. The data is not
// guaranteed to be durable until Finish is called.
//
// Note that Write *is* allowed to modify the slice passed in, whether
// temporarily or permanently. Callers of Write need to take this into
// account.
Write(p []byte) error
// Finish completes the object and makes the data durable.
// No further calls are allowed after calling Finish.
Finish() error
// Abort gives up on finishing the object. There is no guarantee about whether
// the object exists after calling Abort.
// No further calls are allowed after calling Abort.
Abort()
}
// Settings that must be specified when creating the Provider.
type Settings struct {
Logger base.Logger
// Local filesystem configuration.
FS vfs.FS
FSDirName string
// FSDirInitialListing is a listing of FSDirName at the time of calling Open.
//
// This is an optional optimization to avoid double listing on Open when the
// higher layer already has a listing. When nil, we obtain the listing on
// Open.
FSDirInitialListing []string
// Cleaner cleans obsolete files from the local filesystem.
//
// The default cleaner uses the DeleteCleaner.
FSCleaner base.Cleaner
// NoSyncOnClose decides whether the implementation will enforce a
// close-time synchronization (e.g., fdatasync() or sync_file_range())
// on files it writes to. Setting this to true removes the guarantee for a
// sync on close. Some implementations can still issue a non-blocking sync.
NoSyncOnClose bool
// BytesPerSync enables periodic syncing of files in order to smooth out
// writes to disk. This option does not provide any persistence guarantee, but
// is used to avoid latency spikes if the OS automatically decides to write
// out a large chunk of dirty filesystem buffers.
BytesPerSync int
// Fields here are set only if the provider is to support shared objects
// (experimental).
Shared struct {
Storage shared.Storage
}
}
// DefaultSettings initializes default settings (with no shared storage),
// suitable for tests and tools.
func DefaultSettings(fs vfs.FS, dirName string) Settings {
return Settings{
Logger: base.DefaultLogger,
FS: fs,
FSDirName: dirName,
FSCleaner: base.DeleteCleaner{},
NoSyncOnClose: false,
BytesPerSync: 512 * 1024, // 512KB
}
}
// ObjectMetadata contains the metadata required to be able to access an object.
type ObjectMetadata struct {
FileNum base.FileNum
FileType base.FileType
// The fields below are only set if the object is on shared storage.
Shared struct {
// CreatorID identifies the DB instance that originally created the object.
CreatorID CreatorID
// CreatorFileNum is the identifier for the object within the context of the
// DB instance that originally created the object.
CreatorFileNum base.FileNum
}
}
// CreatorID identifies the DB instance that originally created a shared object.
// This ID is incorporated in backing object names.
// Must be non-zero.
type CreatorID = sharedobjcat.CreatorID
// IsShared returns true if the object is on shared storage.
func (meta *ObjectMetadata) IsShared() bool {
return meta.Shared.CreatorID.IsSet()
}
// Open creates the Provider.
func Open(settings Settings) (p *Provider, _ error) {
fsDir, err := settings.FS.OpenDir(settings.FSDirName)
if err != nil {
return nil, err
}
defer func() {
if p == nil {
fsDir.Close()
}
}()
p = &Provider{
st: settings,
fsDir: fsDir,
}
p.mu.knownObjects = make(map[base.FileNum]ObjectMetadata)
// Add local FS objects.
if err := p.vfsInit(); err != nil {
return nil, err
}
// Initialize shared subsystem (if configured) and add shared objects.
if err := p.sharedInit(); err != nil {
return nil, err
}
return p, nil
}
// Close the provider.
func (p *Provider) Close() error {
var err error
if p.fsDir != nil {
err = p.fsDir.Close()
p.fsDir = nil
}
return err
}
// OpenForReading opens an existing object.
func (p *Provider) OpenForReading(fileType base.FileType, fileNum base.FileNum) (Readable, error) {
meta, err := p.Lookup(fileType, fileNum)
if err != nil {
return nil, err
}
if !meta.IsShared() {
return p.vfsOpenForReading(fileType, fileNum, false /* mustExist */)
}
return p.sharedOpenForReading(meta)
}
// OpenForReadingMustExist is a variant of OpenForReading which causes a fatal
// error if the file does not exist. The fatal error message contains
// information helpful for debugging.
func (p *Provider) OpenForReadingMustExist(
fileType base.FileType, fileNum base.FileNum,
) (Readable, error) {
meta, err := p.Lookup(fileType, fileNum)
if err != nil {
p.st.Logger.Fatalf("%v", err)
return nil, err
}
if !meta.IsShared() {
return p.vfsOpenForReading(fileType, fileNum, true /* mustExist */)
}
// TODO(radu): implement "must exist" behavior.
return p.sharedOpenForReading(meta)
}
// CreateOptions contains optional arguments for Create.
type CreateOptions struct {
// PreferSharedStorage causes the object to be created on shared storage if
// the provider has shared storage configured.
PreferSharedStorage bool
}
// Create creates a new object and opens it for writing.
//
// The object is not guaranteed to be durable (accessible in case of crashes)
// until Sync is called.
func (p *Provider) Create(
fileType base.FileType, fileNum base.FileNum, opts CreateOptions,
) (w Writable, meta ObjectMetadata, err error) {
if opts.PreferSharedStorage && p.st.Shared.Storage != nil {
w, meta, err = p.sharedCreate(fileType, fileNum)
} else {
w, meta, err = p.vfsCreate(fileType, fileNum)
}
if err != nil {
err = errors.Wrapf(err, "creating object %s", errors.Safe(fileNum))
return nil, ObjectMetadata{}, err
}
p.addMetadata(meta)
return w, meta, nil
}
// Remove removes an object.
//
// The object is not guaranteed to be durably removed until Sync is called.
func (p *Provider) Remove(fileType base.FileType, fileNum base.FileNum) error {
meta, err := p.Lookup(fileType, fileNum)
if err != nil {
return err
}
if !meta.IsShared() {
err = p.vfsRemove(fileType, fileNum)
}
// TODO(radu): implement shared object removal (i.e. deref).
if err != nil && !IsNotExistError(err) {
// We want to be able to retry a Remove, so we keep the object in our list.
// TODO(radu): we should mark the object as "zombie" and not allow any other
// operations.
return errors.Wrapf(err, "removing object %s", errors.Safe(fileNum))
}
p.removeMetadata(fileNum)
return err
}
// IsNotExistError indicates whether the error is known to report that a file or
// directory does not exist.
func IsNotExistError(err error) bool {
return oserror.IsNotExist(err)
}
// Sync flushes the metadata from creation or removal of objects since the last Sync.
func (p *Provider) Sync() error {
if err := p.vfsSync(); err != nil {
return err
}
if err := p.sharedSync(); err != nil {
return err
}
return nil
}
// LinkOrCopyFromLocal creates a new object that is either a copy of a given
// local file or a hard link (if the new object is created on the same FS, and
// if the FS supports it).
//
// The object is not guaranteed to be durable (accessible in case of crashes)
// until Sync is called.
func (p *Provider) LinkOrCopyFromLocal(
srcFS vfs.FS, srcFilePath string, dstFileType base.FileType, dstFileNum base.FileNum,
) (ObjectMetadata, error) {
if srcFS == p.st.FS {
// Wrap the normal filesystem with one which wraps newly created files with
// vfs.NewSyncingFile.
fs := vfs.NewSyncingFS(p.st.FS, vfs.SyncingFileOptions{
NoSyncOnClose: p.st.NoSyncOnClose,
BytesPerSync: p.st.BytesPerSync,
})
dstPath := p.vfsPath(dstFileType, dstFileNum)
if err := vfs.LinkOrCopy(fs, srcFilePath, dstPath); err != nil {
return ObjectMetadata{}, err
}
meta := ObjectMetadata{
FileNum: dstFileNum,
FileType: dstFileType,
}
p.addMetadata(meta)
return meta, nil
}
// TODO(radu): for the copy case, we should use `p.Create` and do the copy ourselves.
panic("unimplemented")
}
// Lookup returns the metadata of an object that is already known to the Provider.
// Does not perform any I/O.
func (p *Provider) Lookup(fileType base.FileType, fileNum base.FileNum) (ObjectMetadata, error) {
p.mu.RLock()
defer p.mu.RUnlock()
meta, ok := p.mu.knownObjects[fileNum]
if !ok {
return ObjectMetadata{}, errors.Wrapf(
os.ErrNotExist,
"file %s (type %d) unknown to the objstorage provider",
errors.Safe(fileNum), errors.Safe(fileType),
)
}
if meta.FileType != fileType {
return ObjectMetadata{}, errors.AssertionFailedf(
"file %s type mismatch (known type %d, expected type %d)",
errors.Safe(fileNum), errors.Safe(meta.FileType), errors.Safe(fileType),
)
}
return meta, nil
}
// Path returns an internal, implementation-dependent path for the object. It is
// meant to be used for informational purposes (like logging).
func (p *Provider) Path(meta ObjectMetadata) string {
if !meta.IsShared() {
return p.vfsPath(meta.FileType, meta.FileNum)
}
return p.sharedPath(meta)
}
// Size returns the size of the object.
func (p *Provider) Size(meta ObjectMetadata) (int64, error) {
if !meta.IsShared() {
return p.vfsSize(meta.FileType, meta.FileNum)
}
return p.sharedSize(meta)
}
// List returns the objects currently known to the provider. Does not perform any I/O.
func (p *Provider) List() []ObjectMetadata {
p.mu.RLock()
defer p.mu.RUnlock()
res := make([]ObjectMetadata, 0, len(p.mu.knownObjects))
for _, meta := range p.mu.knownObjects {
res = append(res, meta)
}
sort.Slice(res, func(i, j int) bool {
return res[i].FileNum < res[j].FileNum
})
return res
}
func (p *Provider) addMetadata(meta ObjectMetadata) {
p.mu.Lock()
defer p.mu.Unlock()
p.mu.knownObjects[meta.FileNum] = meta
if meta.IsShared() {
p.mu.shared.catalogBatch.AddObject(sharedobjcat.SharedObjectMetadata{
FileNum: meta.FileNum,
FileType: meta.FileType,
CreatorID: meta.Shared.CreatorID,
CreatorFileNum: meta.Shared.CreatorFileNum,
})
} else {
p.mu.localObjectsChanged = true
}
}
func (p *Provider) removeMetadata(fileNum base.FileNum) {
p.mu.Lock()
defer p.mu.Unlock()
meta, ok := p.mu.knownObjects[fileNum]
if !ok {
return
}
delete(p.mu.knownObjects, fileNum)
if meta.IsShared() {
p.mu.shared.catalogBatch.DeleteObject(fileNum)
} else {
p.mu.localObjectsChanged = true
}
}

View File

@@ -0,0 +1,200 @@
// 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 objstorage
const (
// Constants for dynamic readahead of data blocks. Note that the size values
// make sense as some multiple of the default block size; and they should
// both be larger than the default block size.
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 */
)
// readaheadState contains state variables related to readahead. Updated on
// file reads.
type readaheadState struct {
// Number of sequential reads.
numReads int64
// Size issued to the next call to Prefetch. Starts at or above
// initialReadaheadSize and grows exponentially until maxReadaheadSize.
size int64
// prevSize is the size used in the last Prefetch call.
prevSize int64
// The byte offset up to which the OS has been asked to read ahead / cached.
// When reading ahead, reads up to this limit should not incur an IO
// operation. Reads after this limit can benefit from a new call to
// Prefetch.
limit int64
}
func makeReadaheadState() readaheadState {
return readaheadState{
size: initialReadaheadSize,
}
}
func (rs *readaheadState) recordCacheHit(offset, blockLength int64) {
currentReadEnd := offset + blockLength
if rs.numReads >= minFileReadsForReadahead {
if currentReadEnd >= rs.limit && offset <= rs.limit+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 {
// We read too far away from rs.limit to benefit from readahead in
// any scenario. Reset all variables.
rs.numReads = 1
rs.limit = currentReadEnd
rs.size = initialReadaheadSize
rs.prevSize = 0
return
}
// Reads in the range [rs.limit - rs.prevSize, rs.limit] end up
// here. This is a read that is potentially benefitting from a past
// readahead.
return
}
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
// Blocks are being read sequentially and would benefit from readahead
// down the line.
rs.numReads++
return
}
// 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.numReads = 1
rs.limit = currentReadEnd
rs.size = initialReadaheadSize
rs.prevSize = 0
}
// maybeReadahead updates state and determines whether to issue a readahead /
// prefetch call for a block read at offset for blockLength bytes.
// Returns a size value (greater than 0) that should be prefetched if readahead
// would be beneficial.
func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 {
currentReadEnd := offset + blockLength
if rs.numReads >= minFileReadsForReadahead {
// The minimum threshold of sequential reads to justify reading ahead
// has been reached.
// There are two intervals: the interval being read:
// [offset, currentReadEnd]
// 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
// 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 {
// 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
// being read.
//
// rs.limit rs.limit + maxReadaheadSize
// ++++++++++|===========================|
//
// |-------------|
// offset currentReadEnd
//
// This case is also possible, as are all cases with an overlap
// between [rs.limit, rs.limit + maxReadaheadSize] and [offset,
// currentReadEnd]:
//
// rs.limit rs.limit + maxReadaheadSize
// ++++++++++|===========================|
//
// |-------------|
// offset currentReadEnd
//
//
rs.numReads++
rs.limit = offset + rs.size
rs.prevSize = rs.size
// Increase rs.size for the next read.
rs.size *= 2
if rs.size > maxReadaheadSize {
rs.size = maxReadaheadSize
}
return rs.prevSize
}
if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+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)
// |+++++++++++++|=============|
//
// |-------------|
// offset currentReadEnd
//
// Or too far behind:
//
// (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize)
// |+++++++++++++|=============|
//
// |-------------|
// offset currentReadEnd
//
rs.numReads = 1
rs.limit = currentReadEnd
rs.size = initialReadaheadSize
rs.prevSize = 0
return 0
}
// Reads in the range [rs.limit - rs.prevSize, rs.limit] end up
// here. This is a read that is potentially benefitting from a past
// readahead, but there's no reason to issue a readahead call at the
// moment.
//
// (rs.limit - rs.prevSize) (rs.limit + maxReadaheadSize)
// |+++++++++++++|===============|
// (rs.limit)
//
// |-------|
// offset currentReadEnd
//
rs.numReads++
return 0
}
if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize {
// Blocks are being read sequentially and would benefit from readahead
// down the line.
//
// (rs.limit) (rs.limit + maxReadaheadSize)
// |=============|
//
// |-------|
// offset currentReadEnd
//
rs.numReads++
return 0
}
// 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)
// |+++++++++++++|=============|
//
// |-------|
// offset currentReadEnd
//
rs.numReads = 1
rs.limit = currentReadEnd
rs.size = initialReadaheadSize
rs.prevSize = 0
return 0
}

View File

@@ -0,0 +1,176 @@
// 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 objstorage
import (
"fmt"
"sync"
"sync/atomic"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage/sharedobjcat"
)
// sharedSubsystem contains the provider fields related to shared storage.
// All fields remain unset if shared storage is not configured.
type sharedSubsystem struct {
catalog *sharedobjcat.Catalog
// initialized guards access to the creatorID field.
initialized atomic.Bool
creatorID CreatorID
initOnce sync.Once
}
func (ss *sharedSubsystem) init(creatorID CreatorID) {
ss.initOnce.Do(func() {
ss.creatorID = creatorID
ss.initialized.Store(true)
})
}
// sharedInit initializes the shared object subsystem (if configured) and finds
// any shared objects.
func (p *Provider) sharedInit() error {
if p.st.Shared.Storage == nil {
return nil
}
catalog, contents, err := sharedobjcat.Open(p.st.FS, p.st.FSDirName)
if err != nil {
return errors.Wrapf(err, "pebble: could not open shared object catalog")
}
p.shared.catalog = catalog
// The creator ID may or may not be initialized yet.
if contents.CreatorID.IsSet() {
p.shared.init(contents.CreatorID)
p.st.Logger.Infof("shared storage configured; creatorID = %s", contents.CreatorID)
} else {
p.st.Logger.Infof("shared storage configured; no creatorID yet")
}
for _, meta := range contents.Objects {
o := ObjectMetadata{
FileNum: meta.FileNum,
FileType: meta.FileType,
}
o.Shared.CreatorID = meta.CreatorID
o.Shared.CreatorFileNum = meta.CreatorFileNum
p.mu.knownObjects[o.FileNum] = o
}
return nil
}
// SetCreatorID sets the CreatorID which is needed in order to use shared
// objects. Shared object usage is disabled until this method is called the
// first time. Once set, the Creator ID is persisted and cannot change.
//
// Cannot be called if shared storage is not configured for the provider.
func (p *Provider) SetCreatorID(creatorID CreatorID) error {
if p.st.Shared.Storage == nil {
return errors.AssertionFailedf("attempt to set CreatorID but shared storage not enabled")
}
// Note: this call is a cheap no-op if the creator ID was already set. This
// call also checks if we are trying to change the ID.
if err := p.shared.catalog.SetCreatorID(creatorID); err != nil {
return err
}
if !p.shared.initialized.Load() {
p.st.Logger.Infof("shared storage creatorID set to %s", creatorID)
p.shared.init(creatorID)
}
return nil
}
func (p *Provider) sharedCheckInitialized() error {
if p.st.Shared.Storage == nil {
return errors.Errorf("shared object support not configured")
}
if !p.shared.initialized.Load() {
return errors.Errorf("shared object support not available: shared creator ID not yet set")
}
return nil
}
func (p *Provider) sharedSync() error {
batch := func() sharedobjcat.Batch {
p.mu.Lock()
defer p.mu.Unlock()
res := p.mu.shared.catalogBatch.Copy()
p.mu.shared.catalogBatch.Reset()
return res
}()
if batch.IsEmpty() {
return nil
}
err := p.shared.catalog.ApplyBatch(batch)
if err != nil {
// We have to put back the batch (for the next Sync).
p.mu.Lock()
defer p.mu.Unlock()
batch.Append(p.mu.shared.catalogBatch)
p.mu.shared.catalogBatch = batch
return err
}
return nil
}
func (p *Provider) sharedPath(meta ObjectMetadata) string {
return "shared://" + sharedObjectName(meta)
}
func sharedObjectName(meta ObjectMetadata) string {
// TODO(radu): prepend a "shard" value for better distribution within the bucket?
return fmt.Sprintf(
"%s-%s",
meta.Shared.CreatorID, base.MakeFilename(meta.FileType, meta.Shared.CreatorFileNum),
)
}
func (p *Provider) sharedCreate(
fileType base.FileType, fileNum base.FileNum,
) (Writable, ObjectMetadata, error) {
if err := p.sharedCheckInitialized(); err != nil {
return nil, ObjectMetadata{}, err
}
meta := ObjectMetadata{
FileNum: fileNum,
FileType: fileType,
}
meta.Shared.CreatorID = p.shared.creatorID
meta.Shared.CreatorFileNum = fileNum
objName := sharedObjectName(meta)
writer, err := p.st.Shared.Storage.CreateObject(objName)
if err != nil {
return nil, ObjectMetadata{}, err
}
return &sharedWritable{
storageWriter: writer,
}, meta, nil
}
func (p *Provider) sharedOpenForReading(meta ObjectMetadata) (Readable, error) {
if err := p.sharedCheckInitialized(); err != nil {
return nil, err
}
objName := sharedObjectName(meta)
size, err := p.st.Shared.Storage.Size(objName)
if err != nil {
return nil, err
}
return newSharedReadable(p.st.Shared.Storage, objName, size), nil
}
func (p *Provider) sharedSize(meta ObjectMetadata) (int64, error) {
if err := p.sharedCheckInitialized(); err != nil {
return 0, err
}
objName := sharedObjectName(meta)
return p.st.Shared.Storage.Size(objName)
}

View File

@@ -0,0 +1,123 @@
// 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 shared
import (
"fmt"
"io"
"sort"
)
// WithLogging wraps the given Storage implementation and emits logs for various
// operations.
func WithLogging(wrapped Storage, logf func(fmt string, args ...interface{})) Storage {
return &loggingStore{
logf: logf,
wrapped: wrapped,
}
}
// loggingStore wraps a shared.Storage implementation and emits logs of the
// operations.
type loggingStore struct {
logf func(fmt string, args ...interface{})
wrapped Storage
}
var _ Storage = (*loggingStore)(nil)
func (l *loggingStore) Close() error {
l.logf("close")
return l.wrapped.Close()
}
func (l *loggingStore) ReadObjectAt(
basename string, offset int64,
) (_ io.ReadCloser, totalSize int64, _ error) {
r, totalSize, err := l.wrapped.ReadObjectAt(basename, offset)
l.logf("read object %q at %d: %s", basename, offset, errOrPrintf(err, "%d bytes", totalSize))
if err != nil {
return nil, 0, err
}
return &loggingReader{
l: l,
name: basename,
ReadCloser: r,
}, totalSize, nil
}
type loggingReader struct {
l *loggingStore
name string
io.ReadCloser
}
func (l *loggingReader) Close() error {
l.l.logf("close reader for %q", l.name)
return l.ReadCloser.Close()
}
func (l *loggingStore) CreateObject(basename string) (io.WriteCloser, error) {
l.logf("create object %q", basename)
writer, err := l.wrapped.CreateObject(basename)
if err != nil {
return nil, err
}
return &loggingWriter{
l: l,
name: basename,
WriteCloser: writer,
}, nil
}
type loggingWriter struct {
l *loggingStore
name string
bytesWritten int64
io.WriteCloser
}
func (l *loggingWriter) Write(p []byte) (n int, err error) {
n, err = l.WriteCloser.Write(p)
l.bytesWritten += int64(n)
return n, err
}
func (l *loggingWriter) Close() error {
l.l.logf("close writer for %q after %d bytes", l.name, l.bytesWritten)
return l.WriteCloser.Close()
}
func (l *loggingStore) List(prefix, delimiter string) ([]string, error) {
l.logf("list (prefix=%q, delimiter=%q)", prefix, delimiter)
res, err := l.wrapped.List(prefix, delimiter)
if err != nil {
return nil, err
}
sorted := append([]string(nil), res...)
sort.Strings(sorted)
for _, s := range sorted {
l.logf(" - %s", s)
}
return res, nil
}
func (l *loggingStore) Delete(basename string) error {
l.logf("delete object %q", basename)
return l.wrapped.Delete(basename)
}
func (l *loggingStore) Size(basename string) (int64, error) {
size, err := l.wrapped.Size(basename)
l.logf("size of object %q: %s", basename, errOrPrintf(err, "%d", size))
return size, err
}
func errOrPrintf(err error, format string, args ...interface{}) string {
if err != nil {
return fmt.Sprintf("error: %v", err)
}
return fmt.Sprintf(format, args...)
}

View File

@@ -0,0 +1,156 @@
// 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 shared
import (
"bytes"
"io"
"os"
"strings"
"sync"
)
// NewInMem returns an in-memory implementation of the shared.Storage
// interface (for testing).
func NewInMem() Storage {
store := &inMemStore{}
store.mu.objects = make(map[string]*inMemObj)
return store
}
// inMemStore is an in-memory implementation of the shared.Storage interface
// (for testing).
type inMemStore struct {
mu struct {
sync.Mutex
objects map[string]*inMemObj
}
}
var _ Storage = (*inMemStore)(nil)
type inMemObj struct {
name string
data []byte
}
func (s *inMemStore) Close() error {
*s = inMemStore{}
return nil
}
func (s *inMemStore) ReadObjectAt(basename string, offset int64) (io.ReadCloser, int64, error) {
obj, err := s.getObj(basename)
if err != nil {
return nil, 0, err
}
remaining := int64(len(obj.data)) - offset
if remaining < 0 {
return nil, 0, io.EOF
}
return newInMemReader(obj.data[int(offset):]), remaining, nil
}
type inMemReader struct {
bytes.Reader
}
func newInMemReader(b []byte) *inMemReader {
r := &inMemReader{}
r.Reset(b)
return r
}
var _ io.ReadCloser = (*inMemReader)(nil)
func (r *inMemReader) Close() error {
r.Reset(nil)
return nil
}
func (s *inMemStore) CreateObject(basename string) (io.WriteCloser, error) {
return &inMemWriter{
store: s,
name: basename,
}, nil
}
type inMemWriter struct {
store *inMemStore
name string
buf bytes.Buffer
}
var _ io.WriteCloser = (*inMemWriter)(nil)
func (o *inMemWriter) Write(p []byte) (n int, err error) {
if o.store == nil {
panic("Write after Close")
}
return o.buf.Write(p)
}
func (o *inMemWriter) Close() error {
if o.store != nil {
o.store.addObj(&inMemObj{
name: o.name,
data: o.buf.Bytes(),
})
o.store = nil
}
return nil
}
func (s *inMemStore) List(prefix, delimiter string) ([]string, error) {
if delimiter != "" {
panic("delimiter unimplemented")
}
s.mu.Lock()
defer s.mu.Unlock()
res := make([]string, 0, len(s.mu.objects))
for name := range s.mu.objects {
if strings.HasPrefix(name, prefix) {
res = append(res, name)
}
}
return res, nil
}
func (s *inMemStore) Delete(basename string) error {
s.rmObj(basename)
return nil
}
// Size returns the length of the named object in bytesWritten.
func (s *inMemStore) Size(basename string) (int64, error) {
obj, err := s.getObj(basename)
if err != nil {
return 0, err
}
return int64(len(obj.data)), nil
}
func (s *inMemStore) getObj(name string) (*inMemObj, error) {
s.mu.Lock()
defer s.mu.Unlock()
obj, ok := s.mu.objects[name]
if !ok {
return nil, os.ErrNotExist
}
return obj, nil
}
func (s *inMemStore) addObj(o *inMemObj) {
s.mu.Lock()
defer s.mu.Unlock()
s.mu.objects[o.name] = o
}
func (s *inMemStore) rmObj(name string) {
s.mu.Lock()
defer s.mu.Unlock()
delete(s.mu.objects, name)
}

View File

@@ -0,0 +1,61 @@
// 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 shared
import "io"
// Storage is an interface for a blob storage driver. This is lower-level
// than an FS-like interface, however FS/File-like abstractions can be built on
// top of these methods.
//
// TODO(bilal): Consider pushing shared file obsoletion as well as path
// generation behind this interface.
type Storage interface {
io.Closer
// ReadObjectAt returns a Reader for reading the object at the requested name
// and offset, along with the total size of the object.
ReadObjectAt(basename string, offset int64) (_ io.ReadCloser, totalSize int64, _ error)
// CreateObject returns a writer for the object at the request name. A new
// empty object is created if CreateObject is called on an existing object.
//
// A Writer *must* be closed via either Close, and if closing returns a
// non-nil error, that error should be handled or reported to the user -- an
// implementation may buffer written data until Close and only then return
// an error, or Write may return an opaque io.EOF with the underlying cause
// returned by the subsequent Close().
//
// TODO(radu): if we encounter some unrelated error while writing to the
// WriteCloser, we'd want to abort the whole thing rather than letting Close
// finalize the upload.
CreateObject(basename string) (io.WriteCloser, error)
// List enumerates files within the supplied prefix, returning a list of
// objects within that prefix. If delimiter is non-empty, names which have the
// same prefix, prior to the delimiter but after the prefix, are grouped into a
// single result which is that prefix. The order that results are returned is
// undefined. If a prefix is specified, the prefix is trimmed from the result
// list.
//
// An example would be, if the storage contains objects a, b/4, b/5 and b/6,
// these would be the return values:
// List("", "") -> ["a", "b/4", "b/5", "b/6"]
// List("", "/") -> ["a", "b"]
// List("b", "/") -> ["4", "5", "6"]
// List("b", "") -> ["/4", "/5", "/6"]
List(prefix, delimiter string) ([]string, error)
// Delete removes the named object from the store.
//
// TODO(radu): do we get an error when the object isn't there? How can we
// check for that error?
Delete(basename string) error
// Size returns the length of the named object in bytesWritten.
//
// TODO(radu): same as above - how can we tell if it's a "no such object" error?
Size(basename string) (int64, error)
}

View File

@@ -0,0 +1,145 @@
// 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 objstorage
import (
"bytes"
"encoding/binary"
"io"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/objstorage/sharedobjcat"
)
// SharedObjectBacking encodes the metadata necessary to incorporate a shared
// object into a different Pebble instance.
type SharedObjectBacking []byte
const (
tagCreatorID = 1
tagCreatorFileNum = 2
// Any new tags that don't have the tagNotSafeToIgnoreMask bit set must be
// followed by the length of the data (so they can be skipped).
// Any new tags that have the tagNotSafeToIgnoreMask bit set cause errors if
// they are encountered by earlier code that doesn't know the tag.
tagNotSafeToIgnoreMask = 64
)
// SharedObjectBacking encodes the shared object metadata.
func (meta *ObjectMetadata) SharedObjectBacking() (SharedObjectBacking, error) {
if !meta.IsShared() {
return nil, errors.AssertionFailedf("object %s not on shared storage", meta.FileNum)
}
buf := make([]byte, 0, binary.MaxVarintLen64*4)
buf = binary.AppendUvarint(buf, tagCreatorID)
buf = binary.AppendUvarint(buf, uint64(meta.Shared.CreatorID))
buf = binary.AppendUvarint(buf, tagCreatorFileNum)
buf = binary.AppendUvarint(buf, uint64(meta.Shared.CreatorFileNum))
return buf, nil
}
// fromSharedObjectBacking decodes the shared object metadata.
func fromSharedObjectBacking(
fileType base.FileType, fileNum base.FileNum, buf SharedObjectBacking,
) (ObjectMetadata, error) {
var creatorID uint64
var creatorFileNum uint64
br := bytes.NewReader(buf)
for {
tag, err := binary.ReadUvarint(br)
if err == io.EOF {
break
}
if err != nil {
return ObjectMetadata{}, err
}
switch tag {
case tagCreatorID:
creatorID, err = binary.ReadUvarint(br)
case tagCreatorFileNum:
creatorFileNum, err = binary.ReadUvarint(br)
// TODO(radu): encode file type as well?
default:
// Ignore unknown tags, unless they're not safe to ignore.
if tag&tagNotSafeToIgnoreMask != 0 {
return ObjectMetadata{}, errors.Newf("unknown tag %d", tag)
}
var dataLen uint64
dataLen, err = binary.ReadUvarint(br)
if err == nil {
_, err = br.Seek(int64(dataLen), io.SeekCurrent)
}
}
if err != nil {
return ObjectMetadata{}, err
}
}
if creatorID == 0 {
return ObjectMetadata{}, errors.Newf("shared object backing missing creator ID")
}
if creatorFileNum == 0 {
return ObjectMetadata{}, errors.Newf("shared object backing missing creator file num")
}
meta := ObjectMetadata{
FileNum: fileNum,
FileType: fileType,
}
meta.Shared.CreatorID = CreatorID(creatorID)
meta.Shared.CreatorFileNum = base.FileNum(creatorFileNum)
return meta, nil
}
// SharedObjectToAttach contains the arguments needed to attach an existing shared object.
type SharedObjectToAttach struct {
// FileNum is the file number that will be used to refer to this object (in
// the context of this instance).
FileNum base.FileNum
FileType base.FileType
// Backing contains the metadata for the share dobject backing (normally
// generated from a different instance).
Backing SharedObjectBacking
}
// AttachSharedObjects registers existing shared objects with this provider.
func (p *Provider) AttachSharedObjects(objs []SharedObjectToAttach) ([]ObjectMetadata, error) {
metas := make([]ObjectMetadata, len(objs))
for i, o := range objs {
meta, err := fromSharedObjectBacking(o.FileType, o.FileNum, o.Backing)
if err != nil {
return nil, err
}
metas[i] = meta
}
func() {
p.mu.Lock()
defer p.mu.Unlock()
for _, meta := range metas {
p.mu.shared.catalogBatch.AddObject(sharedobjcat.SharedObjectMetadata{
FileNum: meta.FileNum,
FileType: meta.FileType,
CreatorID: meta.Shared.CreatorID,
CreatorFileNum: meta.Shared.CreatorFileNum,
})
}
}()
if err := p.sharedSync(); err != nil {
return nil, err
}
p.mu.Lock()
defer p.mu.Unlock()
for _, meta := range metas {
p.mu.knownObjects[meta.FileNum] = meta
}
return metas, nil
}

View File

@@ -0,0 +1,97 @@
// 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 objstorage
import (
"io"
"github.com/cockroachdb/pebble/objstorage/shared"
)
// sharedReadable is a very simple implementation of Readable on top of the
// ReadCloser returned by shared.Storage.CreateObject.
type sharedReadable struct {
storage shared.Storage
objName string
size int64
// rh is used for direct ReadAt calls without a read handle.
rh sharedReadHandle
}
var _ Readable = (*sharedReadable)(nil)
func newSharedReadable(storage shared.Storage, objName string, size int64) *sharedReadable {
r := &sharedReadable{
storage: storage,
objName: objName,
size: size,
}
r.rh.readable = r
return r
}
func (r *sharedReadable) ReadAt(p []byte, offset int64) (n int, err error) {
return r.rh.ReadAt(p, offset)
}
func (r *sharedReadable) Close() error {
err := r.rh.Close()
r.storage = nil
return err
}
func (r *sharedReadable) Size() int64 {
return r.size
}
func (r *sharedReadable) NewReadHandle() ReadHandle {
// TODO(radu): use a pool.
return &sharedReadHandle{readable: r}
}
type sharedReadHandle struct {
readable *sharedReadable
lastReader io.ReadCloser
lastOffset int64
}
var _ ReadHandle = (*sharedReadHandle)(nil)
func (r *sharedReadHandle) ReadAt(p []byte, offset int64) (n int, err error) {
// See if this continues the previous read so that we can reuse the last reader.
if r.lastReader == nil || r.lastOffset != offset {
// We need to create a new reader.
if r.lastReader != nil {
if err := r.lastReader.Close(); err != nil {
return 0, err
}
r.lastReader = nil
}
reader, _, err := r.readable.storage.ReadObjectAt(r.readable.objName, offset)
if err != nil {
return 0, err
}
r.lastReader = reader
r.lastOffset = offset
}
n, err = io.ReadFull(r.lastReader, p)
r.lastOffset += int64(n)
return n, err
}
func (r *sharedReadHandle) Close() error {
var err error
if r.lastReader != nil {
err = r.lastReader.Close()
r.lastReader = nil
}
r.readable = nil
return err
}
func (r *sharedReadHandle) MaxReadahead() {}
func (r *sharedReadHandle) RecordCacheHit(offset, size int64) {}

View File

@@ -0,0 +1,34 @@
// 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 objstorage
import "io"
// sharedWritable is a very simple implementation of Writable on top of the
// WriteCloser returned by shared.Storage.CreateObject.
type sharedWritable struct {
storageWriter io.WriteCloser
}
var _ Writable = (*sharedWritable)(nil)
// Write is part of the Writable interface.
func (w *sharedWritable) Write(p []byte) error {
_, err := w.storageWriter.Write(p)
return err
}
// Finish is part of the Writable interface.
func (w *sharedWritable) Finish() error {
err := w.storageWriter.Close()
w.storageWriter = nil
return err
}
// Abort is part of the Writable interface.
func (w *sharedWritable) Abort() {
_ = w.storageWriter.Close()
w.storageWriter = nil
}

View File

@@ -0,0 +1,382 @@
// 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 sharedobjcat
import (
"fmt"
"io"
"sort"
"sync"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/record"
"github.com/cockroachdb/pebble/vfs"
"github.com/cockroachdb/pebble/vfs/atomicfs"
)
// Catalog is used to manage the on-disk shared object catalog.
//
// The catalog file is a log of records, where each record is an encoded
// versionEdit.
type Catalog struct {
fs vfs.FS
dirname string
mu struct {
sync.Mutex
creatorID CreatorID
objects map[base.FileNum]SharedObjectMetadata
marker *atomicfs.Marker
catalogFile vfs.File
catalogRecWriter *record.Writer
rotationHelper record.RotationHelper
// catalogFilename is the filename of catalogFile when catalogFile != nil, otherwise
// it is the filename of the last catalog file.
catalogFilename string
}
}
// CreatorID identifies the DB instance that originally created a shared object.
// This ID is incorporated in backing object names.
// Must be non-zero.
type CreatorID uint64
// IsSet returns true if the CreatorID is not zero.
func (c CreatorID) IsSet() bool { return c != 0 }
func (c CreatorID) String() string { return fmt.Sprintf("%020d", c) }
// SharedObjectMetadata encapsulates the data stored in the catalog file for each object.
type SharedObjectMetadata struct {
// FileNum is the identifier for the object within the context of a single DB
// instance.
FileNum base.FileNum
// FileType is the type of the object. Only certain FileTypes are possible.
FileType base.FileType
// CreatorID identifies the DB instance that originally created the object.
CreatorID CreatorID
// CreatorFileNum is the identifier for the object within the context of the
// DB instance that originally created the object.
CreatorFileNum base.FileNum
}
const (
catalogFilenameBase = "SHARED-CATALOG"
catalogMarkerName = "shared-catalog"
// We create a new file when the size exceeds 1MB (and some other conditions
// hold; see record.RotationHelper).
rotateFileSize = 1024 * 1024 // 1MB
)
// CatalogContents contains the shared objects in the catalog.
type CatalogContents struct {
// CreatorID, if it is set.
CreatorID CreatorID
Objects []SharedObjectMetadata
}
// Open creates a Catalog and loads any existing catalog file, returning the
// creator ID (if it is set) and the contents.
func Open(fs vfs.FS, dirname string) (*Catalog, CatalogContents, error) {
c := &Catalog{
fs: fs,
dirname: dirname,
}
c.mu.objects = make(map[base.FileNum]SharedObjectMetadata)
var err error
c.mu.marker, c.mu.catalogFilename, err = atomicfs.LocateMarker(fs, dirname, catalogMarkerName)
if err != nil {
return nil, CatalogContents{}, err
}
// If the filename is empty, there is no existing catalog.
if c.mu.catalogFilename != "" {
if err := c.loadFromCatalogFile(c.mu.catalogFilename); err != nil {
return nil, CatalogContents{}, err
}
if err := c.mu.marker.RemoveObsolete(); err != nil {
return nil, CatalogContents{}, err
}
// TODO(radu): remove obsolete catalog files.
}
res := CatalogContents{
CreatorID: c.mu.creatorID,
Objects: make([]SharedObjectMetadata, 0, len(c.mu.objects)),
}
for _, meta := range c.mu.objects {
res.Objects = append(res.Objects, meta)
}
// Sort the objects so the function is deterministic.
sort.Slice(res.Objects, func(i, j int) bool {
return res.Objects[i].FileNum < res.Objects[j].FileNum
})
return c, res, nil
}
// SetCreatorID sets the creator ID. If it is already set, it must match.
func (c *Catalog) SetCreatorID(id CreatorID) error {
if !id.IsSet() {
return errors.AssertionFailedf("attempt to unset CreatorID")
}
c.mu.Lock()
defer c.mu.Unlock()
if c.mu.creatorID.IsSet() {
if c.mu.creatorID != id {
return errors.AssertionFailedf("attempt to change CreatorID from %s to %s", c.mu.creatorID, id)
}
return nil
}
ve := versionEdit{CreatorID: id}
if err := c.writeToCatalogFileLocked(&ve); err != nil {
return errors.Wrapf(err, "pebble: could not write to shared object catalog: %v", err)
}
c.mu.creatorID = id
return nil
}
// Close any open files.
func (c *Catalog) Close() error {
return c.closeCatalogFile()
}
func (c *Catalog) closeCatalogFile() error {
if c.mu.catalogFile == nil {
return nil
}
err1 := c.mu.catalogRecWriter.Close()
err2 := c.mu.catalogFile.Close()
c.mu.catalogRecWriter = nil
c.mu.catalogFile = nil
if err1 != nil {
return err1
}
return err2
}
// Batch is used to perform multiple object additions/deletions at once.
type Batch struct {
ve versionEdit
}
// AddObject adds a new object to the batch.
//
// The given FileNum must be new - it must not match that of any object that was
// ever in the catalog.
func (b *Batch) AddObject(meta SharedObjectMetadata) {
b.ve.NewObjects = append(b.ve.NewObjects, meta)
}
// DeleteObject adds an object removal to the batch.
func (b *Batch) DeleteObject(fileNum base.FileNum) {
b.ve.DeletedObjects = append(b.ve.DeletedObjects, fileNum)
}
// Reset clears the batch.
func (b *Batch) Reset() {
b.ve.NewObjects = b.ve.NewObjects[:0]
b.ve.DeletedObjects = b.ve.DeletedObjects[:0]
}
// IsEmpty returns true if the batch is empty.
func (b *Batch) IsEmpty() bool {
return len(b.ve.NewObjects) == 0 && len(b.ve.DeletedObjects) == 0
}
// Copy returns a copy of the Batch.
func (b *Batch) Copy() Batch {
var res Batch
if len(b.ve.NewObjects) > 0 {
res.ve.NewObjects = make([]SharedObjectMetadata, len(b.ve.NewObjects))
copy(res.ve.NewObjects, b.ve.NewObjects)
}
if len(b.ve.DeletedObjects) > 0 {
res.ve.DeletedObjects = make([]base.FileNum, len(b.ve.DeletedObjects))
copy(res.ve.DeletedObjects, b.ve.DeletedObjects)
}
return res
}
// Append merges two batches.
func (b *Batch) Append(other Batch) {
b.ve.NewObjects = append(b.ve.NewObjects, other.ve.NewObjects...)
b.ve.DeletedObjects = append(b.ve.DeletedObjects, other.ve.DeletedObjects...)
}
// ApplyBatch applies a batch of updates; returns after the change is stably
// recorded on storage.
func (c *Catalog) ApplyBatch(b Batch) error {
c.mu.Lock()
defer c.mu.Unlock()
for _, n := range b.ve.DeletedObjects {
if _, exists := c.mu.objects[n]; !exists {
return errors.AssertionFailedf("deleting non-existent object %s", n)
}
}
for _, meta := range b.ve.NewObjects {
if _, exists := c.mu.objects[meta.FileNum]; exists {
return errors.AssertionFailedf("adding existing object %s", meta.FileNum)
}
}
if err := c.writeToCatalogFileLocked(&b.ve); err != nil {
return errors.Wrapf(err, "pebble: could not write to shared object catalog: %v", err)
}
// Apply the batch to our current state.
for _, n := range b.ve.DeletedObjects {
delete(c.mu.objects, n)
}
for _, meta := range b.ve.NewObjects {
c.mu.objects[meta.FileNum] = meta
}
b.Reset()
return nil
}
func (c *Catalog) loadFromCatalogFile(filename string) error {
catalogPath := c.fs.PathJoin(c.dirname, filename)
f, err := c.fs.Open(catalogPath)
if err != nil {
return errors.Wrapf(
err, "pebble: could not open shared object catalog file %q for DB %q",
errors.Safe(filename), c.dirname,
)
}
defer f.Close()
rr := record.NewReader(f, 0 /* logNum */)
for {
r, err := rr.Next()
if err == io.EOF || record.IsInvalidRecord(err) {
break
}
if err != nil {
return errors.Wrapf(err, "pebble: error when loading shared object catalog file %q",
errors.Safe(filename))
}
var ve versionEdit
err = ve.Decode(r)
if err != nil {
return errors.Wrapf(err, "pebble: error when loading shared object catalog file %q",
errors.Safe(filename))
}
// Apply the version edit to the current state.
if ve.CreatorID.IsSet() {
c.mu.creatorID = ve.CreatorID
}
for _, fileNum := range ve.DeletedObjects {
delete(c.mu.objects, fileNum)
}
for _, meta := range ve.NewObjects {
c.mu.objects[meta.FileNum] = meta
}
}
return nil
}
// writeToCatalogFileLocked writes a versionEdit to the catalog file.
// Creates a new file if this is the first write.
func (c *Catalog) writeToCatalogFileLocked(ve *versionEdit) error {
c.mu.rotationHelper.AddRecord(int64(len(ve.NewObjects) + len(ve.DeletedObjects)))
snapshotSize := int64(len(c.mu.objects))
var shouldRotate bool
if c.mu.catalogFile == nil {
shouldRotate = true
} else if c.mu.catalogRecWriter.Size() >= rotateFileSize {
shouldRotate = c.mu.rotationHelper.ShouldRotate(snapshotSize)
}
if shouldRotate {
if c.mu.catalogFile != nil {
if err := c.closeCatalogFile(); err != nil {
return err
}
}
if err := c.createNewCatalogFileLocked(); err != nil {
return err
}
c.mu.rotationHelper.Rotate(snapshotSize)
}
return writeRecord(ve, c.mu.catalogFile, c.mu.catalogRecWriter)
}
func makeCatalogFilename(iter uint64) string {
return fmt.Sprintf("%s-%06d", catalogFilenameBase, iter)
}
// createNewCatalogFileLocked creates a new catalog file, populates it with the
// current catalog and sets c.mu.catalogFile and c.mu.catalogRecWriter.
func (c *Catalog) createNewCatalogFileLocked() (outErr error) {
if c.mu.catalogFile != nil {
return errors.AssertionFailedf("catalogFile already open")
}
filename := makeCatalogFilename(c.mu.marker.NextIter())
filepath := c.fs.PathJoin(c.dirname, filename)
file, err := c.fs.Create(filepath)
if err != nil {
return err
}
recWriter := record.NewWriter(file)
err = func() error {
// Create a versionEdit that gets us from an empty catalog to the current state.
var ve versionEdit
ve.NewObjects = make([]SharedObjectMetadata, 0, len(c.mu.objects))
for _, meta := range c.mu.objects {
ve.NewObjects = append(ve.NewObjects, meta)
}
if err := writeRecord(&ve, file, recWriter); err != nil {
return err
}
// Move the marker to the new filename. Move handles syncing the data
// directory as well.
if err := c.mu.marker.Move(filename); err != nil {
return errors.Wrap(err, "moving marker")
}
return nil
}()
if err != nil {
_ = recWriter.Close()
_ = file.Close()
_ = c.fs.Remove(filepath)
return err
}
// Remove any previous file (ignoring any error).
if c.mu.catalogFilename != "" {
_ = c.fs.Remove(c.fs.PathJoin(c.dirname, c.mu.catalogFilename))
}
c.mu.catalogFile = file
c.mu.catalogRecWriter = recWriter
c.mu.catalogFilename = filename
return nil
}
func writeRecord(ve *versionEdit, file vfs.File, recWriter *record.Writer) error {
w, err := recWriter.Next()
if err != nil {
return err
}
if err := ve.Encode(w); err != nil {
return err
}
if err := recWriter.Flush(); err != nil {
return err
}
return file.Sync()
}

View File

@@ -0,0 +1,158 @@
// 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 sharedobjcat
import (
"bufio"
"encoding/binary"
"io"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
)
// versionEdit is a modification to the shared object state which can be encoded
// into a record.
//
// TODO(radu): consider adding creation and deletion time for debugging purposes.
type versionEdit struct {
NewObjects []SharedObjectMetadata
DeletedObjects []base.FileNum
CreatorID CreatorID
}
const (
// tagNewObject is followed by the FileNum, creator ID and creator FileNum.
tagNewObject = 1
// tagDeletedObject is followed by the FileNum.
tagDeletedObject = 2
// tagCreatorID is followed by the Creator ID for this store. This ID can
// never change.
tagCreatorID = 3
)
// Object type values. We don't want to encode FileType directly because it is
// more general (and we want freedom to change it in the future).
const (
objTypeTable = 1
)
func objTypeToFileType(objType uint64) (base.FileType, error) {
switch objType {
case objTypeTable:
return base.FileTypeTable, nil
default:
return 0, errors.Newf("unknown object type %d", objType)
}
}
func fileTypeToObjType(fileType base.FileType) (uint64, error) {
switch fileType {
case base.FileTypeTable:
return objTypeTable, nil
default:
return 0, errors.Newf("unknown object type for file type %d", fileType)
}
}
// Encode encodes an edit to the specified writer.
func (v *versionEdit) Encode(w io.Writer) error {
buf := make([]byte, 0, binary.MaxVarintLen64*(len(v.NewObjects)*4+len(v.DeletedObjects)*2+2))
for _, meta := range v.NewObjects {
objType, err := fileTypeToObjType(meta.FileType)
if err != nil {
return err
}
buf = binary.AppendUvarint(buf, uint64(tagNewObject))
buf = binary.AppendUvarint(buf, uint64(meta.FileNum))
buf = binary.AppendUvarint(buf, objType)
buf = binary.AppendUvarint(buf, uint64(meta.CreatorID))
buf = binary.AppendUvarint(buf, uint64(meta.CreatorFileNum))
}
for _, fileNum := range v.DeletedObjects {
buf = binary.AppendUvarint(buf, uint64(tagDeletedObject))
buf = binary.AppendUvarint(buf, uint64(fileNum))
}
if v.CreatorID.IsSet() {
buf = binary.AppendUvarint(buf, uint64(tagCreatorID))
buf = binary.AppendUvarint(buf, uint64(v.CreatorID))
}
_, err := w.Write(buf)
return err
}
// Decode decodes an edit from the specified reader.
func (v *versionEdit) Decode(r io.Reader) error {
br, ok := r.(io.ByteReader)
if !ok {
br = bufio.NewReader(r)
}
for {
tag, err := binary.ReadUvarint(br)
if err == io.EOF {
break
}
if err != nil {
return err
}
err = nil
switch tag {
case tagNewObject:
var fileNum, creatorID, creatorFileNum uint64
var fileType base.FileType
fileNum, err = binary.ReadUvarint(br)
if err == nil {
var objType uint64
objType, err = binary.ReadUvarint(br)
if err == nil {
fileType, err = objTypeToFileType(objType)
}
}
if err == nil {
creatorID, err = binary.ReadUvarint(br)
}
if err == nil {
creatorFileNum, err = binary.ReadUvarint(br)
}
if err == nil {
v.NewObjects = append(v.NewObjects, SharedObjectMetadata{
FileNum: base.FileNum(fileNum),
FileType: fileType,
CreatorID: CreatorID(creatorID),
CreatorFileNum: base.FileNum(creatorFileNum),
})
}
case tagDeletedObject:
var fileNum uint64
fileNum, err = binary.ReadUvarint(br)
if err == nil {
v.DeletedObjects = append(v.DeletedObjects, base.FileNum(fileNum))
}
case tagCreatorID:
var id uint64
id, err = binary.ReadUvarint(br)
if err == nil {
v.CreatorID = CreatorID(id)
}
default:
// Ignore unknown tags.
}
if err != nil {
if err == io.EOF {
return errCorruptCatalog
}
return err
}
}
return nil
}
var errCorruptCatalog = base.CorruptionErrorf("pebble: corrupt shared object catalog")

109
vendor/github.com/cockroachdb/pebble/objstorage/vfs.go generated vendored Normal file
View File

@@ -0,0 +1,109 @@
// 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 objstorage
import (
"github.com/cockroachdb/errors"
"github.com/cockroachdb/pebble/internal/base"
"github.com/cockroachdb/pebble/vfs"
)
func (p *Provider) vfsPath(fileType base.FileType, fileNum base.FileNum) string {
return base.MakeFilepath(p.st.FS, p.st.FSDirName, fileType, fileNum)
}
func (p *Provider) vfsOpenForReading(
fileType base.FileType, fileNum base.FileNum, mustExist bool,
) (Readable, error) {
filename := p.vfsPath(fileType, fileNum)
file, err := p.st.FS.Open(filename, vfs.RandomReadsOption)
if err != nil {
if mustExist {
base.MustExist(p.st.FS, filename, p.st.Logger, err)
}
return nil, err
}
// TODO(radu): we use the existence of the file descriptor as an indication
// that the File might support Prefetch and SequentialReadsOption. We should
// replace this with a cleaner way to obtain the capabilities of the FS / File.
if fd := file.Fd(); fd != vfs.InvalidFd {
return newFileReadable(file, p.st.FS, filename)
}
return newGenericFileReadable(file)
}
func (p *Provider) vfsCreate(
fileType base.FileType, fileNum base.FileNum,
) (Writable, ObjectMetadata, error) {
filename := p.vfsPath(fileType, fileNum)
file, err := p.st.FS.Create(filename)
if err != nil {
return nil, ObjectMetadata{}, err
}
file = vfs.NewSyncingFile(file, vfs.SyncingFileOptions{
NoSyncOnClose: p.st.NoSyncOnClose,
BytesPerSync: p.st.BytesPerSync,
})
meta := ObjectMetadata{
FileNum: fileNum,
FileType: fileType,
}
return newFileBufferedWritable(file), meta, nil
}
func (p *Provider) vfsRemove(fileType base.FileType, fileNum base.FileNum) error {
return p.st.FSCleaner.Clean(p.st.FS, fileType, p.vfsPath(fileType, fileNum))
}
// vfsInit finds any local FS objects.
func (p *Provider) vfsInit() error {
listing := p.st.FSDirInitialListing
if listing == nil {
var err error
listing, err = p.st.FS.List(p.st.FSDirName)
if err != nil {
return errors.Wrapf(err, "pebble: could not list store directory")
}
}
for _, filename := range listing {
fileType, fileNum, ok := base.ParseFilename(p.st.FS, filename)
if ok && fileType == base.FileTypeTable {
o := ObjectMetadata{
FileType: fileType,
FileNum: fileNum,
}
p.mu.knownObjects[o.FileNum] = o
}
}
return nil
}
func (p *Provider) vfsSync() error {
p.mu.Lock()
shouldSync := p.mu.localObjectsChanged
p.mu.localObjectsChanged = false
p.mu.Unlock()
if !shouldSync {
return nil
}
if err := p.fsDir.Sync(); err != nil {
p.mu.Lock()
defer p.mu.Unlock()
p.mu.localObjectsChanged = true
return err
}
return nil
}
func (p *Provider) vfsSize(fileType base.FileType, fileNum base.FileNum) (int64, error) {
filename := p.vfsPath(fileType, fileNum)
stat, err := p.st.FS.Stat(filename)
if err != nil {
return 0, err
}
return stat.Size(), nil
}

View File

@@ -0,0 +1,244 @@
// 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 objstorage
import (
"fmt"
"os"
"sync"
"github.com/cockroachdb/pebble/internal/invariants"
"github.com/cockroachdb/pebble/vfs"
)
// fileReadable implements objstorage.Readable on top of a vfs.File that
// supports Prefetch and/or on an FS that supports SequentialReadsOption.
type fileReadable struct {
file vfs.File
size int64
// The following fields are used to possibly open the file again using the
// sequential reads option (see vfsReadHandle).
filename string
fs vfs.FS
}
var _ Readable = (*fileReadable)(nil)
func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) {
info, err := file.Stat()
if err != nil {
return nil, err
}
r := &fileReadable{
file: file,
size: info.Size(),
filename: filename,
fs: fs,
}
invariants.SetFinalizer(r, func(obj interface{}) {
if obj.(*fileReadable).file != nil {
fmt.Fprintf(os.Stderr, "Readable was not closed")
os.Exit(1)
}
})
return r, nil
}
// ReadAt is part of the objstorage.Readable interface.
func (r *fileReadable) ReadAt(p []byte, off int64) (n int, err error) {
return r.file.ReadAt(p, off)
}
// Close is part of the objstorage.Readable interface.
func (r *fileReadable) Close() error {
defer func() { r.file = nil }()
return r.file.Close()
}
// Size is part of the objstorage.Readable interface.
func (r *fileReadable) Size() int64 {
return r.size
}
// NewReadHandle is part of the objstorage.Readable interface.
func (r *fileReadable) NewReadHandle() ReadHandle {
rh := readHandlePool.Get().(*vfsReadHandle)
rh.r = r
rh.rs = makeReadaheadState()
return rh
}
type vfsReadHandle struct {
r *fileReadable
rs readaheadState
// sequentialFile holds a file descriptor to the same underlying File,
// except with fadvise(FADV_SEQUENTIAL) called on it to take advantage of
// OS-level readahead. Once this is non-nil, the other variables in
// readaheadState don't matter much as we defer to OS-level readahead.
sequentialFile vfs.File
}
var _ ReadHandle = (*vfsReadHandle)(nil)
var readHandlePool = sync.Pool{
New: func() interface{} {
i := &vfsReadHandle{}
// Note: this is a no-op if invariants are disabled or race is enabled.
invariants.SetFinalizer(i, func(obj interface{}) {
if obj.(*vfsReadHandle).r != nil {
fmt.Fprintf(os.Stderr, "ReadHandle was not closed")
os.Exit(1)
}
})
return i
},
}
// Close is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) Close() error {
var err error
if rh.sequentialFile != nil {
err = rh.sequentialFile.Close()
}
*rh = vfsReadHandle{}
readHandlePool.Put(rh)
return err
}
// ReadAt is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) ReadAt(p []byte, offset int64) (n int, err error) {
if rh.sequentialFile != nil {
// Use OS-level read-ahead.
return rh.sequentialFile.ReadAt(p, offset)
}
if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 {
if readaheadSize >= maxReadaheadSize {
// We've reached the maximum readahead size. Beyond this point, rely on
// OS-level readahead.
rh.MaxReadahead()
} else {
_ = rh.r.file.Prefetch(offset, readaheadSize)
}
}
return rh.r.file.ReadAt(p, offset)
}
// MaxReadahead is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) MaxReadahead() {
if rh.sequentialFile != nil {
return
}
// TODO(radu): we could share the reopened file descriptor across multiple
// handles.
f, err := rh.r.fs.Open(rh.r.filename, vfs.SequentialReadsOption)
if err == nil {
rh.sequentialFile = f
}
}
// RecordCacheHit is part of the objstorage.ReadHandle interface.
func (rh *vfsReadHandle) RecordCacheHit(offset, size int64) {
if rh.sequentialFile != nil {
// Using OS-level readahead, so do nothing.
return
}
rh.rs.recordCacheHit(offset, size)
}
// genericFileReadable implements objstorage.Readable on top of any vfs.File.
// This implementation does not support read-ahead.
type genericFileReadable struct {
file vfs.File
size int64
rh NoopReadHandle
}
var _ Readable = (*genericFileReadable)(nil)
func newGenericFileReadable(file vfs.File) (*genericFileReadable, error) {
info, err := file.Stat()
if err != nil {
return nil, err
}
r := &genericFileReadable{
file: file,
size: info.Size(),
rh: MakeNoopReadHandle(file),
}
invariants.SetFinalizer(r, func(obj interface{}) {
if obj.(*genericFileReadable).file != nil {
fmt.Fprintf(os.Stderr, "Readable was not closed")
os.Exit(1)
}
})
return r, nil
}
// ReadAt is part of the objstorage.Readable interface.
func (r *genericFileReadable) ReadAt(p []byte, off int64) (n int, err error) {
return r.file.ReadAt(p, off)
}
// Close is part of the objstorage.Readable interface.
func (r *genericFileReadable) Close() error {
defer func() { r.file = nil }()
return r.file.Close()
}
// Size is part of the objstorage.Readable interface.
func (r *genericFileReadable) Size() int64 {
return r.size
}
// NewReadHandle is part of the objstorage.Readable interface.
func (r *genericFileReadable) NewReadHandle() ReadHandle {
return &r.rh
}
// TestingCheckMaxReadahead returns true if the ReadHandle has switched to
// OS-level read-ahead.
func TestingCheckMaxReadahead(rh ReadHandle) bool {
switch rh := rh.(type) {
case *vfsReadHandle:
return rh.sequentialFile != nil
case *PreallocatedReadHandle:
return rh.sequentialFile != nil
default:
panic("unknown ReadHandle type")
}
}
// PreallocatedReadHandle is used to avoid an allocation in NewReadHandle; see
// UsePreallocatedReadHandle.
type PreallocatedReadHandle struct {
vfsReadHandle
}
// Close is part of the objstorage.ReadHandle interface.
func (rh *PreallocatedReadHandle) Close() error {
var err error
if rh.sequentialFile != nil {
err = rh.sequentialFile.Close()
}
rh.vfsReadHandle = vfsReadHandle{}
return err
}
// UsePreallocatedReadHandle is equivalent to calling readable.NewReadHandle()
// but uses the existing storage of a PreallocatedReadHandle when possible
// (currently this happens if we are reading from a local file).
// The returned handle still needs to be closed.
func UsePreallocatedReadHandle(readable Readable, rh *PreallocatedReadHandle) ReadHandle {
if r, ok := readable.(*fileReadable); ok {
// See fileReadable.NewReadHandle.
rh.vfsReadHandle = vfsReadHandle{r: r}
return rh
}
return readable.NewReadHandle()
}

View File

@@ -0,0 +1,64 @@
// 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 objstorage
import (
"bufio"
"github.com/cockroachdb/pebble/vfs"
)
// NewFileWritable returns a Writable that uses a file as underlying storage.
func NewFileWritable(file vfs.File) Writable {
return newFileBufferedWritable(file)
}
type fileBufferedWritable struct {
file vfs.File
bw *bufio.Writer
}
var _ Writable = (*fileBufferedWritable)(nil)
func newFileBufferedWritable(file vfs.File) *fileBufferedWritable {
return &fileBufferedWritable{
file: file,
bw: bufio.NewWriter(file),
}
}
// Write is part of the objstorage.Writable interface.
func (w *fileBufferedWritable) Write(p []byte) error {
// Ignoring the length written since bufio.Writer.Write is guaranteed to
// return an error if the length written is < len(p).
_, err := w.bw.Write(p)
return err
}
// Finish is part of the objstorage.Writable interface.
func (w *fileBufferedWritable) Finish() error {
err := w.bw.Flush()
if err == nil {
err = w.file.Sync()
}
err = firstError(err, w.file.Close())
w.bw = nil
w.file = nil
return err
}
// Abort is part of the objstorage.Writable interface.
func (w *fileBufferedWritable) Abort() {
_ = w.file.Close()
w.bw = nil
w.file = nil
}
func firstError(err0, err1 error) error {
if err0 != nil {
return err0
}
return err1
}