[WIP] Initial commit

This commit is contained in:
eleksir
2025-05-08 18:12:52 +03:00
commit 5379ae6eb3
1516 changed files with 648142 additions and 0 deletions

View File

@@ -0,0 +1,241 @@
// Copyright 2021 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 atomicfs
import (
"fmt"
"strconv"
"strings"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/vfs"
)
// ReadMarker looks up the current state of a marker returning just the
// current value of the marker. Callers that may need to move the marker
// to a new value should use LocateMarker.
func ReadMarker(fs vfs.FS, dir, markerName string) (string, error) {
state, err := scanForMarker(fs, dir, markerName)
if err != nil {
return "", err
}
return state.value, nil
}
// LocateMarker loads the current state of a marker. It returns a handle
// to the Marker that may be used to move the marker and the
// current value of the marker.
func LocateMarker(fs vfs.FS, dir, markerName string) (*Marker, string, error) {
state, err := scanForMarker(fs, dir, markerName)
if err != nil {
return nil, "", err
}
dirFD, err := fs.OpenDir(dir)
if err != nil {
return nil, "", err
}
return &Marker{
fs: fs,
dir: dir,
dirFD: dirFD,
name: markerName,
filename: state.filename,
iter: state.iter,
obsoleteFiles: state.obsolete,
}, state.value, nil
}
type scannedState struct {
// filename is the latest marker file found (the one with the highest iter value).
filename string
iter uint64
value string
// obsolete is a list of earlier markers that were found.
obsolete []string
}
func scanForMarker(fs vfs.FS, dir, markerName string) (scannedState, error) {
ls, err := fs.List(dir)
if err != nil {
return scannedState{}, err
}
var state scannedState
for _, filename := range ls {
if !strings.HasPrefix(filename, `marker.`) {
continue
}
// Any filenames with the `marker.` prefix are required to be
// well-formed and parse as markers.
name, iter, value, err := parseMarkerFilename(filename)
if err != nil {
return scannedState{}, err
}
if name != markerName {
continue
}
if state.filename == "" || state.iter < iter {
if state.filename != "" {
state.obsolete = append(state.obsolete, state.filename)
}
state.filename = filename
state.iter = iter
state.value = value
} else {
state.obsolete = append(state.obsolete, filename)
}
}
return state, nil
}
// A Marker provides an interface for maintaining a single string value on the
// filesystem. The marker may be atomically moved from value to value.
//
// The implementation creates a new marker file for each new value, embedding
// the value in the marker filename.
//
// Marker is not safe for concurrent use. Multiple processes may not read or
// move the same marker simultaneously. A Marker may only be constructed through
// LocateMarker.
//
// Marker names must be unique within the directory.
type Marker struct {
fs vfs.FS
dir string
dirFD vfs.File
// name identifies the marker.
name string
// filename contains the entire filename of the current marker. It
// has a format of `marker.<name>.<iter>.<value>`. It's not
// necessarily in sync with iter, since filename is only updated
// when the marker is successfully moved.
filename string
// iter holds the current iteration value. It matches the iteration
// value encoded within filename, if filename is non-empty. Iter is
// monotonically increasing over the lifetime of a marker. Actual
// marker files will always have a positive iter value.
iter uint64
// obsoleteFiles holds a list of marker files discovered by LocateMarker that
// are old values for this marker. These files may exist in certain error
// cases or crashes (e.g. if the deletion of the previous marker file failed
// during Move).
obsoleteFiles []string
}
func markerFilename(name string, iter uint64, value string) string {
return fmt.Sprintf("marker.%s.%06d.%s", name, iter, value)
}
func parseMarkerFilename(s string) (name string, iter uint64, value string, err error) {
// Check for and remove the `marker.` prefix.
if !strings.HasPrefix(s, `marker.`) {
return "", 0, "", errors.Newf("invalid marker filename: %q", s)
}
s = s[len(`marker.`):]
// Extract the marker's name.
i := strings.IndexByte(s, '.')
if i == -1 {
return "", 0, "", errors.Newf("invalid marker filename: %q", s)
}
name = s[:i]
s = s[i+1:]
// Extract the marker's iteration number.
i = strings.IndexByte(s, '.')
if i == -1 {
return "", 0, "", errors.Newf("invalid marker filename: %q", s)
}
iter, err = strconv.ParseUint(s[:i], 10, 64)
if err != nil {
return "", 0, "", errors.Newf("invalid marker filename: %q", s)
}
// Everything after the iteration's `.` delimiter is the value.
s = s[i+1:]
return name, iter, s, nil
}
// Close releases all resources in use by the marker.
func (a *Marker) Close() error {
return a.dirFD.Close()
}
// Move atomically moves the marker to a new value.
//
// If Move returns a nil error, the new marker value is guaranteed to be
// persisted to stable storage. If Move returns an error, the current
// value of the marker may be the old value or the new value. Callers
// may retry a Move error.
//
// If an error occurs while syncing the directory, Move panics.
func (a *Marker) Move(newValue string) error {
a.iter++
dstFilename := markerFilename(a.name, a.iter, newValue)
dstPath := a.fs.PathJoin(a.dir, dstFilename)
oldFilename := a.filename
// Create the new marker.
f, err := a.fs.Create(dstPath)
if err != nil {
// On a distributed filesystem, an error doesn't guarantee that
// the file wasn't created. A retry of the same Move call will
// use a new iteration value, and try to a create a new file. If
// the errored invocation was actually successful in creating
// the file, we'll leak a file. That's okay, because the next
// time the marker is located we'll add it to the obsolete files
// list.
//
// Note that the unconditional increment of `a.iter` means that
// `a.iter` and `a.filename` are not necessarily in sync,
// because `a.filename` is only updated on success.
return err
}
a.filename = dstFilename
if err := f.Close(); err != nil {
return err
}
// Remove the now defunct file. If an error is surfaced, we record
// the file as an obsolete file. The file's presence does not
// affect correctness, and it will be cleaned up the next time
// RemoveObsolete is called, either by this process or the next.
if oldFilename != "" {
if err := a.fs.Remove(a.fs.PathJoin(a.dir, oldFilename)); err != nil && !oserror.IsNotExist(err) {
a.obsoleteFiles = append(a.obsoleteFiles, oldFilename)
}
}
// Sync the directory to ensure marker movement is synced.
if err := a.dirFD.Sync(); err != nil {
// Fsync errors are unrecoverable.
// See https://wiki.postgresql.org/wiki/Fsync_Errors and
// https://danluu.com/fsyncgate.
panic(errors.WithStack(err))
}
return nil
}
// NextIter returns the next iteration number that the marker will use.
// Clients may use this number for formulating new values that are
// unused.
func (a *Marker) NextIter() uint64 {
return a.iter + 1
}
// RemoveObsolete removes any obsolete files discovered while locating
// the marker or files unable to be removed during Move.
func (a *Marker) RemoveObsolete() error {
for i, filename := range a.obsoleteFiles {
if err := a.fs.Remove(a.fs.PathJoin(a.dir, filename)); err != nil && !oserror.IsNotExist(err) {
a.obsoleteFiles = a.obsoleteFiles[i:]
return err
}
}
a.obsoleteFiles = nil
return nil
}

137
vendor/github.com/cockroachdb/pebble/vfs/clone.go generated vendored Normal file
View File

@@ -0,0 +1,137 @@
// Copyright 2019 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 vfs
import (
"io"
"sort"
"github.com/cockroachdb/errors/oserror"
)
type cloneOpts struct {
skip func(string) bool
sync bool
tryLink bool
}
// A CloneOption configures the behavior of Clone.
type CloneOption func(*cloneOpts)
// CloneSkip configures Clone to skip files for which the provided function
// returns true when passed the file's path.
func CloneSkip(fn func(string) bool) CloneOption {
return func(co *cloneOpts) { co.skip = fn }
}
// CloneSync configures Clone to sync files and directories.
var CloneSync CloneOption = func(o *cloneOpts) { o.sync = true }
// CloneTryLink configures Clone to link files to the destination if the source and
// destination filesystems are the same. If the source and destination
// filesystems are not the same or the filesystem does not support linking, then
// Clone falls back to copying.
var CloneTryLink CloneOption = func(o *cloneOpts) { o.tryLink = true }
// Clone recursively copies a directory structure from srcFS to dstFS. srcPath
// specifies the path in srcFS to copy from and must be compatible with the
// srcFS path format. dstDir is the target directory in dstFS and must be
// compatible with the dstFS path format. Returns (true,nil) on a successful
// copy, (false,nil) if srcPath does not exist, and (false,err) if an error
// occurred.
func Clone(srcFS, dstFS FS, srcPath, dstPath string, opts ...CloneOption) (bool, error) {
var o cloneOpts
for _, opt := range opts {
opt(&o)
}
srcFile, err := srcFS.Open(srcPath)
if err != nil {
if oserror.IsNotExist(err) {
// Ignore non-existent errors. Those will translate into non-existent
// files in the destination filesystem.
return false, nil
}
return false, err
}
defer srcFile.Close()
stat, err := srcFile.Stat()
if err != nil {
return false, err
}
if stat.IsDir() {
if err := dstFS.MkdirAll(dstPath, 0755); err != nil {
return false, err
}
list, err := srcFS.List(srcPath)
if err != nil {
return false, err
}
// Sort the paths so we get deterministic test output.
sort.Strings(list)
for _, name := range list {
if o.skip != nil && o.skip(srcFS.PathJoin(srcPath, name)) {
continue
}
_, err := Clone(srcFS, dstFS, srcFS.PathJoin(srcPath, name), dstFS.PathJoin(dstPath, name), opts...)
if err != nil {
return false, err
}
}
if o.sync {
dir, err := dstFS.OpenDir(dstPath)
if err != nil {
return false, err
}
if err := dir.Sync(); err != nil {
return false, err
}
if err := dir.Close(); err != nil {
return false, err
}
}
return true, nil
}
// If the source and destination filesystems are the same and the user
// specified they'd prefer to link if possible, try to use a hardlink,
// falling back to copying if it fails.
if srcFS == dstFS && o.tryLink {
if err := LinkOrCopy(srcFS, srcPath, dstPath); oserror.IsNotExist(err) {
// Clone's semantics are such that it returns (false,nil) if the
// source does not exist.
return false, nil
} else if err != nil {
return false, err
} else {
return true, nil
}
}
data, err := io.ReadAll(srcFile)
if err != nil {
return false, err
}
dstFile, err := dstFS.Create(dstPath)
if err != nil {
return false, err
}
if _, err = dstFile.Write(data); err != nil {
return false, err
}
if o.sync {
if err := dstFile.Sync(); err != nil {
return false, err
}
}
if err := dstFile.Close(); err != nil {
return false, err
}
return true, nil
}

View File

@@ -0,0 +1,132 @@
// 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.
//go:build linux
// +build linux
package vfs
import (
"os"
"syscall"
"github.com/cockroachdb/errors"
"golang.org/x/sys/unix"
)
func wrapOSFileImpl(f *os.File) File {
lf := &linuxFile{File: f, fd: f.Fd()}
if lf.fd != InvalidFd {
lf.useSyncRange = isSyncRangeSupported(lf.fd)
}
return lf
}
func (defaultFS) OpenDir(name string) (File, error) {
f, err := os.OpenFile(name, syscall.O_CLOEXEC, 0)
if err != nil {
return nil, errors.WithStack(err)
}
return &linuxDir{f}, nil
}
// Assert that linuxFile and linuxDir implement vfs.File.
var (
_ File = (*linuxDir)(nil)
_ File = (*linuxFile)(nil)
)
type linuxDir struct {
*os.File
}
func (d *linuxDir) Prefetch(offset int64, length int64) error { return nil }
func (d *linuxDir) Preallocate(offset, length int64) error { return nil }
func (d *linuxDir) SyncData() error { return d.Sync() }
func (d *linuxDir) SyncTo(offset int64) (fullSync bool, err error) { return false, nil }
type linuxFile struct {
*os.File
fd uintptr
useSyncRange bool
}
func (f *linuxFile) Prefetch(offset int64, length int64) error {
_, _, err := unix.Syscall(unix.SYS_READAHEAD, uintptr(f.fd), uintptr(offset), uintptr(length))
return err
}
func (f *linuxFile) Preallocate(offset, length int64) error {
return unix.Fallocate(int(f.fd), unix.FALLOC_FL_KEEP_SIZE, offset, length)
}
func (f *linuxFile) SyncData() error {
return unix.Fdatasync(int(f.fd))
}
func (f *linuxFile) SyncTo(offset int64) (fullSync bool, err error) {
if !f.useSyncRange {
// Use fdatasync, which does provide persistence guarantees but won't
// update all file metadata. From the `fdatasync` man page:
//
// fdatasync() is similar to fsync(), but does not flush modified
// metadata unless that metadata is needed in order to allow a
// subsequent data retrieval to be correctly handled. For example,
// changes to st_atime or st_mtime (respectively, time of last access
// and time of last modification; see stat(2)) do not require flushing
// because they are not necessary for a subsequent data read to be
// handled correctly. On the other hand, a change to the file size
// (st_size, as made by say ftruncate(2)), would require a metadata
// flush.
if err = unix.Fdatasync(int(f.fd)); err != nil {
return false, err
}
return true, nil
}
const (
waitBefore = 0x1
write = 0x2
// waitAfter = 0x4
)
// By specifying write|waitBefore for the flags, we're instructing
// SyncFileRange to a) wait for any outstanding data being written to finish,
// and b) to queue any other dirty data blocks in the range [0,offset] for
// writing. The actual writing of this data will occur asynchronously. The
// use of `waitBefore` is to limit how much dirty data is allowed to
// accumulate. Linux sometimes behaves poorly when a large amount of dirty
// data accumulates, impacting other I/O operations.
return false, unix.SyncFileRange(int(f.fd), 0, offset, write|waitBefore)
}
type syncFileRange func(fd int, off int64, n int64, flags int) (err error)
// sync_file_range depends on both the filesystem, and the broader kernel
// support. In particular, Windows Subsystem for Linux does not support
// sync_file_range, even when used with ext{2,3,4}. syncRangeSmokeTest performs
// a test of of sync_file_range, returning false on ENOSYS, and true otherwise.
func syncRangeSmokeTest(fd uintptr, syncFn syncFileRange) bool {
err := syncFn(int(fd), 0 /* offset */, 0 /* nbytes */, 0 /* flags */)
return err != unix.ENOSYS
}
func isSyncRangeSupported(fd uintptr) bool {
var stat unix.Statfs_t
if err := unix.Fstatfs(int(fd), &stat); err != nil {
return false
}
// Allowlist which filesystems we allow using sync_file_range with as some
// filesystems treat that syscall as a noop (notably ZFS). A allowlist is
// used instead of a denylist in order to have a more graceful failure mode
// in case a filesystem we haven't tested is encountered. Currently only
// ext2/3/4 are known to work properly.
const extMagic = 0xef53
switch stat.Type {
case extMagic:
return syncRangeSmokeTest(fd, unix.SyncFileRange)
}
return false
}

