0

Initial commit

This commit is contained in:
eleksir
2022-09-04 02:34:54 +03:00
commit d9de157ad8
1017 changed files with 407553 additions and 0 deletions

View File

@@ -0,0 +1,238 @@
// 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
// 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 string
iter uint64
value string
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 marking a single file on the
// filesystem. The marker may be atomically moved from name to name.
// Marker is not safe for concurrent use. Multiple processes may not
// read or move the same marker simultaneously. An 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 files discovered by LocateMarker
// that are old values for this marker. These files may exist if the
// filesystem doesn't guarantee atomic renames (eg, if it's
// implemented as a link(newpath), remove(oldpath), and a crash in
// between may leave an entry at the old path).
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 mark the provided filename.
// 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.
//
// The provided filename does not need to exist on the filesystem.
func (a *Marker) Move(filename string) error {
a.iter++
dstFilename := markerFilename(a.name, a.iter, filename)
dstPath := a.fs.PathJoin(a.dir, dstFilename)
oldFilename := a.filename
// The marker has never been placed. Create a new file.
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 filenames 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 {
obsolete := a.obsoleteFiles
for _, filename := range obsolete {
if err := a.fs.Remove(a.fs.PathJoin(a.dir, filename)); err != nil && !oserror.IsNotExist(err) {
return err
}
a.obsoleteFiles = obsolete[1:]
}
a.obsoleteFiles = nil
return nil
}

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

@@ -0,0 +1,121 @@
// 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/ioutil"
"sort"
"github.com/cockroachdb/errors/oserror"
)
type cloneOpts struct {
skip func(string) bool
sync 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 }
// 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
}
stat, err := srcFile.Stat()
if err != nil {
return false, err
}
if stat.IsDir() {
if err := srcFile.Close(); err != nil {
return false, err
}
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
}
data, err := ioutil.ReadAll(srcFile)
if err != nil {
return false, err
}
if err := srcFile.Close(); 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
}

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

@@ -0,0 +1,20 @@
// 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 (
"os"
"syscall"
"github.com/cockroachdb/errors"
)
func (defaultFS) OpenDir(name string) (File, error) {
f, err := os.OpenFile(name, syscall.O_CLOEXEC, 0)
return f, errors.WithStack(err)
}

View File

@@ -0,0 +1,33 @@
// 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 windows
// +build windows
package vfs
import (
"os"
"syscall"
"github.com/cockroachdb/errors"
)
type windowsDir struct {
File
}
func (windowsDir) Sync() error {
// Silently ignore Sync() on Windows. This is the same behavior as
// RocksDB. See port/win/io_win.cc:WinDirectory::Fsync().
return nil
}
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
}

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