View File

@@ -0,0 +1,49 @@
// 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.
//go:build darwin || dragonfly || freebsd || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd netbsd openbsd solaris
package vfs
import (
"os"
"syscall"
"github.com/cockroachdb/errors"
)
func wrapOSFileImpl(osFile *os.File) File {
return &unixFile{File: osFile, fd: osFile.Fd()}
}
func (defaultFS) OpenDir(name string) (File, error) {
f, err := os.OpenFile(name, syscall.O_CLOEXEC, 0)
if err != nil {
return nil, errors.WithStack(err)
}
return &unixFile{f, InvalidFd}, nil
}
// Assert that unixFile implements vfs.File.
var _ File = (*unixFile)(nil)
type unixFile struct {
*os.File
fd uintptr
}
func (*unixFile) Prefetch(offset int64, length int64) error { return nil }
func (*unixFile) Preallocate(offset, length int64) error { return nil }
func (f *unixFile) SyncData() error {
return f.Sync()
}
func (f *unixFile) SyncTo(int64) (fullSync bool, err error) {
if err = f.Sync(); err != nil {
return false, err
}
return true, nil
}

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.
//go:build windows
// +build windows
package vfs
import (
"os"
"syscall"
"github.com/cockroachdb/errors"
)
func wrapOSFileImpl(f *os.File) File {
return &windowsFile{f}
}
func (defaultFS) OpenDir(name string) (File, error) {
f, err := os.OpenFile(name, syscall.O_CLOEXEC, 0)
if err != nil {
return nil, errors.WithStack(err)
}
return &windowsDir{f}, nil
}
// Assert that windowsFile and windowsDir implement vfs.File.
var (
_ File = (*windowsFile)(nil)
_ File = (*windowsDir)(nil)
)
type windowsDir struct {
*os.File
}
func (*windowsDir) Prefetch(offset int64, length int64) error { return nil }
func (*windowsDir) Preallocate(off, length int64) error { return nil }
// Silently ignore Sync() on Windows. This is the same behavior as
// RocksDB. See port/win/io_win.cc:WinDirectory::Fsync().
func (*windowsDir) Sync() error { return nil }
func (*windowsDir) SyncData() error { return nil }
func (*windowsDir) SyncTo(length int64) (fullSync bool, err error) { return false, nil }
type windowsFile struct {
*os.File
}
func (*windowsFile) Prefetch(offset int64, length int64) error { return nil }
func (*windowsFile) Preallocate(offset, length int64) error { return nil }
func (f *windowsFile) SyncData() error { return f.Sync() }
func (f *windowsFile) SyncTo(length int64) (fullSync bool, err error) {
if err = f.Sync(); err != nil {
return false, err
}
return true, nil
}

453
vendor/github.com/cockroachdb/pebble/vfs/disk_full.go generated vendored Normal file
View File

@@ -0,0 +1,453 @@
// Copyright 2021 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 vfs
import (
"io"
"os"
"sync"
"sync/atomic"
"syscall"
"github.com/cockroachdb/errors"
)
// OnDiskFull wraps the provided FS with an FS that examines returned errors,
// looking for ENOSPC errors. It invokes the provided callback when the
// underlying filesystem returns an error signifying the storage is out of
// disk space.
//
// All new writes to the filesystem are blocked while the callback executes,
// so care must be taken to avoid expensive work from within the callback.
//
// Once the callback completes, any write-oriented operations that encountered
// ENOSPC are retried exactly once. Once the callback completes, it will not
// be invoked again until a new operation that began after the callback
// returned encounters an ENOSPC error.
//
// OnDiskFull may be used to automatically manage a ballast file, which is
// removed from the filesystem from within the callback. Note that if managing
// a ballast, the caller should maintain a reference to the inner FS and
// remove the ballast on the unwrapped FS.
func OnDiskFull(fs FS, fn func()) FS {
newFS := &enospcFS{inner: fs}
newFS.mu.Cond.L = &newFS.mu.Mutex
newFS.mu.onDiskFull = fn
return newFS
}
type enospcFS struct {
inner FS
// generation is a monotonically increasing number that encodes the
// current state of ENOSPC error handling. Incoming writes are
// organized into generations to provide strong guarantees on when the
// disk full callback is invoked. The callback is invoked once per
// write generation.
//
// Special significance is given to the parity of this generation
// field to optimize incoming writes in the normal state, which only
// need to perform a single atomic load. If generation is odd, an
// ENOSPC error is being actively handled. The generations associated
// with writes are always even.
//
// The lifecycle of a write is:
//
// 1. Atomically load the current generation.
// a. If it's even, this is the write's generation number.
// b. If it's odd, an ENOSPC was recently encountered and the
// corresponding invocation of the disk full callback has not
// yet completed. The write must wait until the callback has
// completed and generation is updated to an even number, which
// becomes the write's generation number.
// 2. Perform the write. If it encounters no error or an error other
// than ENOSPC, the write returns and proceeds no further in this
// lifecycle.
// 3. Handle ENOSPC. If the write encounters ENOSPC, the callback must
// be invoked for the write's generation. The write's goroutine
// acquires the FS's mutex.
// a. If the FS's current generation is still equal to the write's
// generation, the write is the first write of its generation to
// encounter ENOSPC. It increments the FS's current generation
// to an odd number, signifying that an ENOSPC is being handled
// and invokes the callback.
// b. If the FS's current generation has changed, some other write
// from the same generation encountered an ENOSPC first. This
// write waits on the condition variable until the FS's current
// generation is updated indicating that the generation's
// callback invocation has completed.
// 3. Retry the write once. The callback for the write's generation
// has completed, either by this write's goroutine or another's.
// The write may proceed with the expectation that the callback
// remedied the full disk by freeing up disk space and an ENOSPC
// should not be encountered again for at least a few minutes. If
// we do encounter another ENOSPC on the retry, the callback was
// unable to remedy the full disk and another retry won't be
// useful. Any error, including ENOSPC, during the retry is
// returned without further handling. None of the retries invoke
// the callback.
//
// This scheme has a few nice properties:
// * Once the disk-full callback completes, it won't be invoked
// again unless a write that started strictly later encounters an
// ENOSPC. This is convenient if the callback strives to 'fix' the
// full disk, for example, by removing a ballast file. A new
// invocation of the callback guarantees a new problem.
// * Incoming writes block if there's an unhandled ENOSPC. Some
// writes, like WAL or MANIFEST fsyncs, are fatal if they encounter
// an ENOSPC.
generation atomic.Uint32
mu struct {
sync.Mutex
sync.Cond
onDiskFull func()
}
}
// Unwrap returns the underlying FS. This may be called by vfs.Root to access
// the underlying filesystem.
func (fs *enospcFS) Unwrap() FS {
return fs.inner
}
// waitUntilReady is called before every FS or File operation that
// might return ENOSPC. If an ENOSPC was encountered and the corresponding
// invocation of the `onDiskFull` callback has not yet returned,
// waitUntilReady blocks until the callback returns. The returned generation
// is always even.
func (fs *enospcFS) waitUntilReady() uint32 {
gen := fs.generation.Load()
if gen%2 == 0 {
// An even generation indicates that we're not currently handling an
// ENOSPC. Allow the write to proceed.
return gen
}
// We're currently handling an ENOSPC error. Wait on the condition
// variable until we're not handling an ENOSPC.
fs.mu.Lock()
defer fs.mu.Unlock()
// Load the generation again with fs.mu locked.
gen = fs.generation.Load()
for gen%2 == 1 {
fs.mu.Wait()
gen = fs.generation.Load()
}
return gen
}
func (fs *enospcFS) handleENOSPC(gen uint32) {
fs.mu.Lock()
defer fs.mu.Unlock()
currentGeneration := fs.generation.Load()
// If the current generation is still `gen`, this is the first goroutine
// to hit an ENOSPC within this write generation, so this goroutine is
// responsible for invoking the callback.
if currentGeneration == gen {
// Increment the generation to an odd number, indicating that the FS
// is out-of-disk space and incoming writes should pause and wait for
// the next generation before continuing.
fs.generation.Store(gen + 1)
func() {
// Drop the mutex while we invoke the callback, re-acquiring
// afterwards.
fs.mu.Unlock()
defer fs.mu.Lock()
fs.mu.onDiskFull()
}()
// Update the current generation again to an even number, indicating
// that the callback has completed for the write generation `gen`.
fs.generation.Store(gen + 2)
fs.mu.Broadcast()
return
}
// The current generation has already been incremented, so either the
// callback is currently being run by another goroutine or it's already
// completed. Wait for it complete if it hasn't already.
//
// The current generation may be updated multiple times, including to an
// odd number signifying a later write generation has already encountered
// ENOSPC. In that case, the callback was not able to remedy the full disk
// and waiting is unlikely to be helpful. Continuing to wait risks
// blocking an unbounded number of generations. Retrying and bubbling the
// ENOSPC up might be helpful if we can abort a large compaction that
// started before we became more selective about compaction picking, so
// this loop only waits for this write generation's callback and no
// subsequent generations' callbacks.
for currentGeneration == gen+1 {
fs.mu.Wait()
currentGeneration = fs.generation.Load()
}
}
func (fs *enospcFS) Create(name string) (File, error) {
gen := fs.waitUntilReady()
f, err := fs.inner.Create(name)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
f, err = fs.inner.Create(name)
}
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) Link(oldname, newname string) error {
gen := fs.waitUntilReady()
err := fs.inner.Link(oldname, newname)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
err = fs.inner.Link(oldname, newname)
}
return err
}
func (fs *enospcFS) Open(name string, opts ...OpenOption) (File, error) {
f, err := fs.inner.Open(name, opts...)
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
f, err := fs.inner.OpenReadWrite(name, opts...)
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) OpenDir(name string) (File, error) {
f, err := fs.inner.OpenDir(name)
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) Remove(name string) error {
gen := fs.waitUntilReady()
err := fs.inner.Remove(name)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
err = fs.inner.Remove(name)
}
return err
}
func (fs *enospcFS) RemoveAll(name string) error {
gen := fs.waitUntilReady()
err := fs.inner.RemoveAll(name)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
err = fs.inner.RemoveAll(name)
}
return err
}
func (fs *enospcFS) Rename(oldname, newname string) error {
gen := fs.waitUntilReady()
err := fs.inner.Rename(oldname, newname)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
err = fs.inner.Rename(oldname, newname)
}
return err
}
func (fs *enospcFS) ReuseForWrite(oldname, newname string) (File, error) {
gen := fs.waitUntilReady()
f, err := fs.inner.ReuseForWrite(oldname, newname)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
f, err = fs.inner.ReuseForWrite(oldname, newname)
}
if f != nil {
f = &enospcFile{
fs: fs,
inner: f,
}
}
return f, err
}
func (fs *enospcFS) MkdirAll(dir string, perm os.FileMode) error {
gen := fs.waitUntilReady()
err := fs.inner.MkdirAll(dir, perm)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
err = fs.inner.MkdirAll(dir, perm)
}
return err
}
func (fs *enospcFS) Lock(name string) (io.Closer, error) {
gen := fs.waitUntilReady()
closer, err := fs.inner.Lock(name)
if err != nil && isENOSPC(err) {
fs.handleENOSPC(gen)
closer, err = fs.inner.Lock(name)
}
return closer, err
}
func (fs *enospcFS) List(dir string) ([]string, error) {
return fs.inner.List(dir)
}
func (fs *enospcFS) Stat(name string) (os.FileInfo, error) {
return fs.inner.Stat(name)
}
func (fs *enospcFS) PathBase(path string) string {
return fs.inner.PathBase(path)
}
func (fs *enospcFS) PathJoin(elem ...string) string {
return fs.inner.PathJoin(elem...)
}
func (fs *enospcFS) PathDir(path string) string {
return fs.inner.PathDir(path)
}
func (fs *enospcFS) GetDiskUsage(path string) (DiskUsage, error) {
return fs.inner.GetDiskUsage(path)
}
type enospcFile struct {
fs *enospcFS
inner File
}
var _ File = (*enospcFile)(nil)
func (f *enospcFile) Close() error {
return f.inner.Close()
}
func (f *enospcFile) Read(p []byte) (n int, err error) {
return f.inner.Read(p)
}
func (f *enospcFile) ReadAt(p []byte, off int64) (n int, err error) {
return f.inner.ReadAt(p, off)
}
func (f *enospcFile) Write(p []byte) (n int, err error) {
gen := f.fs.waitUntilReady()
n, err = f.inner.Write(p)
if err != nil && isENOSPC(err) {
f.fs.handleENOSPC(gen)
var n2 int
n2, err = f.inner.Write(p[n:])
n += n2
}
return n, err
}
func (f *enospcFile) WriteAt(p []byte, ofs int64) (n int, err error) {
gen := f.fs.waitUntilReady()
n, err = f.inner.WriteAt(p, ofs)
if err != nil && isENOSPC(err) {
f.fs.handleENOSPC(gen)
var n2 int
n2, err = f.inner.WriteAt(p[n:], ofs+int64(n))
n += n2
}
return n, err
}
func (f *enospcFile) Prefetch(offset, length int64) error {
return f.inner.Prefetch(offset, length)
}
func (f *enospcFile) Preallocate(offset, length int64) error {
return f.inner.Preallocate(offset, length)
}
func (f *enospcFile) Stat() (os.FileInfo, error) {
return f.inner.Stat()
}
func (f *enospcFile) Sync() error {
gen := f.fs.waitUntilReady()
err := f.inner.Sync()
if err != nil && isENOSPC(err) {
f.fs.handleENOSPC(gen)
// NB: It is NOT safe to retry the Sync. See the PostgreSQL
// 'fsyncgate' discussion. A successful Sync after a failed one does
// not provide any guarantees and (always?) loses the unsynced writes.
// We need to bubble the error up and hope we weren't syncing a WAL or
// MANIFEST, because we'll have no choice but to crash. Errors while
// syncing an sstable will result in a failed flush/compaction, and
// the relevant sstable(s) will be marked as obsolete and deleted.
// See: https://lwn.net/Articles/752063/
}
return err
}
func (f *enospcFile) SyncData() error {
return f.inner.SyncData()
}
func (f *enospcFile) SyncTo(length int64) (fullSync bool, err error) {
return f.inner.SyncTo(length)
}
func (f *enospcFile) Fd() uintptr {
return f.inner.Fd()
}
var _ FS = (*enospcFS)(nil)
func isENOSPC(err error) bool {
err = errors.UnwrapAll(err)
e, ok := err.(syscall.Errno)
return ok && e == syscall.ENOSPC
}

791
vendor/github.com/cockroachdb/pebble/vfs/disk_health.go generated vendored Normal file
View File

@@ -0,0 +1,791 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
package vfs
import (
"fmt"
"io"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/cockroachdb/redact"
)
const (
// preallocatedSlotCount is the default number of slots available for
// concurrent filesystem operations. The slot count may be exceeded, but
// each additional slot will incur an additional allocation. We choose 16
// here with the expectation that it is significantly more than required in
// practice. See the comment above the diskHealthCheckingFS type definition.
preallocatedSlotCount = 16
// deltaBits is the number of bits in the packed 64-bit integer used for
// identifying a delta from the file creation time in milliseconds.
deltaBits = 40
// writeSizeBits is the number of bits in the packed 64-bit integer used for
// identifying the size of the write operation, if the operation is sized. See
// writeSizePrecision below for precision of size.
writeSizeBits = 20
// Track size of writes at kilobyte precision. See comment above lastWritePacked for more.
writeSizePrecision = 1024
)
// Variables to enable testing.
var (
// defaultTickInterval is the default interval between two ticks of each
// diskHealthCheckingFile loop iteration.
defaultTickInterval = 2 * time.Second
)
// OpType is the type of IO operation being monitored by a
// diskHealthCheckingFile.
type OpType uint8
// The following OpTypes is limited to the subset of file system operations that
// a diskHealthCheckingFile supports (namely writes and syncs).
const (
OpTypeUnknown OpType = iota
OpTypeWrite
OpTypeSync
OpTypeSyncData
OpTypeSyncTo
OpTypeCreate
OpTypeLink
OpTypeMkdirAll
OpTypePreallocate
OpTypeRemove
OpTypeRemoveAll
OpTypeRename
OpTypeReuseForWrite
// Note: opTypeMax is just used in tests. It must appear last in the list
// of OpTypes.
opTypeMax
)
// String implements fmt.Stringer.
func (o OpType) String() string {
switch o {
case OpTypeWrite:
return "write"
case OpTypeSync:
return "sync"
case OpTypeSyncData:
return "syncdata"
case OpTypeSyncTo:
return "syncto"
case OpTypeCreate:
return "create"
case OpTypeLink:
return "link"
case OpTypeMkdirAll:
return "mkdirall"
case OpTypePreallocate:
return "preallocate"
case OpTypeRemove:
return "remove"
case OpTypeRemoveAll:
return "removall"
case OpTypeRename:
return "rename"
case OpTypeReuseForWrite:
return "reuseforwrite"
case OpTypeUnknown:
return "unknown"
default:
panic(fmt.Sprintf("vfs: unknown op type: %d", o))
}
}
// diskHealthCheckingFile is a File wrapper to detect slow disk operations, and
// call onSlowDisk if a disk operation is seen to exceed diskSlowThreshold.
//
// This struct creates a goroutine (in startTicker()) that, at every tick
// interval, sees if there's a disk operation taking longer than the specified
// duration. This setup is preferable to creating a new timer at every disk
// operation, as it reduces overhead per disk operation.
type diskHealthCheckingFile struct {
file File
onSlowDisk func(opType OpType, writeSizeInBytes int, duration time.Duration)
diskSlowThreshold time.Duration
tickInterval time.Duration
stopper chan struct{}
// lastWritePacked is a 64-bit unsigned int. The most significant
// 40 bits represent an delta (in milliseconds) from the creation
// time of the diskHealthCheckingFile. The next most significant 20 bits
// represent the size of the write in KBs, if the write has a size. (If
// it doesn't, the 20 bits are zeroed). The least significant four bits
// contains the OpType.
//
// The use of 40 bits for an delta provides ~34 years of effective
// monitoring time before the uint wraps around, at millisecond precision.
// ~34 years of process uptime "ought to be enough for anybody". Millisecond
// writeSizePrecision is sufficient, given that we are monitoring for writes that take
// longer than one millisecond.
//
// The use of 20 bits for the size in KBs allows representing sizes up
// to nearly one GB. If the write is larger than that, we round down to ~one GB.
//
// The use of four bits for OpType allows for 16 operation types.
//
// NB: this packing scheme is not persisted, and is therefore safe to adjust
// across process boundaries.
lastWritePacked atomic.Uint64
createTime time.Time
}
// newDiskHealthCheckingFile instantiates a new diskHealthCheckingFile, with the
// specified time threshold and event listener.
func newDiskHealthCheckingFile(
file File,
diskSlowThreshold time.Duration,
onSlowDisk func(OpType OpType, writeSizeInBytes int, duration time.Duration),
) *diskHealthCheckingFile {
return &diskHealthCheckingFile{
file: file,
onSlowDisk: onSlowDisk,
diskSlowThreshold: diskSlowThreshold,
tickInterval: defaultTickInterval,
stopper: make(chan struct{}),
createTime: time.Now(),
}
}
// startTicker starts a new goroutine with a ticker to monitor disk operations.
// Can only be called if the ticker goroutine isn't running already.
func (d *diskHealthCheckingFile) startTicker() {
if d.diskSlowThreshold == 0 {
return
}
go func() {
ticker := time.NewTicker(d.tickInterval)
defer ticker.Stop()
for {
select {
case <-d.stopper:
return
case <-ticker.C:
packed := d.lastWritePacked.Load()
if packed == 0 {
continue
}
delta, writeSize, op := unpack(packed)
lastWrite := d.createTime.Add(delta)
now := time.Now()
if lastWrite.Add(d.diskSlowThreshold).Before(now) {
// diskSlowThreshold was exceeded. Call the passed-in
// listener.
d.onSlowDisk(op, writeSize, now.Sub(lastWrite))
}
}
}
}()
}
// stopTicker stops the goroutine started in startTicker.
func (d *diskHealthCheckingFile) stopTicker() {
close(d.stopper)
}
// Fd implements (vfs.File).Fd.
func (d *diskHealthCheckingFile) Fd() uintptr {
return d.file.Fd()
}
// Read implements (vfs.File).Read
func (d *diskHealthCheckingFile) Read(p []byte) (int, error) {
return d.file.Read(p)
}
// ReadAt implements (vfs.File).ReadAt
func (d *diskHealthCheckingFile) ReadAt(p []byte, off int64) (int, error) {
return d.file.ReadAt(p, off)
}
// Write implements the io.Writer interface.
func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) {
d.timeDiskOp(OpTypeWrite, int64(len(p)), func() {
n, err = d.file.Write(p)
})
return n, err
}
// Write implements the io.WriterAt interface.
func (d *diskHealthCheckingFile) WriteAt(p []byte, ofs int64) (n int, err error) {
d.timeDiskOp(OpTypeWrite, int64(len(p)), func() {
n, err = d.file.WriteAt(p, ofs)
})
return n, err
}
// Close implements the io.Closer interface.
func (d *diskHealthCheckingFile) Close() error {
d.stopTicker()
return d.file.Close()
}
// Prefetch implements (vfs.File).Prefetch.
func (d *diskHealthCheckingFile) Prefetch(offset, length int64) error {
return d.file.Prefetch(offset, length)
}
// Preallocate implements (vfs.File).Preallocate.
func (d *diskHealthCheckingFile) Preallocate(off, n int64) (err error) {
d.timeDiskOp(OpTypePreallocate, n, func() {
err = d.file.Preallocate(off, n)
})
return err
}
// Stat implements (vfs.File).Stat.
func (d *diskHealthCheckingFile) Stat() (os.FileInfo, error) {
return d.file.Stat()
}
// Sync implements the io.Syncer interface.
func (d *diskHealthCheckingFile) Sync() (err error) {
d.timeDiskOp(OpTypeSync, 0, func() {
err = d.file.Sync()
})
return err
}
// SyncData implements (vfs.File).SyncData.
func (d *diskHealthCheckingFile) SyncData() (err error) {
d.timeDiskOp(OpTypeSyncData, 0, func() {
err = d.file.SyncData()
})
return err
}
// SyncTo implements (vfs.File).SyncTo.
func (d *diskHealthCheckingFile) SyncTo(length int64) (fullSync bool, err error) {
d.timeDiskOp(OpTypeSyncTo, length, func() {
fullSync, err = d.file.SyncTo(length)
})
return fullSync, err
}
// timeDiskOp runs the specified closure and makes its timing visible to the
// monitoring goroutine, in case it exceeds one of the slow disk durations.
// opType should always be set. writeSizeInBytes should be set if the write
// operation is sized. If not, it should be set to zero.
func (d *diskHealthCheckingFile) timeDiskOp(opType OpType, writeSizeInBytes int64, op func()) {
if d == nil {
op()
return
}
delta := time.Since(d.createTime)
packed := pack(delta, writeSizeInBytes, opType)
if d.lastWritePacked.Swap(packed) != 0 {
panic("concurrent write operations detected on file")
}
defer func() {
if d.lastWritePacked.Swap(0) != packed {
panic("concurrent write operations detected on file")
}
}()
op()
}
// Note the slight lack of symmetry between pack & unpack. pack takes an int64 for writeSizeInBytes, since
// callers of pack use an int64. This is dictated by the vfs interface. unpack OTOH returns an int. This is
// safe because the packing scheme implies we only actually need 32 bits.
func pack(delta time.Duration, writeSizeInBytes int64, opType OpType) uint64 {
// We have no guarantee of clock monotonicity. If we have a small regression
// in the clock, we set deltaMillis to zero, so we can still catch the operation
// if happens to be slow.
deltaMillis := delta.Milliseconds()
if deltaMillis < 0 {
deltaMillis = 0
}
// As of 3/7/2023, the use of 40 bits for an delta provides ~34 years
// of effective monitoring time before the uint wraps around, at millisecond
// precision.
if deltaMillis > 1<<deltaBits-1 {
panic("vfs: last write delta would result in integer wraparound")
}
// See writeSizePrecision to get the unit of writeSize. As of 1/26/2023, the unit is KBs.
writeSize := writeSizeInBytes / writeSizePrecision
// If the size of the write is larger than we can store in the packed int, store the max
// value we can store in the packed int.
const writeSizeCeiling = 1<<writeSizeBits - 1
if writeSize > writeSizeCeiling {
writeSize = writeSizeCeiling
}
return uint64(deltaMillis)<<(64-deltaBits) | uint64(writeSize)<<(64-deltaBits-writeSizeBits) | uint64(opType)
}
func unpack(packed uint64) (delta time.Duration, writeSizeInBytes int, opType OpType) {
delta = time.Duration(packed>>(64-deltaBits)) * time.Millisecond
wz := int64(packed>>(64-deltaBits-writeSizeBits)) & ((1 << writeSizeBits) - 1) * writeSizePrecision
// Given the packing scheme, converting wz to an int will not truncate anything.
writeSizeInBytes = int(wz)
opType = OpType(packed & 0xf)
return delta, writeSizeInBytes, opType
}
// diskHealthCheckingDir implements disk-health checking for directories. Unlike
// other files, we allow directories to receive concurrent write operations
// (Syncs are the only write operations supported by a directory.) Since the
// diskHealthCheckingFile's timeDiskOp can only track a single in-flight
// operation at a time, we time the operation using the filesystem-level
// timeFilesystemOp function instead.
type diskHealthCheckingDir struct {
File
name string
fs *diskHealthCheckingFS
}
// Sync implements the io.Syncer interface.
func (d *diskHealthCheckingDir) Sync() (err error) {
d.fs.timeFilesystemOp(d.name, OpTypeSync, func() {
err = d.File.Sync()
})
return err
}
// DiskSlowInfo captures info about detected slow operations on the vfs.
type DiskSlowInfo struct {
// Path of file being written to.
Path string
// Operation being performed on the file.
OpType OpType
// Size of write in bytes, if the write is sized.
WriteSize int
// Duration that has elapsed since this disk operation started.
Duration time.Duration
}
func (i DiskSlowInfo) String() string {
return redact.StringWithoutMarkers(i)
}
// SafeFormat implements redact.SafeFormatter.
func (i DiskSlowInfo) SafeFormat(w redact.SafePrinter, _ rune) {
switch i.OpType {
// Operations for which i.WriteSize is meaningful.
case OpTypeWrite, OpTypeSyncTo, OpTypePreallocate:
w.Printf("disk slowness detected: %s on file %s (%d bytes) has been ongoing for %0.1fs",
redact.Safe(i.OpType.String()), redact.Safe(filepath.Base(i.Path)),
redact.Safe(i.WriteSize), redact.Safe(i.Duration.Seconds()))
default:
w.Printf("disk slowness detected: %s on file %s has been ongoing for %0.1fs",
redact.Safe(i.OpType.String()), redact.Safe(filepath.Base(i.Path)),
redact.Safe(i.Duration.Seconds()))
}
}
// diskHealthCheckingFS adds disk-health checking facilities to a VFS.
// It times disk write operations in two ways:
//
// 1. Wrapping vfs.Files.
//
// The bulk of write I/O activity is file writing and syncing, invoked through
// the `vfs.File` interface. This VFS wraps all files open for writing with a
// special diskHealthCheckingFile implementation of the vfs.File interface. See
// above for the implementation.
//
// 2. Monitoring filesystem metadata operations.
//
// Filesystem metadata operations (create, link, remove, rename, etc) are also
// sources of disk writes. Unlike a vfs.File which requires Write and Sync calls
// to be sequential, a vfs.FS may receive these filesystem metadata operations
// in parallel. To accommodate this parallelism, the diskHealthCheckingFS's
// write-oriented filesystem operations record their start times into a 'slot'
// on the filesystem. A single long-running goroutine periodically scans the
// slots looking for slow operations.
//
// The number of slots on a diskHealthCheckingFS grows to a working set of the
// maximum concurrent filesystem operations. This is expected to be very few
// for these reasons:
// 1. Pebble has limited write concurrency. Flushes, compactions and WAL
// rotations are the primary sources of filesystem metadata operations. With
// the default max-compaction concurrency, these operations require at most 5
// concurrent slots if all 5 perform a filesystem metadata operation
// simultaneously.
// 2. Pebble's limited concurrent I/O writers spend most of their time
// performing file I/O, not performing the filesystem metadata operations that
// require recording a slot on the diskHealthCheckingFS.
// 3. In CockroachDB, each additional store/Pebble instance has its own vfs.FS
// which provides a separate goroutine and set of slots.
// 4. In CockroachDB, many of the additional sources of filesystem metadata
// operations (like encryption-at-rest) are sequential with respect to Pebble's
// threads.
type diskHealthCheckingFS struct {
tickInterval time.Duration
diskSlowThreshold time.Duration
onSlowDisk func(DiskSlowInfo)
fs FS
mu struct {
sync.Mutex
tickerRunning bool
stopper chan struct{}
inflight []*slot
}
// prealloc preallocates the memory for mu.inflight slots and the slice
// itself. The contained fields are not accessed directly except by
// WithDiskHealthChecks when initializing mu.inflight. The number of slots
// in d.mu.inflight will grow to the maximum number of concurrent file
// metadata operations (create, remove, link, etc). If the number of
// concurrent operations never exceeds preallocatedSlotCount, we'll never
// incur an additional allocation.
prealloc struct {
slots [preallocatedSlotCount]slot
slotPtrSlice [preallocatedSlotCount]*slot
}
}
type slot struct {
name string
opType OpType
startNanos atomic.Int64
}
// diskHealthCheckingFS implements FS.
var _ FS = (*diskHealthCheckingFS)(nil)
// WithDiskHealthChecks wraps an FS and ensures that all write-oriented
// operations on the FS are wrapped with disk health detection checks. Disk
// operations that are observed to take longer than diskSlowThreshold trigger an
// onSlowDisk call.
//
// A threshold of zero disables disk-health checking.
func WithDiskHealthChecks(
innerFS FS, diskSlowThreshold time.Duration, onSlowDisk func(info DiskSlowInfo),
) (FS, io.Closer) {
if diskSlowThreshold == 0 {
return innerFS, noopCloser{}
}
fs := &diskHealthCheckingFS{
fs: innerFS,
tickInterval: defaultTickInterval,
diskSlowThreshold: diskSlowThreshold,
onSlowDisk: onSlowDisk,
}
fs.mu.stopper = make(chan struct{})
// The fs holds preallocated slots and a preallocated array of slot pointers
// with equal length. Initialize the inflight slice to use a slice backed by
// the preallocated array with each slot initialized to a preallocated slot.
fs.mu.inflight = fs.prealloc.slotPtrSlice[:]
for i := range fs.mu.inflight {
fs.mu.inflight[i] = &fs.prealloc.slots[i]
}
return fs, fs
}
func (d *diskHealthCheckingFS) timeFilesystemOp(name string, opType OpType, op func()) {
if d == nil {
op()
return
}
// Record this operation's start time on the FS, so that the long-running
// goroutine can monitor the filesystem operation.
//
// The diskHealthCheckingFile implementation uses a single field that is
// atomically updated, taking advantage of the fact that writes to a single
// vfs.File handle are not performed in parallel. The vfs.FS however may
// receive write filesystem operations in parallel. To accommodate this
// parallelism, writing goroutines append their start time to a
// mutex-protected vector. On ticks, the long-running goroutine scans the
// vector searching for start times older than the slow-disk threshold. When
// a writing goroutine completes its operation, it atomically overwrites its
// slot to signal completion.
var s *slot
func() {
d.mu.Lock()
defer d.mu.Unlock()
// If there's no long-running goroutine to monitor this filesystem
// operation, start one.
if !d.mu.tickerRunning {
d.startTickerLocked()
}
startNanos := time.Now().UnixNano()
for i := 0; i < len(d.mu.inflight); i++ {
if d.mu.inflight[i].startNanos.Load() == 0 {
// This slot is not in use. Claim it.
s = d.mu.inflight[i]
s.name = name
s.opType = opType
s.startNanos.Store(startNanos)
break
}
}
// If we didn't find any unused slots, create a new slot and append it.
// This slot will exist forever. The number of slots will grow to the
// maximum number of concurrent filesystem operations over the lifetime
// of the process. Only operations that grow the number of slots must
// incur an allocation.
if s == nil {
s = &slot{
name: name,
opType: opType,
}
s.startNanos.Store(startNanos)
d.mu.inflight = append(d.mu.inflight, s)
}
}()
op()
// Signal completion by zeroing the start time.
s.startNanos.Store(0)
}
// startTickerLocked starts a new goroutine with a ticker to monitor disk
// filesystem operations. Requires d.mu and !d.mu.tickerRunning.
func (d *diskHealthCheckingFS) startTickerLocked() {
d.mu.tickerRunning = true
stopper := d.mu.stopper
go func() {
ticker := time.NewTicker(d.tickInterval)
defer ticker.Stop()
type exceededSlot struct {
name string
opType OpType
startNanos int64
}
var exceededSlots []exceededSlot
for {
select {
case <-ticker.C:
// Scan the inflight slots for any slots recording a start
// time older than the diskSlowThreshold.
exceededSlots = exceededSlots[:0]
d.mu.Lock()
now := time.Now()
for i := range d.mu.inflight {
nanos := d.mu.inflight[i].startNanos.Load()
if nanos != 0 && time.Unix(0, nanos).Add(d.diskSlowThreshold).Before(now) {
// diskSlowThreshold was exceeded. Copy this inflightOp into
// exceededSlots and call d.onSlowDisk after dropping the mutex.
inflightOp := exceededSlot{
name: d.mu.inflight[i].name,
opType: d.mu.inflight[i].opType,
startNanos: nanos,
}
exceededSlots = append(exceededSlots, inflightOp)
}
}
d.mu.Unlock()
for i := range exceededSlots {
d.onSlowDisk(
DiskSlowInfo{
Path: exceededSlots[i].name,
OpType: exceededSlots[i].opType,
WriteSize: 0, // writes at the fs level are not sized
Duration: now.Sub(time.Unix(0, exceededSlots[i].startNanos)),
})
}
case <-stopper:
return
}
}
}()
}
// Close implements io.Closer. Close stops the long-running goroutine that
// monitors for slow filesystem metadata operations. Close may be called
// multiple times. If the filesystem is used after Close has been called, a new
// long-running goroutine will be created.
func (d *diskHealthCheckingFS) Close() error {
d.mu.Lock()
if !d.mu.tickerRunning {
// Nothing to stop.
d.mu.Unlock()
return nil
}
// Grab the stopper so we can request the long-running goroutine to stop.
// Replace the stopper in case this FS is reused. It's possible to Close and
// reuse a disk-health checking FS. This is to accommodate the on-by-default
// behavior in Pebble, and the possibility that users may continue to use
// the Pebble default FS beyond the lifetime of a single DB.
stopper := d.mu.stopper
d.mu.stopper = make(chan struct{})
d.mu.tickerRunning = false
d.mu.Unlock()
// Ask the long-running goroutine to stop. This is a synchronous channel
// send.
stopper <- struct{}{}
close(stopper)
return nil
}
// Create implements the FS interface.
func (d *diskHealthCheckingFS) Create(name string) (File, error) {
var f File
var err error
d.timeFilesystemOp(name, OpTypeCreate, func() {
f, err = d.fs.Create(name)
})
if err != nil {
return f, err
}
if d.diskSlowThreshold == 0 {
return f, nil
}
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(opType OpType, writeSizeInBytes int, duration time.Duration) {
d.onSlowDisk(
DiskSlowInfo{
Path: name,
OpType: opType,
WriteSize: writeSizeInBytes,
Duration: duration,
})
})
checkingFile.startTicker()
return checkingFile, nil
}
// GetDiskUsage implements the FS interface.
func (d *diskHealthCheckingFS) GetDiskUsage(path string) (DiskUsage, error) {
return d.fs.GetDiskUsage(path)
}
// Link implements the FS interface.
func (d *diskHealthCheckingFS) Link(oldname, newname string) error {
var err error
d.timeFilesystemOp(newname, OpTypeLink, func() {
err = d.fs.Link(oldname, newname)
})
return err
}
// List implements the FS interface.
func (d *diskHealthCheckingFS) List(dir string) ([]string, error) {
return d.fs.List(dir)
}
// Lock implements the FS interface.
func (d *diskHealthCheckingFS) Lock(name string) (io.Closer, error) {
return d.fs.Lock(name)
}
// MkdirAll implements the FS interface.
func (d *diskHealthCheckingFS) MkdirAll(dir string, perm os.FileMode) error {
var err error
d.timeFilesystemOp(dir, OpTypeMkdirAll, func() {
err = d.fs.MkdirAll(dir, perm)
})
return err
}
// Open implements the FS interface.
func (d *diskHealthCheckingFS) Open(name string, opts ...OpenOption) (File, error) {
return d.fs.Open(name, opts...)
}
// OpenReadWrite implements the FS interface.
func (d *diskHealthCheckingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
return d.fs.OpenReadWrite(name, opts...)
}
// OpenDir implements the FS interface.
func (d *diskHealthCheckingFS) OpenDir(name string) (File, error) {
f, err := d.fs.OpenDir(name)
if err != nil {
return f, err
}
// Directories opened with OpenDir must be opened with health checking,
// because they may be explicitly synced.
return &diskHealthCheckingDir{
File: f,
name: name,
fs: d,
}, nil
}
// PathBase implements the FS interface.
func (d *diskHealthCheckingFS) PathBase(path string) string {
return d.fs.PathBase(path)
}
// PathJoin implements the FS interface.
func (d *diskHealthCheckingFS) PathJoin(elem ...string) string {
return d.fs.PathJoin(elem...)
}
// PathDir implements the FS interface.
func (d *diskHealthCheckingFS) PathDir(path string) string {
return d.fs.PathDir(path)
}
// Remove implements the FS interface.
func (d *diskHealthCheckingFS) Remove(name string) error {
var err error
d.timeFilesystemOp(name, OpTypeRemove, func() {
err = d.fs.Remove(name)
})
return err
}
// RemoveAll implements the FS interface.
func (d *diskHealthCheckingFS) RemoveAll(name string) error {
var err error
d.timeFilesystemOp(name, OpTypeRemoveAll, func() {
err = d.fs.RemoveAll(name)
})
return err
}
// Rename implements the FS interface.
func (d *diskHealthCheckingFS) Rename(oldname, newname string) error {
var err error
d.timeFilesystemOp(newname, OpTypeRename, func() {
err = d.fs.Rename(oldname, newname)
})
return err
}
// ReuseForWrite implements the FS interface.
func (d *diskHealthCheckingFS) ReuseForWrite(oldname, newname string) (File, error) {
var f File
var err error
d.timeFilesystemOp(newname, OpTypeReuseForWrite, func() {
f, err = d.fs.ReuseForWrite(oldname, newname)
})
if err != nil {
return f, err
}
if d.diskSlowThreshold == 0 {
return f, nil
}
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(opType OpType, writeSizeInBytes int, duration time.Duration) {
d.onSlowDisk(
DiskSlowInfo{
Path: newname,
OpType: opType,
WriteSize: writeSizeInBytes,
Duration: duration,
})
})
checkingFile.startTicker()
return checkingFile, nil
}
// Stat implements the FS interface.
func (d *diskHealthCheckingFS) Stat(name string) (os.FileInfo, error) {
return d.fs.Stat(name)
}
type noopCloser struct{}
func (noopCloser) Close() error { return nil }