@@ -0,0 +1,409 @@
// 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
atomic struct {
// 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 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 := atomic.LoadUint32(&fs.atomic.generation)
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 = atomic.LoadUint32(&fs.atomic.generation)
for gen%2 == 1 {
fs.mu.Wait()
gen = atomic.LoadUint32(&fs.atomic.generation)
}
return gen
}
func (fs *enospcFS) handleENOSPC(gen uint32) {
fs.mu.Lock()
defer fs.mu.Unlock()
currentGeneration := atomic.LoadUint32(&fs.atomic.generation)
// 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.
atomic.StoreUint32(&fs.atomic.generation, 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`.
atomic.StoreUint32(&fs.atomic.generation, 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 = atomic.LoadUint32(&fs.atomic.generation)
}
}
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 = WithFd(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 = WithFd(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 = WithFd(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 = WithFd(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
}
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) 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
}
// Ensure that *enospcFS implements the FS interface.
var _ FS = (*enospcFS)(nil)
func isENOSPC(err error) bool {
err = errors.UnwrapAll(err)
e, ok := err.(syscall.Errno)
return ok && e == syscall.ENOSPC
}

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

@@ -0,0 +1,495 @@
// 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 (
"io"
"os"
"sync"
"sync/atomic"
"time"
)
const (
// defaultTickInterval is the default interval between two ticks of each
// diskHealthCheckingFile loop iteration.
defaultTickInterval = 2 * time.Second
// 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
)
// 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
onSlowDisk func(time.Duration)
diskSlowThreshold time.Duration
tickInterval time.Duration
stopper chan struct{}
lastWriteNanos int64
}
// newDiskHealthCheckingFile instantiates a new diskHealthCheckingFile, with the
// specified time threshold and event listener.
func newDiskHealthCheckingFile(
file File, diskSlowThreshold time.Duration, onSlowDisk func(time.Duration),
) *diskHealthCheckingFile {
return &diskHealthCheckingFile{
File: file,
onSlowDisk: onSlowDisk,
diskSlowThreshold: diskSlowThreshold,
tickInterval: defaultTickInterval,
stopper: make(chan struct{}),
}
}
// 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:
lastWriteNanos := atomic.LoadInt64(&d.lastWriteNanos)
if lastWriteNanos == 0 {
continue
}
lastWrite := time.Unix(0, lastWriteNanos)
now := time.Now()
if lastWrite.Add(d.diskSlowThreshold).Before(now) {
// diskSlowThreshold was exceeded. Call the passed-in
// listener.
d.onSlowDisk(now.Sub(lastWrite))
}
}
}
}()
}
// stopTicker stops the goroutine started in startTicker.
func (d *diskHealthCheckingFile) stopTicker() {
close(d.stopper)
}
// Write implements the io.Writer interface.
func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) {
d.timeDiskOp(func() {
n, err = d.File.Write(p)
})
return n, err
}
// Close implements the io.Closer interface.
func (d *diskHealthCheckingFile) Close() error {
d.stopTicker()
return d.File.Close()
}
// Sync implements the io.Syncer interface.
func (d *diskHealthCheckingFile) Sync() (err error) {
d.timeDiskOp(func() {
err = d.File.Sync()
})
return 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.
func (d *diskHealthCheckingFile) timeDiskOp(op func()) {
if d == nil {
op()
return
}
atomic.StoreInt64(&d.lastWriteNanos, time.Now().UnixNano())
defer func() {
atomic.StoreInt64(&d.lastWriteNanos, 0)
}()
op()
}
// 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(string, time.Duration)
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
startNanos 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(string, time.Duration),
) (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, 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 atomic.LoadInt64(&d.mu.inflight[i].startNanos) == 0 {
// This slot is not in use. Claim it.
s = d.mu.inflight[i]
s.name = name
atomic.StoreInt64(&s.startNanos, 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,
startNanos: startNanos,
}
d.mu.inflight = append(d.mu.inflight, s)
}
}()
op()
// Signal completion by zeroing the start time.
atomic.StoreInt64(&s.startNanos, 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()
for {
select {
case <-ticker.C:
// Scan the inflight slots for any slots recording a start
// time older than the diskSlowThreshold.
d.mu.Lock()
now := time.Now()
for i := range d.mu.inflight {
nanos := atomic.LoadInt64(&d.mu.inflight[i].startNanos)
if nanos != 0 && time.Unix(0, nanos).Add(d.diskSlowThreshold).Before(now) {
// diskSlowThreshold was exceeded. Invoke the provided
// callback.
d.onSlowDisk(d.mu.inflight[i].name, now.Sub(time.Unix(0, nanos)))
}
}
d.mu.Unlock()
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, 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(duration time.Duration) {
d.onSlowDisk(name, duration)
})
checkingFile.startTicker()
return WithFd(f, 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, 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, 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...)
}
// 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.
checkingFile := newDiskHealthCheckingFile(f, d.diskSlowThreshold, func(duration time.Duration) {
d.onSlowDisk(name, duration)
})
checkingFile.startTicker()
return WithFd(f, checkingFile), 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, 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, 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, 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, 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(duration time.Duration) {
d.onSlowDisk(newname, duration)
})
checkingFile.startTicker()
return WithFd(f, 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.Bsize) * uint64(stat.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 || openbsd || dragonfly || freebsd
// +build darwin openbsd 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,23 @@
// 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
// +build darwin dragonfly freebsd linux openbsd
package vfs
import (
"syscall"
"github.com/cockroachdb/errors"
"golang.org/x/sys/unix"
)
var errNotEmpty = syscall.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)
}

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

@@ -0,0 +1,42 @@
// 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
// fdGetter is an interface for a file with an Fd() method. A lot of
// File related optimizations (eg. Prefetch(), WAL recycling) rely on the
// existence of the Fd method to return a raw file descriptor.
type fdGetter interface {
Fd() uintptr
}
// fdFileWrapper is a File wrapper that also exposes an Fd() method. Used to
// wrap outer (wrapped) Files that could unintentionally hide the Fd() method
// exposed by the inner (unwrapped) File. It effectively lets the Fd() method
// bypass the outer File and go to the inner File.
type fdFileWrapper struct {
File
// All methods usually pass through to File above, except for Fd(), which
// bypasses it and gets called directly on the inner file.
inner fdGetter
}
func (f *fdFileWrapper) Fd() uintptr {
return f.inner.Fd()
}
// WithFd takes an inner (unwrapped) and an outer (wrapped) vfs.File,
// and returns an fdFileWrapper if the inner file has an Fd() method. Use this
// method to fix the hiding of the Fd() method and the subsequent unintentional
// disabling of Fd-related file optimizations.
func WithFd(inner, outer File) File {
if f, ok := inner.(fdGetter); ok {
return &fdFileWrapper{
File: outer,
inner: f,
}
}
return outer
}

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"
"syscall"
"github.com/cockroachdb/errors"
)
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 := syscall.Flock_t{
Type: syscall.F_WRLCK,
Whence: io.SeekStart,
Start: 0,
Len: 0, // 0 means to lock the entire file.
Pid: int32(os.Getpid()),
}
if err := syscall.FcntlFlock(f.Fd(), syscall.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,41 @@
// 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"
"syscall"
)
// lockCloser hides all of an syscall.Handle's methods, except for Close.
type lockCloser struct {
fd syscall.Handle
}
func (l lockCloser) Close() error {
return syscall.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 := syscall.UTF16PtrFromString(name)
if err != nil {
return nil, err
}
fd, err := syscall.CreateFile(p,
syscall.GENERIC_READ|syscall.GENERIC_WRITE,
0, nil, syscall.CREATE_ALWAYS,
syscall.FILE_ATTRIBUTE_NORMAL,
0,
)
if err != nil {
return nil, err
}
return lockCloser{fd: fd}, nil
}

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

@@ -0,0 +1,710 @@
// 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"
"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{refs: 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
strict bool
ignoreSyncs bool
}
var _ FS = &MemFS{}
// 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:]
}
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,
write: true,
}
}
return nil
})
if err != nil {
return nil, err
}
atomic.AddInt32(&ret.n.refs, 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) (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,
}
}
}
return nil
})
if err != nil {
return nil, err
}
if ret == nil {
return nil, &os.PathError{
Op: "open",
Path: fullname,
Err: oserror.ErrNotExist,
}
}
atomic.AddInt32(&ret.n.refs, 1)
return ret, nil
}
// Open implements FS.Open.
func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) {
return y.open(fullname)
}
// OpenDir implements FS.OpenDir.
func (y *MemFS) OpenDir(fullname string) (File, error) {
return y.open(fullname)
}
// 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
}
// 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 := atomic.LoadInt32(&child.refs); 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. We translate Lock into Create so that have the normal
// detection of non-existent directory paths.
return y.Create(fullname)
}
// 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 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
}
func (f *memFile) Close() error {
if n := atomic.AddInt32(&f.n.refs, -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
}
return copy(p, f.n.mu.data[off:]), 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) 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
}
// 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
}

View File

@@ -0,0 +1,25 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !linux
// +build !linux
package vfs
func preallocExtend(fd uintptr, offset, length int64) error {
// It is ok for correctness to no-op file preallocation. WAL recycling is the
// more important mechanism for WAL sync performance and it doesn't rely on
// fallocate or posix_fallocate in order to be effective.
return nil
}

View File

@@ -0,0 +1,28 @@
// Copyright 2016 The etcd Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build linux
// +build linux
package vfs
import (
"syscall"
"golang.org/x/sys/unix"
)
func preallocExtend(fd uintptr, offset, length int64) error {
return syscall.Fallocate(int(fd), unix.FALLOC_FL_KEEP_SIZE, offset, length)
}

View File

@@ -0,0 +1,16 @@
// 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
// Prefetch signals the OS (on supported platforms) to fetch the next size
// bytes in file (as returned by os.File.Fd()) after offset into cache. Any
// subsequent reads in that range will not issue disk IO.
func Prefetch(file uintptr, offset uint64, size uint64) error {
// No-op.
return nil
}

View File

@@ -0,0 +1,18 @@
// 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 "syscall"
// Prefetch signals the OS (on supported platforms) to fetch the next size
// bytes in file (as returned by os.File.Fd()) after offset into cache. Any
// subsequent reads in that range will not issue disk IO.
func Prefetch(file uintptr, offset uint64, size uint64) error {
_, _, err := syscall.Syscall(syscall.SYS_READAHEAD, uintptr(file), uintptr(offset), uintptr(size))
return err
}

View File

@@ -0,0 +1,192 @@
// 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 bool
BytesPerSync int
PreallocateSize int
}
type syncingFile struct {
File
fd uintptr
useSyncRange bool
closing bool
noSyncOnClose bool
bytesPerSync int64
preallocateSize int64
atomic struct {
// The offset at which dirty data has been written.
offset 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 int64
}
preallocatedBlocks int64
syncData func() error
syncTo func(offset int64) error
timeDiskOp func(op func())
}
// 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,
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.atomic.syncOffset = -1
type fd interface {
Fd() uintptr
}
if d, ok := f.(fd); ok {
s.fd = d.Fd()
}
type dhChecker interface {
timeDiskOp(op func())
}
if d, ok := f.(dhChecker); ok {
s.timeDiskOp = d.timeDiskOp
} else {
s.timeDiskOp = func(op func()) {
op()
}
}
s.init()
if s.syncData == nil {
s.syncData = s.File.Sync
}
return WithFd(f, s)
}
// NB: syncingFile.Write is unsafe for concurrent use!
func (f *syncingFile) Write(p []byte) (n int, err error) {
_ = f.preallocate(atomic.LoadInt64(&f.atomic.offset))
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.
atomic.AddInt64(&f.atomic.offset, int64(n))
if err := f.maybeSync(); err != nil {
return 0, err
}
return n, nil
}
func (f *syncingFile) preallocate(offset int64) error {
if f.fd == 0 || 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 preallocExtend(f.fd, offset, length)
}
func (f *syncingFile) ratchetSyncOffset(offset int64) {
for {
syncOffset := atomic.LoadInt64(&f.atomic.syncOffset)
if syncOffset >= offset {
return
}
if atomic.CompareAndSwapInt64(&f.atomic.syncOffset, 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 sync_file_range).
f.ratchetSyncOffset(atomic.LoadInt64(&f.atomic.offset))
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 := atomic.LoadInt64(&f.atomic.offset)
if offset <= syncRangeBuffer {
return nil
}
const syncRangeAlignment = 4 << 10 // 4 KB
syncToOffset := offset - syncRangeBuffer
syncToOffset -= syncToOffset % syncRangeAlignment
syncOffset := atomic.LoadInt64(&f.atomic.syncOffset)
if syncToOffset < 0 || (syncToOffset-syncOffset) < f.bytesPerSync {
return nil
}
if f.fd == 0 {
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). The syncTo
// implementation must call ratchetSyncOffset with as much of the file as it
// has synced.
return errors.WithStack(f.syncTo(syncToOffset))
}
func (f *syncingFile) Close() error {
// Sync any data that has been written but not yet synced unless the file
// has noSyncOnClose option explicitly set.
// Note that if SyncFileRange was used, atomic.syncOffset will be less than
// atomic.offset. See syncingFile.syncToRange.
f.closing = true
if !f.noSyncOnClose || f.useSyncRange {
if atomic.LoadInt64(&f.atomic.offset) > atomic.LoadInt64(&f.atomic.syncOffset) {
if err := f.Sync(); err != nil {
return errors.WithStack(err)
}
}
}
return errors.WithStack(f.File.Close())
}

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 || arm
// +build !linux arm
package vfs
func (f *syncingFile) init() {
f.syncTo = f.syncToGeneric
}
func (f *syncingFile) syncToGeneric(_ int64) error {
return f.Sync()
}

View File

@@ -0,0 +1,105 @@
// 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 && !arm
// +build linux,!arm
package vfs
import "syscall"
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, fn syncFileRange) bool {
err := fn(int(fd), 0 /* offset */, 0 /* nbytes */, 0 /* flags */)
return err != syscall.ENOSYS
}
func isSyncRangeSupported(fd uintptr) bool {
var stat syscall.Statfs_t
if err := syscall.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, syscall.SyncFileRange)
}
return false
}
func (f *syncingFile) init() {
if f.fd == 0 {
return
}
f.timeDiskOp(func() {
f.useSyncRange = isSyncRangeSupported(f.fd)
})
if f.useSyncRange {
f.syncTo = f.syncToRange
} else {
f.syncTo = f.syncToFdatasync
}
f.syncData = f.syncFdatasync
}
func (f *syncingFile) syncFdatasync() error {
if f.fd == 0 {
return f.File.Sync()
}
var err error
f.timeDiskOp(func() {
err = syscall.Fdatasync(int(f.fd))
})
return err
}
func (f *syncingFile) syncToFdatasync(_ int64) error {
return f.Sync()
}
func (f *syncingFile) syncToRange(offset int64) error {
const (
waitBefore = 0x1
write = 0x2
// waitAfter = 0x4
)
// The flags for the sync_file_range system call. Unless the file has
// noSyncOnClose explicitly set and it is being closed, the waitBefore
// flag will be set which may block the call.
flags := write
if !f.noSyncOnClose || !f.closing {
flags |= waitBefore
}
// Note that syncToRange is only called with an offset that is guaranteed to
// be less than atomic.offset (i.e. the write offset). This implies the
// syncingFile.Close will Sync the rest of the data, as well as the file's
// metadata.
f.ratchetSyncOffset(offset)
// 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.
var err error
f.timeDiskOp(func() {
err = syscall.SyncFileRange(int(f.fd), 0, offset, flags)
})
return err
}

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

@@ -0,0 +1,349 @@
// 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
Stat() (os.FileInfo, error)
Sync() error
}
// 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)
// 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{}
func (defaultFS) Create(name string) (File, error) {
const openFlags = os.O_RDWR | os.O_CREATE | os.O_EXCL | syscall.O_CLOEXEC
f, 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 f, errors.WithStack(removeErr)
}
f, err = os.OpenFile(name, openFlags, 0666)
}
return f, 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) {
file, err := os.OpenFile(name, os.O_RDONLY|syscall.O_CLOEXEC, 0)
if err != nil {
return nil, errors.WithStack(err)
}
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 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) {
type fd interface {
Fd() uintptr
}
if fdFile, ok := f.(fd); ok {
_ = fadviseRandom(fdFile.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) {
type fd interface {
Fd() uintptr
}
if fdFile, ok := f.(fd); ok {
_ = fadviseSequential(fdFile.Fd())
}
}
// Copy copies the contents of oldname to newname. If newname exists, it will
// be overwritten.
func Copy(fs FS, oldname, newname string) error {
src, err := fs.Open(oldname)
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, 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)
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 newname doesn't exist or oldname 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")