View File

@@ -0,0 +1,44 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//go:build linux
// +build linux
package vfs
import "golang.org/x/sys/unix"
func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) {
stat := unix.Statfs_t{}
if err := unix.Statfs(path, &stat); err != nil {
return DiskUsage{}, err
}
// We use stat.Frsize here rather than stat.Bsize because on
// Linux Bavail and Bfree are in Frsize units.
//
// On most filesystems Frsize and Bsize will be set to the
// same value, but on some filesystems bsize returns the
// "optimal transfer block size"[1] which may be different
// (typically larger) than the actual block size.
//
// This confusion is cleared up in the statvfs[2] libc function,
// but the statfs system call used above varies across
// platforms.
//
// Frsize is used by GNU coreutils and other libraries, so
// this also helps ensure that we get the same results as one
// would get if they ran `df` on the given path.
//
// [1] https://man7.org/linux/man-pages/man2/statfs.2.html
// [2] https://man7.org/linux/man-pages/man3/statvfs.3.html
freeBytes := uint64(stat.Frsize) * uint64(stat.Bfree)
availBytes := uint64(stat.Frsize) * uint64(stat.Bavail)
totalBytes := uint64(stat.Frsize) * uint64(stat.Blocks)
return DiskUsage{
AvailBytes: availBytes,
TotalBytes: totalBytes,
UsedBytes: totalBytes - freeBytes,
}, nil
}

View File

@@ -0,0 +1,26 @@
// 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.
//go:build netbsd
// +build netbsd
package vfs
import "golang.org/x/sys/unix"
func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) {
stat := unix.Statvfs_t{}
if err := unix.Statvfs(path, &stat); err != nil {
return DiskUsage{}, err
}
freeBytes := uint64(stat.Bsize) * uint64(stat.Bfree)
availBytes := uint64(stat.Bsize) * uint64(stat.Bavail)
totalBytes := uint64(stat.Bsize) * uint64(stat.Blocks)
return DiskUsage{
AvailBytes: availBytes,
TotalBytes: totalBytes,
UsedBytes: totalBytes - freeBytes,
}, nil
}

View File

@@ -0,0 +1,26 @@
// 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.
//go:build openbsd
// +build openbsd
package vfs
import "golang.org/x/sys/unix"
func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) {
stat := unix.Statfs_t{}
if err := unix.Statfs(path, &stat); err != nil {
return DiskUsage{}, err
}
freeBytes := uint64(stat.F_bsize) * uint64(stat.F_bfree)
availBytes := uint64(stat.F_bsize) * uint64(stat.F_bavail)
totalBytes := uint64(stat.F_bsize) * uint64(stat.F_blocks)
return DiskUsage{
AvailBytes: availBytes,
TotalBytes: totalBytes,
UsedBytes: totalBytes - freeBytes,
}, nil
}

View File

@@ -0,0 +1,26 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//go:build darwin || dragonfly || freebsd
// +build darwin dragonfly freebsd
package vfs
import "golang.org/x/sys/unix"
func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) {
stat := unix.Statfs_t{}
if err := unix.Statfs(path, &stat); err != nil {
return DiskUsage{}, err
}
freeBytes := uint64(stat.Bsize) * uint64(stat.Bfree)
availBytes := uint64(stat.Bsize) * uint64(stat.Bavail)
totalBytes := uint64(stat.Bsize) * uint64(stat.Blocks)
return DiskUsage{
AvailBytes: availBytes,
TotalBytes: totalBytes,
UsedBytes: totalBytes - freeBytes,
}, nil
}

View File

@@ -0,0 +1,22 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//go:build windows
// +build windows
package vfs
import "golang.org/x/sys/windows"
func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) {
p, err := windows.UTF16PtrFromString(path)
if err != nil {
return DiskUsage{}, err
}
var freeBytes uint64
du := DiskUsage{}
err = windows.GetDiskFreeSpaceEx(p, &du.AvailBytes, &du.TotalBytes, &freeBytes)
du.UsedBytes = du.TotalBytes - freeBytes
return du, err
}

View File

@@ -0,0 +1,21 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//go:build darwin || dragonfly || freebsd || linux || openbsd || netbsd
// +build darwin dragonfly freebsd linux openbsd netbsd
package vfs
import (
"github.com/cockroachdb/errors"
"golang.org/x/sys/unix"
)
var errNotEmpty = unix.ENOTEMPTY
// IsNoSpaceError returns true if the given error indicates that the disk is
// out of space.
func IsNoSpaceError(err error) bool {
return errors.Is(err, unix.ENOSPC)
}

View File

@@ -0,0 +1,22 @@
// Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
// of this source code is governed by a BSD-style license that can be found in
// the LICENSE file.
//go:build windows
// +build windows
package vfs
import (
"github.com/cockroachdb/errors"
"golang.org/x/sys/windows"
)
var errNotEmpty = windows.ERROR_DIR_NOT_EMPTY
// IsNoSpaceError returns true if the given error indicates that the disk is
// out of space.
func IsNoSpaceError(err error) bool {
return errors.Is(err, windows.ERROR_DISK_FULL) ||
errors.Is(err, windows.ERROR_HANDLE_DISK_FULL)
}

View File

@@ -0,0 +1,16 @@
// Copyright 2019 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.
//go:build !linux
// +build !linux
package vfs
func fadviseRandom(f uintptr) error {
return nil
}
func fadviseSequential(f uintptr) error {
return nil
}

View File

@@ -0,0 +1,20 @@
// Copyright 2019 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.
//go:build linux
// +build linux
package vfs
import "golang.org/x/sys/unix"
// Calls Fadvise with FADV_RANDOM to disable readahead on a file descriptor.
func fadviseRandom(f uintptr) error {
return unix.Fadvise(int(f), 0, 0, unix.FADV_RANDOM)
}
// Calls Fadvise with FADV_SEQUENTIAL to enable readahead on a file descriptor.
func fadviseSequential(f uintptr) error {
return unix.Fadvise(int(f), 0, 0, unix.FADV_SEQUENTIAL)
}

View File

@@ -0,0 +1,20 @@
// Copyright 2012 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.
//go:build !darwin && !dragonfly && !freebsd && !linux && !netbsd && !openbsd && !solaris && !windows
// +build !darwin,!dragonfly,!freebsd,!linux,!netbsd,!openbsd,!solaris,!windows
package vfs
import (
"io"
"runtime"
"github.com/cockroachdb/errors"
)
func (defFS) Lock(name string) (io.Closer, error) {
return nil, errors.Errorf("pebble: file locking is not implemented on %s/%s",
errors.Safe(runtime.GOOS), errors.Safe(runtime.GOARCH))
}

View File

@@ -0,0 +1,70 @@
// Copyright 2014 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.
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux netbsd openbsd solaris
package vfs
import (
"io"
"os"
"sync"
"github.com/cockroachdb/errors"
"golang.org/x/sys/unix"
)
var lockedFiles struct {
mu struct {
sync.Mutex
files map[string]bool
}
}
// lockCloser hides all of an os.File's methods, except for Close.
type lockCloser struct {
name string
f *os.File
}
func (l lockCloser) Close() error {
lockedFiles.mu.Lock()
defer lockedFiles.mu.Unlock()
if !lockedFiles.mu.files[l.name] {
panic(errors.Errorf("lock file %q is not locked", l.name))
}
delete(lockedFiles.mu.files, l.name)
return l.f.Close()
}
func (defaultFS) Lock(name string) (io.Closer, error) {
lockedFiles.mu.Lock()
defer lockedFiles.mu.Unlock()
if lockedFiles.mu.files == nil {
lockedFiles.mu.files = map[string]bool{}
}
if lockedFiles.mu.files[name] {
return nil, errors.New("lock held by current process")
}
f, err := os.Create(name)
if err != nil {
return nil, err
}
spec := unix.Flock_t{
Type: unix.F_WRLCK,
Whence: io.SeekStart,
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: int32(os.Getpid()),
}
if err := unix.FcntlFlock(f.Fd(), unix.F_SETLK, &spec); err != nil {
f.Close()
return nil, err
}
lockedFiles.mu.files[name] = true
return lockCloser{name, f}, nil
}

View File

@@ -0,0 +1,42 @@
// Copyright 2013 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.
//go:build windows
// +build windows
package vfs
import (
"io"
"golang.org/x/sys/windows"
)
// lockCloser hides all of an windows.Handle's methods, except for Close.
type lockCloser struct {
fd windows.Handle
}
func (l lockCloser) Close() error {
return windows.Close(l.fd)
}
// Lock locks the given file. On Windows, Locking will fail if the file is
// already open by the current process.
func (defaultFS) Lock(name string) (io.Closer, error) {
p, err := windows.UTF16PtrFromString(name)
if err != nil {
return nil, err
}
fd, err := windows.CreateFile(p,
windows.GENERIC_READ|windows.GENERIC_WRITE,
0, nil, windows.CREATE_ALWAYS,
windows.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, err
}
return lockCloser{fd: fd}, nil
}

166
vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go generated vendored Normal file
View File

@@ -0,0 +1,166 @@
// Copyright 2021 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 vfs
import (
"fmt"
"io"
"os"
)
// WithLogging wraps an FS and logs filesystem modification operations to the
// given logFn.
func WithLogging(fs FS, logFn LogFn) FS {
return &loggingFS{
FS: fs,
logFn: logFn,
}
}
// LogFn is a function that is used to capture a log when WithLogging is used.
type LogFn func(fmt string, args ...interface{})
type loggingFS struct {
FS
logFn LogFn
}
var _ FS = (*loggingFS)(nil)
func (fs *loggingFS) Create(name string) (File, error) {
fs.logFn("create: %s", name)
f, err := fs.FS.Create(name)
if err != nil {
return nil, err
}
return newLoggingFile(f, name, fs.logFn), nil
}
func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) {
var optsStr string
if len(opts) > 0 {
optsStr = " (options:"
for i := range opts {
optsStr += fmt.Sprintf(" %T", opts[i])
}
optsStr += ")"
}
fs.logFn("open: %s%s", name, optsStr)
f, err := fs.FS.Open(name, opts...)
if err != nil {
return nil, err
}
return newLoggingFile(f, name, fs.logFn), nil
}
func (fs *loggingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
fs.logFn("open-read-write: %s", name)
f, err := fs.FS.OpenReadWrite(name, opts...)
if err != nil {
return nil, err
}
return newLoggingFile(f, name, fs.logFn), nil
}
func (fs *loggingFS) Link(oldname, newname string) error {
fs.logFn("link: %s -> %s", oldname, newname)
return fs.FS.Link(oldname, newname)
}
func (fs *loggingFS) OpenDir(name string) (File, error) {
fs.logFn("open-dir: %s", name)
f, err := fs.FS.OpenDir(name)
if err != nil {
return nil, err
}
return newLoggingFile(f, name, fs.logFn), nil
}
func (fs *loggingFS) Rename(oldname, newname string) error {
fs.logFn("rename: %s -> %s", oldname, newname)
return fs.FS.Rename(oldname, newname)
}
func (fs *loggingFS) ReuseForWrite(oldname, newname string) (File, error) {
fs.logFn("reuseForWrite: %s -> %s", oldname, newname)
f, err := fs.FS.ReuseForWrite(oldname, newname)
if err != nil {
return nil, err
}
return newLoggingFile(f, newname, fs.logFn), nil
}
func (fs *loggingFS) MkdirAll(dir string, perm os.FileMode) error {
fs.logFn("mkdir-all: %s %#o", dir, perm)
return fs.FS.MkdirAll(dir, perm)
}
func (fs *loggingFS) Lock(name string) (io.Closer, error) {
fs.logFn("lock: %s", name)
return fs.FS.Lock(name)
}
func (fs loggingFS) Remove(name string) error {
fs.logFn("remove: %s", name)
err := fs.FS.Remove(name)
return err
}
func (fs loggingFS) RemoveAll(name string) error {
fs.logFn("remove-all: %s", name)
err := fs.FS.RemoveAll(name)
return err
}
type loggingFile struct {
File
name string
logFn LogFn
}
var _ File = (*loggingFile)(nil)
func newLoggingFile(f File, name string, logFn LogFn) *loggingFile {
return &loggingFile{
File: f,
name: name,
logFn: logFn,
}
}
func (f *loggingFile) Close() error {
f.logFn("close: %s", f.name)
return f.File.Close()
}
func (f *loggingFile) Sync() error {
f.logFn("sync: %s", f.name)
return f.File.Sync()
}
func (f *loggingFile) SyncData() error {
f.logFn("sync-data: %s", f.name)
return f.File.SyncData()
}
func (f *loggingFile) SyncTo(length int64) (fullSync bool, err error) {
f.logFn("sync-to(%d): %s", length, f.name)
return f.File.SyncTo(length)
}
func (f *loggingFile) ReadAt(p []byte, offset int64) (int, error) {
f.logFn("read-at(%d, %d): %s", offset, len(p), f.name)
return f.File.ReadAt(p, offset)
}
func (f *loggingFile) WriteAt(p []byte, offset int64) (int, error) {
f.logFn("write-at(%d, %d): %s", offset, len(p), f.name)
return f.File.WriteAt(p, offset)
}
func (f *loggingFile) Prefetch(offset int64, length int64) error {
f.logFn("prefetch(%d, %d): %s", offset, length, f.name)
return f.File.Prefetch(offset, length)
}

835
vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go generated vendored Normal file
View File

@@ -0,0 +1,835 @@
// Copyright 2012 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 vfs // import "github.com/cockroachdb/pebble/vfs"
import (
"bytes"
"fmt"
"io"
"os"
"path"
"sort"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
"github.com/cockroachdb/pebble/internal/invariants"
)
const sep = "/"
// NewMem returns a new memory-backed FS implementation.
func NewMem() *MemFS {
return &MemFS{
root: newRootMemNode(),
}
}
// NewStrictMem returns a "strict" memory-backed FS implementation. The behaviour is strict wrt
// needing a Sync() call on files or directories for the state changes to be finalized. Any
// changes that are not finalized are visible to reads until MemFS.ResetToSyncedState() is called,
// at which point they are discarded and no longer visible.
//
// Expected usage:
//
// strictFS := NewStrictMem()
// db := Open(..., &Options{FS: strictFS})
// // Do and commit various operations.
// ...
// // Prevent any more changes to finalized state.
// strictFS.SetIgnoreSyncs(true)
// // This will finish any ongoing background flushes, compactions but none of these writes will
// // be finalized since syncs are being ignored.
// db.Close()
// // Discard unsynced state.
// strictFS.ResetToSyncedState()
// // Allow changes to finalized state.
// strictFS.SetIgnoreSyncs(false)
// // Open the DB. This DB should have the same state as if the earlier strictFS operations and
// // db.Close() were not called.
// db := Open(..., &Options{FS: strictFS})
func NewStrictMem() *MemFS {
return &MemFS{
root: newRootMemNode(),
strict: true,
}
}
// NewMemFile returns a memory-backed File implementation. The memory-backed
// file takes ownership of data.
func NewMemFile(data []byte) File {
n := &memNode{}
n.refs.Store(1)
n.mu.data = data
n.mu.modTime = time.Now()
return &memFile{
n: n,
read: true,
}
}
// MemFS implements FS.
type MemFS struct {
mu sync.Mutex
root *memNode
// lockFiles holds a map of open file locks. Presence in this map indicates
// a file lock is currently held. Keys are strings holding the path of the
// locked file. The stored value is untyped and unused; only presence of
// the key within the map is significant.
lockedFiles sync.Map
strict bool
ignoreSyncs bool
// Windows has peculiar semantics with respect to hard links and deleting
// open files. In tests meant to exercise this behavior, this flag can be
// set to error if removing an open file.
windowsSemantics bool
}
var _ FS = &MemFS{}
// UseWindowsSemantics configures whether the MemFS implements Windows-style
// semantics, in particular with respect to whether any of an open file's links
// may be removed. Windows semantics default to off.
func (y *MemFS) UseWindowsSemantics(windowsSemantics bool) {
y.mu.Lock()
defer y.mu.Unlock()
y.windowsSemantics = windowsSemantics
}
// String dumps the contents of the MemFS.
func (y *MemFS) String() string {
y.mu.Lock()
defer y.mu.Unlock()
s := new(bytes.Buffer)
y.root.dump(s, 0)
return s.String()
}
// SetIgnoreSyncs sets the MemFS.ignoreSyncs field. See the usage comment with NewStrictMem() for
// details.
func (y *MemFS) SetIgnoreSyncs(ignoreSyncs bool) {
y.mu.Lock()
if !y.strict {
// noop
return
}
y.ignoreSyncs = ignoreSyncs
y.mu.Unlock()
}
// ResetToSyncedState discards state in the FS that is not synced. See the usage comment with
// NewStrictMem() for details.
func (y *MemFS) ResetToSyncedState() {
if !y.strict {
// noop
return
}
y.mu.Lock()
y.root.resetToSyncedState()
y.mu.Unlock()
}
// walk walks the directory tree for the fullname, calling f at each step. If
// f returns an error, the walk will be aborted and return that same error.
//
// Each walk is atomic: y's mutex is held for the entire operation, including
// all calls to f.
//
// dir is the directory at that step, frag is the name fragment, and final is
// whether it is the final step. For example, walking "/foo/bar/x" will result
// in 3 calls to f:
// - "/", "foo", false
// - "/foo/", "bar", false
// - "/foo/bar/", "x", true
//
// Similarly, walking "/y/z/", with a trailing slash, will result in 3 calls to f:
// - "/", "y", false
// - "/y/", "z", false
// - "/y/z/", "", true
func (y *MemFS) walk(fullname string, f func(dir *memNode, frag string, final bool) error) error {
y.mu.Lock()
defer y.mu.Unlock()
// For memfs, the current working directory is the same as the root directory,
// so we strip off any leading "/"s to make fullname a relative path, and
// the walk starts at y.root.
for len(fullname) > 0 && fullname[0] == sep[0] {
fullname = fullname[1:]
}
if fullname == "." {
fullname = ""
}
dir := y.root
for {
frag, remaining := fullname, ""
i := strings.IndexRune(fullname, rune(sep[0]))
final := i < 0
if !final {
frag, remaining = fullname[:i], fullname[i+1:]
for len(remaining) > 0 && remaining[0] == sep[0] {
remaining = remaining[1:]
}
}
if err := f(dir, frag, final); err != nil {
return err
}
if final {
break
}
child := dir.children[frag]
if child == nil {
return &os.PathError{
Op: "open",
Path: fullname,
Err: oserror.ErrNotExist,
}
}
if !child.isDir {
return &os.PathError{
Op: "open",
Path: fullname,
Err: errors.New("not a directory"),
}
}
dir, fullname = child, remaining
}
return nil
}
// Create implements FS.Create.
func (y *MemFS) Create(fullname string) (File, error) {
var ret *memFile
err := y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
n := &memNode{name: frag}
dir.children[frag] = n
ret = &memFile{
n: n,
fs: y,
read: true,
write: true,
}
}
return nil
})
if err != nil {
return nil, err
}
ret.n.refs.Add(1)
return ret, nil
}
// Link implements FS.Link.
func (y *MemFS) Link(oldname, newname string) error {
var n *memNode
err := y.walk(oldname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
n = dir.children[frag]
}
return nil
})
if err != nil {
return err
}
if n == nil {
return &os.LinkError{
Op: "link",
Old: oldname,
New: newname,
Err: oserror.ErrNotExist,
}
}
return y.walk(newname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
if _, ok := dir.children[frag]; ok {
return &os.LinkError{
Op: "link",
Old: oldname,
New: newname,
Err: oserror.ErrExist,
}
}
dir.children[frag] = n
}
return nil
})
}
func (y *MemFS) open(fullname string, openForWrite bool) (File, error) {
var ret *memFile
err := y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
ret = &memFile{
n: dir,
fs: y,
}
return nil
}
if n := dir.children[frag]; n != nil {
ret = &memFile{
n: n,
fs: y,
read: true,
write: openForWrite,
}
}
}
return nil
})
if err != nil {
return nil, err
}
if ret == nil {
return nil, &os.PathError{
Op: "open",
Path: fullname,
Err: oserror.ErrNotExist,
}
}
ret.n.refs.Add(1)
return ret, nil
}
// Open implements FS.Open.
func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) {
return y.open(fullname, false /* openForWrite */)
}
// OpenReadWrite implements FS.OpenReadWrite.
func (y *MemFS) OpenReadWrite(fullname string, opts ...OpenOption) (File, error) {
f, err := y.open(fullname, true /* openForWrite */)
pathErr, ok := err.(*os.PathError)
if ok && pathErr.Err == oserror.ErrNotExist {
return y.Create(fullname)
}
return f, err
}
// OpenDir implements FS.OpenDir.
func (y *MemFS) OpenDir(fullname string) (File, error) {
return y.open(fullname, false /* openForWrite */)
}
// Remove implements FS.Remove.
func (y *MemFS) Remove(fullname string) error {
return y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
child, ok := dir.children[frag]
if !ok {
return oserror.ErrNotExist
}
if y.windowsSemantics {
// Disallow removal of open files/directories which implements
// Windows semantics. This ensures that we don't regress in the
// ordering of operations and try to remove a file while it is
// still open.
if n := child.refs.Load(); n > 0 {
return oserror.ErrInvalid
}
}
if len(child.children) > 0 {
return errNotEmpty
}
delete(dir.children, frag)
}
return nil
})
}
// RemoveAll implements FS.RemoveAll.
func (y *MemFS) RemoveAll(fullname string) error {
err := y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
_, ok := dir.children[frag]
if !ok {
return nil
}
delete(dir.children, frag)
}
return nil
})
// Match os.RemoveAll which returns a nil error even if the parent
// directories don't exist.
if oserror.IsNotExist(err) {
err = nil
}
return err
}
// Rename implements FS.Rename.
func (y *MemFS) Rename(oldname, newname string) error {
var n *memNode
err := y.walk(oldname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
n = dir.children[frag]
delete(dir.children, frag)
}
return nil
})
if err != nil {
return err
}
if n == nil {
return &os.PathError{
Op: "open",
Path: oldname,
Err: oserror.ErrNotExist,
}
}
return y.walk(newname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
dir.children[frag] = n
n.name = frag
}
return nil
})
}
// ReuseForWrite implements FS.ReuseForWrite.
func (y *MemFS) ReuseForWrite(oldname, newname string) (File, error) {
if err := y.Rename(oldname, newname); err != nil {
return nil, err
}
f, err := y.Open(newname)
if err != nil {
return nil, err
}
y.mu.Lock()
defer y.mu.Unlock()
mf := f.(*memFile)
mf.read = false
mf.write = true
return f, nil
}
// MkdirAll implements FS.MkdirAll.
func (y *MemFS) MkdirAll(dirname string, perm os.FileMode) error {
return y.walk(dirname, func(dir *memNode, frag string, final bool) error {
if frag == "" {
if final {
return nil
}
return errors.New("pebble/vfs: empty file name")
}
child := dir.children[frag]
if child == nil {
dir.children[frag] = &memNode{
name: frag,
children: make(map[string]*memNode),
isDir: true,
}
return nil
}
if !child.isDir {
return &os.PathError{
Op: "open",
Path: dirname,
Err: errors.New("not a directory"),
}
}
return nil
})
}
// Lock implements FS.Lock.
func (y *MemFS) Lock(fullname string) (io.Closer, error) {
// FS.Lock excludes other processes, but other processes cannot see this
// process' memory. However some uses (eg, Cockroach tests) may open and
// close the same MemFS-backed database multiple times. We want mutual
// exclusion in this case too. See cockroachdb/cockroach#110645.
_, loaded := y.lockedFiles.Swap(fullname, nil /* the value itself is insignificant */)
if loaded {
// This file lock has already been acquired. On unix, this results in
// either EACCES or EAGAIN so we mimic.
return nil, syscall.EAGAIN
}
// Otherwise, we successfully acquired the lock. Locks are visible in the
// parent directory listing, and they also must be created under an existent
// directory. Create the path so that we have the normal detection of
// non-existent directory paths, and make the lock visible when listing
// directory entries.
f, err := y.Create(fullname)
if err != nil {
// "Release" the lock since we failed.
y.lockedFiles.Delete(fullname)
return nil, err
}
return &memFileLock{
y: y,
f: f,
fullname: fullname,
}, nil
}
// List implements FS.List.
func (y *MemFS) List(dirname string) ([]string, error) {
if !strings.HasSuffix(dirname, sep) {
dirname += sep
}
var ret []string
err := y.walk(dirname, func(dir *memNode, frag string, final bool) error {
if final {
if frag != "" {
panic("unreachable")
}
ret = make([]string, 0, len(dir.children))
for s := range dir.children {
ret = append(ret, s)
}
}
return nil
})
return ret, err
}
// Stat implements FS.Stat.
func (y *MemFS) Stat(name string) (os.FileInfo, error) {
f, err := y.Open(name)
if err != nil {
if pe, ok := err.(*os.PathError); ok {
pe.Op = "stat"
}
return nil, err
}
defer f.Close()
return f.Stat()
}
// PathBase implements FS.PathBase.
func (*MemFS) PathBase(p string) string {
// Note that MemFS uses forward slashes for its separator, hence the use of
// path.Base, not filepath.Base.
return path.Base(p)
}
// PathJoin implements FS.PathJoin.
func (*MemFS) PathJoin(elem ...string) string {
// Note that MemFS uses forward slashes for its separator, hence the use of
// path.Join, not filepath.Join.
return path.Join(elem...)
}
// PathDir implements FS.PathDir.
func (*MemFS) PathDir(p string) string {
// Note that MemFS uses forward slashes for its separator, hence the use of
// path.Dir, not filepath.Dir.
return path.Dir(p)
}
// GetDiskUsage implements FS.GetDiskUsage.
func (*MemFS) GetDiskUsage(string) (DiskUsage, error) {
return DiskUsage{}, ErrUnsupported
}
// memNode holds a file's data or a directory's children, and implements os.FileInfo.
type memNode struct {
name string
isDir bool
refs atomic.Int32
// Mutable state.
// - For a file: data, syncedDate, modTime: A file is only being mutated by a single goroutine,
// but there can be concurrent readers e.g. DB.Checkpoint() which can read WAL or MANIFEST
// files that are being written to. Additionally Sync() calls can be concurrent with writing.
// - For a directory: children and syncedChildren. Concurrent writes are possible, and
// these are protected using MemFS.mu.
mu struct {
sync.Mutex
data []byte
syncedData []byte
modTime time.Time
}
children map[string]*memNode
syncedChildren map[string]*memNode
}
func newRootMemNode() *memNode {
return &memNode{
name: "/", // set the name to match what file systems do
children: make(map[string]*memNode),
isDir: true,
}
}
func (f *memNode) IsDir() bool {
return f.isDir
}
func (f *memNode) ModTime() time.Time {
f.mu.Lock()
defer f.mu.Unlock()
return f.mu.modTime
}
func (f *memNode) Mode() os.FileMode {
if f.isDir {
return os.ModeDir | 0755
}
return 0755
}
func (f *memNode) Name() string {
return f.name
}
func (f *memNode) Size() int64 {
f.mu.Lock()
defer f.mu.Unlock()
return int64(len(f.mu.data))
}
func (f *memNode) Sys() interface{} {
return nil
}
func (f *memNode) dump(w *bytes.Buffer, level int) {
if f.isDir {
w.WriteString(" ")
} else {
f.mu.Lock()
fmt.Fprintf(w, "%8d ", len(f.mu.data))
f.mu.Unlock()
}
for i := 0; i < level; i++ {
w.WriteString(" ")
}
w.WriteString(f.name)
if !f.isDir {
w.WriteByte('\n')
return
}
if level > 0 { // deal with the fact that the root's name is already "/"
w.WriteByte(sep[0])
}
w.WriteByte('\n')
names := make([]string, 0, len(f.children))
for name := range f.children {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
f.children[name].dump(w, level+1)
}
}
func (f *memNode) resetToSyncedState() {
if f.isDir {
f.children = make(map[string]*memNode)
for k, v := range f.syncedChildren {
f.children[k] = v
}
for _, v := range f.children {
v.resetToSyncedState()
}
} else {
f.mu.Lock()
f.mu.data = append([]byte(nil), f.mu.syncedData...)
f.mu.Unlock()
}
}
// memFile is a reader or writer of a node's data, and implements File.
type memFile struct {
n *memNode
fs *MemFS // nil for a standalone memFile
rpos int
wpos int
read, write bool
}
var _ File = (*memFile)(nil)
func (f *memFile) Close() error {
if n := f.n.refs.Add(-1); n < 0 {
panic(fmt.Sprintf("pebble: close of unopened file: %d", n))
}
f.n = nil
return nil
}
func (f *memFile) Read(p []byte) (int, error) {
if !f.read {
return 0, errors.New("pebble/vfs: file was not opened for reading")
}
if f.n.isDir {
return 0, errors.New("pebble/vfs: cannot read a directory")
}
f.n.mu.Lock()
defer f.n.mu.Unlock()
if f.rpos >= len(f.n.mu.data) {
return 0, io.EOF
}
n := copy(p, f.n.mu.data[f.rpos:])
f.rpos += n
return n, nil
}
func (f *memFile) ReadAt(p []byte, off int64) (int, error) {
if !f.read {
return 0, errors.New("pebble/vfs: file was not opened for reading")
}
if f.n.isDir {
return 0, errors.New("pebble/vfs: cannot read a directory")
}
f.n.mu.Lock()
defer f.n.mu.Unlock()
if off >= int64(len(f.n.mu.data)) {
return 0, io.EOF
}
n := copy(p, f.n.mu.data[off:])
if n < len(p) {
return n, io.EOF
}
return n, nil
}
func (f *memFile) Write(p []byte) (int, error) {
if !f.write {
return 0, errors.New("pebble/vfs: file was not created for writing")
}
if f.n.isDir {
return 0, errors.New("pebble/vfs: cannot write a directory")
}
f.n.mu.Lock()
defer f.n.mu.Unlock()
f.n.mu.modTime = time.Now()
if f.wpos+len(p) <= len(f.n.mu.data) {
n := copy(f.n.mu.data[f.wpos:f.wpos+len(p)], p)
if n != len(p) {
panic("stuff")
}
} else {
f.n.mu.data = append(f.n.mu.data[:f.wpos], p...)
}
f.wpos += len(p)
if invariants.Enabled {
// Mutate the input buffer to flush out bugs in Pebble which expect the
// input buffer to be unmodified.
for i := range p {
p[i] ^= 0xff
}
}
return len(p), nil
}
func (f *memFile) WriteAt(p []byte, ofs int64) (int, error) {
if !f.write {
return 0, errors.New("pebble/vfs: file was not created for writing")
}
if f.n.isDir {
return 0, errors.New("pebble/vfs: cannot write a directory")
}
f.n.mu.Lock()
defer f.n.mu.Unlock()
f.n.mu.modTime = time.Now()
for len(f.n.mu.data) < int(ofs)+len(p) {
f.n.mu.data = append(f.n.mu.data, 0)
}
n := copy(f.n.mu.data[int(ofs):int(ofs)+len(p)], p)
if n != len(p) {
panic("stuff")
}
return len(p), nil
}
func (f *memFile) Prefetch(offset int64, length int64) error { return nil }
func (f *memFile) Preallocate(offset, length int64) error { return nil }
func (f *memFile) Stat() (os.FileInfo, error) {
return f.n, nil
}
func (f *memFile) Sync() error {
if f.fs != nil && f.fs.strict {
f.fs.mu.Lock()
defer f.fs.mu.Unlock()
if f.fs.ignoreSyncs {
return nil
}
if f.n.isDir {
f.n.syncedChildren = make(map[string]*memNode)
for k, v := range f.n.children {
f.n.syncedChildren[k] = v
}
} else {
f.n.mu.Lock()
f.n.mu.syncedData = append([]byte(nil), f.n.mu.data...)
f.n.mu.Unlock()
}
}
return nil
}
func (f *memFile) SyncData() error {
return f.Sync()
}
func (f *memFile) SyncTo(length int64) (fullSync bool, err error) {
// NB: This SyncTo implementation lies, with its return values claiming it
// synced the data up to `length`. When fullSync=false, SyncTo provides no
// durability guarantees, so this can help surface bugs where we improperly
// rely on SyncTo providing durability.
return false, nil
}
func (f *memFile) Fd() uintptr {
return InvalidFd
}
// Flush is a no-op and present only to prevent buffering at higher levels
// (e.g. it prevents sstable.Writer from using a bufio.Writer).
func (f *memFile) Flush() error {
return nil
}
type memFileLock struct {
y *MemFS
f File
fullname string
}
func (l *memFileLock) Close() error {
if l.y == nil {
return nil
}
l.y.lockedFiles.Delete(l.fullname)
l.y = nil
return l.f.Close()
}

View File

@@ -0,0 +1,215 @@
// Copyright 2019 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 vfs
import (
"sync/atomic"
"github.com/cockroachdb/errors"
)
// SyncingFileOptions holds the options for a syncingFile.
type SyncingFileOptions struct {
// NoSyncOnClose elides the automatic Sync during Close if it's not possible
// to sync the remainder of the file in a non-blocking way.
NoSyncOnClose bool
BytesPerSync int
PreallocateSize int
}
type syncingFile struct {
File
// fd can be InvalidFd if the underlying File does not support it.
fd uintptr
noSyncOnClose bool
bytesPerSync int64
preallocateSize int64
// The offset at which dirty data has been written.
offset atomic.Int64
// The offset at which data has been synced. Note that if SyncFileRange is
// being used, the periodic syncing of data during writing will only ever
// sync up to offset-1MB. This is done to avoid rewriting the tail of the
// file multiple times, but has the side effect of ensuring that Close will
// sync the file's metadata.
syncOffset atomic.Int64
preallocatedBlocks int64
}
// NewSyncingFile wraps a writable file and ensures that data is synced
// periodically as it is written. The syncing does not provide persistency
// guarantees for these periodic syncs, but is used to avoid latency spikes if
// the OS automatically decides to write out a large chunk of dirty filesystem
// buffers. The underlying file is fully synced upon close.
func NewSyncingFile(f File, opts SyncingFileOptions) File {
s := &syncingFile{
File: f,
fd: f.Fd(),
noSyncOnClose: bool(opts.NoSyncOnClose),
bytesPerSync: int64(opts.BytesPerSync),
preallocateSize: int64(opts.PreallocateSize),
}
// Ensure a file that is opened and then closed will be synced, even if no
// data has been written to it.
s.syncOffset.Store(-1)
return s
}
// NB: syncingFile.Write is unsafe for concurrent use!
func (f *syncingFile) Write(p []byte) (n int, err error) {
_ = f.preallocate(f.offset.Load())
n, err = f.File.Write(p)
if err != nil {
return n, errors.WithStack(err)
}
// The offset is updated atomically so that it can be accessed safely from
// Sync.
f.offset.Add(int64(n))
if err := f.maybeSync(); err != nil {
return 0, err
}
return n, nil
}
func (f *syncingFile) preallocate(offset int64) error {
if f.fd == InvalidFd || f.preallocateSize == 0 {
return nil
}
newPreallocatedBlocks := (offset + f.preallocateSize - 1) / f.preallocateSize
if newPreallocatedBlocks <= f.preallocatedBlocks {
return nil
}
length := f.preallocateSize * (newPreallocatedBlocks - f.preallocatedBlocks)
offset = f.preallocateSize * f.preallocatedBlocks
f.preallocatedBlocks = newPreallocatedBlocks
return f.Preallocate(offset, length)
}
func (f *syncingFile) ratchetSyncOffset(offset int64) {
for {
syncOffset := f.syncOffset.Load()
if syncOffset >= offset {
return
}
if f.syncOffset.CompareAndSwap(syncOffset, offset) {
return
}
}
}
func (f *syncingFile) Sync() error {
// We update syncOffset (atomically) in order to avoid spurious syncs in
// maybeSync. Note that even if syncOffset is larger than the current file
// offset, we still need to call the underlying file's sync for persistence
// guarantees which are not provided by SyncTo (or by sync_file_range on
// Linux).
f.ratchetSyncOffset(f.offset.Load())
return f.SyncData()
}
func (f *syncingFile) maybeSync() error {
if f.bytesPerSync <= 0 {
return nil
}
// From the RocksDB source:
//
// We try to avoid sync to the last 1MB of data. For two reasons:
// (1) avoid rewrite the same page that is modified later.
// (2) for older version of OS, write can block while writing out
// the page.
// Xfs does neighbor page flushing outside of the specified ranges. We
// need to make sure sync range is far from the write offset.
const syncRangeBuffer = 1 << 20 // 1 MB
offset := f.offset.Load()
if offset <= syncRangeBuffer {
return nil
}
const syncRangeAlignment = 4 << 10 // 4 KB
syncToOffset := offset - syncRangeBuffer
syncToOffset -= syncToOffset % syncRangeAlignment
syncOffset := f.syncOffset.Load()
if syncToOffset < 0 || (syncToOffset-syncOffset) < f.bytesPerSync {
return nil
}
if f.fd == InvalidFd {
return errors.WithStack(f.Sync())
}
// Note that SyncTo will always be called with an offset < atomic.offset.
// The SyncTo implementation may choose to sync the entire file (i.e. on
// OSes which do not support syncing a portion of the file).
fullSync, err := f.SyncTo(syncToOffset)
if err != nil {
return errors.WithStack(err)
}
if fullSync {
f.ratchetSyncOffset(offset)
} else {
f.ratchetSyncOffset(syncToOffset)
}
return nil
}
func (f *syncingFile) Close() error {
// Sync any data that has been written but not yet synced unless the file
// has noSyncOnClose option explicitly set.
//
// NB: If the file is capable of non-durability-guarantee SyncTos, and the
// caller has not called Sync since the last write, syncOffset is guaranteed
// to be less than atomic.offset. This ensures we fall into the below
// conditional and perform a full sync to durably persist the file.
if off := f.offset.Load(); off > f.syncOffset.Load() {
// There's still remaining dirty data.
if f.noSyncOnClose {
// If NoSyncOnClose is set, only perform a SyncTo. On linux, SyncTo
// translates to a non-blocking `sync_file_range` call which
// provides no persistence guarantee. Since it's non-blocking,
// there's no latency hit of a blocking sync call, but we still
// ensure we're not allowing significant dirty data to accumulate.
if _, err := f.File.SyncTo(off); err != nil {
return err
}
f.ratchetSyncOffset(off)
} else if err := f.Sync(); err != nil {
return errors.WithStack(err)
}
}
return errors.WithStack(f.File.Close())
}
// NewSyncingFS wraps a vfs.FS with one that wraps newly created files with
// vfs.NewSyncingFile.
func NewSyncingFS(fs FS, syncOpts SyncingFileOptions) FS {
return &syncingFS{
FS: fs,
syncOpts: syncOpts,
}
}
type syncingFS struct {
FS
syncOpts SyncingFileOptions
}
var _ FS = (*syncingFS)(nil)
func (fs *syncingFS) Create(name string) (File, error) {
f, err := fs.FS.Create(name)
if err != nil {
return nil, err
}
return NewSyncingFile(f, fs.syncOpts), nil
}
func (fs *syncingFS) ReuseForWrite(oldname, newname string) (File, error) {
// TODO(radu): implement this if needed.
panic("unimplemented")
}

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

@@ -0,0 +1,419 @@
// Copyright 2012 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 vfs
import (
"io"
"os"
"path/filepath"
"syscall"
"github.com/cockroachdb/errors"
"github.com/cockroachdb/errors/oserror"
)
// File is a readable, writable sequence of bytes.
//
// Typically, it will be an *os.File, but test code may choose to substitute
// memory-backed implementations.
//
// Write-oriented operations (Write, Sync) must be called sequentially: At most
// 1 call to Write or Sync may be executed at any given time.
type File interface {
io.Closer
io.Reader
io.ReaderAt
// Unlike the specification for io.Writer.Write(), the vfs.File.Write()
// method *is* allowed to modify the slice passed in, whether temporarily
// or permanently. Callers of Write() need to take this into account.
io.Writer
// WriteAt() is only supported for files that were opened with FS.OpenReadWrite.
io.WriterAt
// Preallocate optionally preallocates storage for `length` at `offset`
// within the file. Implementations may choose to do nothing.
Preallocate(offset, length int64) error
Stat() (os.FileInfo, error)
Sync() error
// SyncTo requests that a prefix of the file's data be synced to stable
// storage. The caller passes provides a `length`, indicating how many bytes
// to sync from the beginning of the file. SyncTo is a no-op for
// directories, and therefore always returns false.
//
// SyncTo returns a fullSync return value, indicating one of two possible
// outcomes.
//
// If fullSync is false, the first `length` bytes of the file was queued to
// be synced to stable storage. The syncing of the file prefix may happen
// asynchronously. No persistence guarantee is provided.
//
// If fullSync is true, the entirety of the file's contents were
// synchronously synced to stable storage, and a persistence guarantee is
// provided. In this outcome, any modified metadata for the file is not
// guaranteed to be synced unless that metadata is needed in order to allow
// a subsequent data retrieval to be correctly handled.
SyncTo(length int64) (fullSync bool, err error)
// SyncData requires that all written data be persisted. File metadata is
// not required to be synced. Unsophisticated implementations may call Sync.
SyncData() error
// Prefetch signals the OS (on supported platforms) to fetch the next length
// bytes in file (as returned by os.File.Fd()) after offset into cache. Any
// subsequent reads in that range will not issue disk IO.
Prefetch(offset int64, length int64) error
// Fd returns the raw file descriptor when a File is backed by an *os.File.
// It can be used for specific functionality like Prefetch.
// Returns InvalidFd if not supported.
Fd() uintptr
}
// InvalidFd is a special value returned by File.Fd() when the file is not
// backed by an OS descriptor.
// Note: the special value is consistent with what os.File implementation
// returns on a nil receiver.
const InvalidFd uintptr = ^(uintptr(0))
// OpenOption provide an interface to do work on file handles in the Open()
// call.
type OpenOption interface {
// Apply is called on the file handle after it's opened.
Apply(File)
}
// FS is a namespace for files.
//
// The names are filepath names: they may be / separated or \ separated,
// depending on the underlying operating system.
type FS interface {
// Create creates the named file for reading and writing. If a file
// already exists at the provided name, it's removed first ensuring the
// resulting file descriptor points to a new inode.
Create(name string) (File, error)
// Link creates newname as a hard link to the oldname file.
Link(oldname, newname string) error
// Open opens the named file for reading. openOptions provides
Open(name string, opts ...OpenOption) (File, error)
// OpenReadWrite opens the named file for reading and writing. If the file
// does not exist, it is created.
OpenReadWrite(name string, opts ...OpenOption) (File, error)
// OpenDir opens the named directory for syncing.
OpenDir(name string) (File, error)
// Remove removes the named file or directory.
Remove(name string) error
// Remove removes the named file or directory and any children it
// contains. It removes everything it can but returns the first error it
// encounters.
RemoveAll(name string) error
// Rename renames a file. It overwrites the file at newname if one exists,
// the same as os.Rename.
Rename(oldname, newname string) error
// ReuseForWrite attempts to reuse the file with oldname by renaming it to newname and opening
// it for writing without truncation. It is acceptable for the implementation to choose not
// to reuse oldname, and simply create the file with newname -- in this case the implementation
// should delete oldname. If the caller calls this function with an oldname that does not exist,
// the implementation may return an error.
ReuseForWrite(oldname, newname string) (File, error)
// MkdirAll creates a directory and all necessary parents. The permission
// bits perm have the same semantics as in os.MkdirAll. If the directory
// already exists, MkdirAll does nothing and returns nil.
MkdirAll(dir string, perm os.FileMode) error
// Lock locks the given file, creating the file if necessary, and
// truncating the file if it already exists. The lock is an exclusive lock
// (a write lock), but locked files should neither be read from nor written
// to. Such files should have zero size and only exist to co-ordinate
// ownership across processes.
//
// A nil Closer is returned if an error occurred. Otherwise, close that
// Closer to release the lock.
//
// On Linux and OSX, a lock has the same semantics as fcntl(2)'s advisory
// locks. In particular, closing any other file descriptor for the same
// file will release the lock prematurely.
//
// Attempting to lock a file that is already locked by the current process
// returns an error and leaves the existing lock untouched.
//
// Lock is not yet implemented on other operating systems, and calling it
// will return an error.
Lock(name string) (io.Closer, error)
// List returns a listing of the given directory. The names returned are
// relative to dir.
List(dir string) ([]string, error)
// Stat returns an os.FileInfo describing the named file.
Stat(name string) (os.FileInfo, error)
// PathBase returns the last element of path. Trailing path separators are
// removed before extracting the last element. If the path is empty, PathBase
// returns ".". If the path consists entirely of separators, PathBase returns a
// single separator.
PathBase(path string) string
// PathJoin joins any number of path elements into a single path, adding a
// separator if necessary.
PathJoin(elem ...string) string
// PathDir returns all but the last element of path, typically the path's directory.
PathDir(path string) string
// GetDiskUsage returns disk space statistics for the filesystem where
// path is any file or directory within that filesystem.
GetDiskUsage(path string) (DiskUsage, error)
}
// DiskUsage summarizes disk space usage on a filesystem.
type DiskUsage struct {
// Total disk space available to the current process in bytes.
AvailBytes uint64
// Total disk space in bytes.
TotalBytes uint64
// Used disk space in bytes.
UsedBytes uint64
}
// Default is a FS implementation backed by the underlying operating system's
// file system.
var Default FS = defaultFS{}
type defaultFS struct{}
// wrapOSFile takes a standard library OS file and returns a vfs.File. f may be
// nil, in which case wrapOSFile must not panic. In such cases, it's okay if the
// returned vfs.File may panic if used.
func wrapOSFile(f *os.File) File {
// See the implementations in default_{linux,unix,windows}.go.
return wrapOSFileImpl(f)
}
func (defaultFS) Create(name string) (File, error) {
const openFlags = os.O_RDWR | os.O_CREATE | os.O_EXCL | syscall.O_CLOEXEC
osFile, err := os.OpenFile(name, openFlags, 0666)
// If the file already exists, remove it and try again.
//
// NB: We choose to remove the file instead of truncating it, despite the
// fact that we can't do so atomically, because it's more resistant to
// misuse when using hard links.
// We must loop in case another goroutine/thread/process is also
// attempting to create the a file at the same path.
for oserror.IsExist(err) {
if removeErr := os.Remove(name); removeErr != nil && !oserror.IsNotExist(removeErr) {
return wrapOSFile(osFile), errors.WithStack(removeErr)
}
osFile, err = os.OpenFile(name, openFlags, 0666)
}
return wrapOSFile(osFile), errors.WithStack(err)
}
func (defaultFS) Link(oldname, newname string) error {
return errors.WithStack(os.Link(oldname, newname))
}
func (defaultFS) Open(name string, opts ...OpenOption) (File, error) {
osFile, err := os.OpenFile(name, os.O_RDONLY|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, errors.WithStack(err)
}
file := wrapOSFile(osFile)
for _, opt := range opts {
opt.Apply(file)
}
return file, nil
}
func (defaultFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) {
osFile, err := os.OpenFile(name, os.O_RDWR|syscall.O_CLOEXEC|os.O_CREATE, 0666)
if err != nil {
return nil, errors.WithStack(err)
}
file := wrapOSFile(osFile)
for _, opt := range opts {
opt.Apply(file)
}
return file, nil
}
func (defaultFS) Remove(name string) error {
return errors.WithStack(os.Remove(name))
}
func (defaultFS) RemoveAll(name string) error {
return errors.WithStack(os.RemoveAll(name))
}
func (defaultFS) Rename(oldname, newname string) error {
return errors.WithStack(os.Rename(oldname, newname))
}
func (fs defaultFS) ReuseForWrite(oldname, newname string) (File, error) {
if err := fs.Rename(oldname, newname); err != nil {
return nil, errors.WithStack(err)
}
f, err := os.OpenFile(newname, os.O_RDWR|os.O_CREATE|syscall.O_CLOEXEC, 0666)
return wrapOSFile(f), errors.WithStack(err)
}
func (defaultFS) MkdirAll(dir string, perm os.FileMode) error {
return errors.WithStack(os.MkdirAll(dir, perm))
}
func (defaultFS) List(dir string) ([]string, error) {
f, err := os.Open(dir)
if err != nil {
return nil, err
}
defer f.Close()
dirnames, err := f.Readdirnames(-1)
return dirnames, errors.WithStack(err)
}
func (defaultFS) Stat(name string) (os.FileInfo, error) {
finfo, err := os.Stat(name)
return finfo, errors.WithStack(err)
}
func (defaultFS) PathBase(path string) string {
return filepath.Base(path)
}
func (defaultFS) PathJoin(elem ...string) string {
return filepath.Join(elem...)
}
func (defaultFS) PathDir(path string) string {
return filepath.Dir(path)
}
type randomReadsOption struct{}
// RandomReadsOption is an OpenOption that optimizes opened file handle for
// random reads, by calling fadvise() with POSIX_FADV_RANDOM on Linux systems
// to disable readahead.
var RandomReadsOption OpenOption = &randomReadsOption{}
// Apply implements the OpenOption interface.
func (randomReadsOption) Apply(f File) {
if fd := f.Fd(); fd != InvalidFd {
_ = fadviseRandom(fd)
}
}
type sequentialReadsOption struct{}
// SequentialReadsOption is an OpenOption that optimizes opened file handle for
// sequential reads, by calling fadvise() with POSIX_FADV_SEQUENTIAL on Linux
// systems to enable readahead.
var SequentialReadsOption OpenOption = &sequentialReadsOption{}
// Apply implements the OpenOption interface.
func (sequentialReadsOption) Apply(f File) {
if fd := f.Fd(); fd != InvalidFd {
_ = fadviseSequential(fd)
}
}
// Copy copies the contents of oldname to newname. If newname exists, it will
// be overwritten.
func Copy(fs FS, oldname, newname string) error {
return CopyAcrossFS(fs, oldname, fs, newname)
}
// CopyAcrossFS copies the contents of oldname on srcFS to newname dstFS. If
// newname exists, it will be overwritten.
func CopyAcrossFS(srcFS FS, oldname string, dstFS FS, newname string) error {
src, err := srcFS.Open(oldname, SequentialReadsOption)
if err != nil {
return err
}
defer src.Close()
dst, err := dstFS.Create(newname)
if err != nil {
return err
}
defer dst.Close()
if _, err := io.Copy(dst, src); err != nil {
return err
}
return dst.Sync()
}
// LimitedCopy copies up to maxBytes from oldname to newname. If newname
// exists, it will be overwritten.
func LimitedCopy(fs FS, oldname, newname string, maxBytes int64) error {
src, err := fs.Open(oldname, SequentialReadsOption)
if err != nil {
return err
}
defer src.Close()
dst, err := fs.Create(newname)
if err != nil {
return err
}
defer dst.Close()
if _, err := io.Copy(dst, &io.LimitedReader{R: src, N: maxBytes}); err != nil {
return err
}
return dst.Sync()
}
// LinkOrCopy creates newname as a hard link to the oldname file. If creating
// the hard link fails, LinkOrCopy falls back to copying the file (which may
// also fail if oldname doesn't exist or newname already exists).
func LinkOrCopy(fs FS, oldname, newname string) error {
err := fs.Link(oldname, newname)
if err == nil {
return nil
}
// Permit a handful of errors which we know won't be fixed by copying the
// file. Note that we don't check for the specifics of the error code as it
// isn't easy to do so in a portable manner. On Unix we'd have to check for
// LinkError.Err == syscall.EXDEV. On Windows we'd have to check for
// ERROR_NOT_SAME_DEVICE, ERROR_INVALID_FUNCTION, and
// ERROR_INVALID_PARAMETER. Rather that such OS specific checks, we fall back
// to always trying to copy if hard-linking failed.
if oserror.IsExist(err) || oserror.IsNotExist(err) || oserror.IsPermission(err) {
return err
}
return Copy(fs, oldname, newname)
}
// Root returns the base FS implementation, unwrapping all nested FSs that
// expose an Unwrap method.
func Root(fs FS) FS {
type unwrapper interface {
Unwrap() FS
}
for {
u, ok := fs.(unwrapper)
if !ok {
break
}
fs = u.Unwrap()
}
return fs
}
// ErrUnsupported may be returned a FS when it does not support an operation.
var ErrUnsupported = errors.New("pebble: not supported")