[WIP] Initial commit
This commit is contained in:
241
vendor/github.com/cockroachdb/pebble/vfs/atomicfs/marker.go
generated
vendored
Normal file
241
vendor/github.com/cockroachdb/pebble/vfs/atomicfs/marker.go
generated
vendored
Normal 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
137
vendor/github.com/cockroachdb/pebble/vfs/clone.go
generated
vendored
Normal 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
|
||||
}
|
||||
132
vendor/github.com/cockroachdb/pebble/vfs/default_linux.go
generated
vendored
Normal file
132
vendor/github.com/cockroachdb/pebble/vfs/default_linux.go
generated
vendored
Normal 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
|
||||
}
|
||||
49
vendor/github.com/cockroachdb/pebble/vfs/default_unix.go
generated
vendored
Normal file
49
vendor/github.com/cockroachdb/pebble/vfs/default_unix.go
generated
vendored
Normal 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
|
||||
}
|
||||
61
vendor/github.com/cockroachdb/pebble/vfs/default_windows.go
generated
vendored
Normal file
61
vendor/github.com/cockroachdb/pebble/vfs/default_windows.go
generated
vendored
Normal 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
453
vendor/github.com/cockroachdb/pebble/vfs/disk_full.go
generated
vendored
Normal 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
791
vendor/github.com/cockroachdb/pebble/vfs/disk_health.go
generated
vendored
Normal 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 }
|
||||
44
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_linux.go
generated
vendored
Normal file
44
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_linux.go
generated
vendored
Normal 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
|
||||
}
|
||||
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_netbsd.go
generated
vendored
Normal file
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_netbsd.go
generated
vendored
Normal 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
|
||||
}
|
||||
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go
generated
vendored
Normal file
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go
generated
vendored
Normal 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
|
||||
}
|
||||
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go
generated
vendored
Normal file
26
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go
generated
vendored
Normal 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
|
||||
}
|
||||
22
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_windows.go
generated
vendored
Normal file
22
vendor/github.com/cockroachdb/pebble/vfs/disk_usage_windows.go
generated
vendored
Normal 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
|
||||
}
|
||||
21
vendor/github.com/cockroachdb/pebble/vfs/errors_unix.go
generated
vendored
Normal file
21
vendor/github.com/cockroachdb/pebble/vfs/errors_unix.go
generated
vendored
Normal 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)
|
||||
}
|
||||
22
vendor/github.com/cockroachdb/pebble/vfs/errors_windows.go
generated
vendored
Normal file
22
vendor/github.com/cockroachdb/pebble/vfs/errors_windows.go
generated
vendored
Normal 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)
|
||||
}
|
||||
16
vendor/github.com/cockroachdb/pebble/vfs/fadvise_generic.go
generated
vendored
Normal file
16
vendor/github.com/cockroachdb/pebble/vfs/fadvise_generic.go
generated
vendored
Normal 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
|
||||
}
|
||||
20
vendor/github.com/cockroachdb/pebble/vfs/fadvise_linux.go
generated
vendored
Normal file
20
vendor/github.com/cockroachdb/pebble/vfs/fadvise_linux.go
generated
vendored
Normal 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)
|
||||
}
|
||||
20
vendor/github.com/cockroachdb/pebble/vfs/file_lock_generic.go
generated
vendored
Normal file
20
vendor/github.com/cockroachdb/pebble/vfs/file_lock_generic.go
generated
vendored
Normal 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))
|
||||
}
|
||||
70
vendor/github.com/cockroachdb/pebble/vfs/file_lock_unix.go
generated
vendored
Normal file
70
vendor/github.com/cockroachdb/pebble/vfs/file_lock_unix.go
generated
vendored
Normal 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
|
||||
}
|
||||
42
vendor/github.com/cockroachdb/pebble/vfs/file_lock_windows.go
generated
vendored
Normal file
42
vendor/github.com/cockroachdb/pebble/vfs/file_lock_windows.go
generated
vendored
Normal 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
166
vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go
generated
vendored
Normal 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
835
vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go
generated
vendored
Normal 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()
|
||||
}
|
||||
215
vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go
generated
vendored
Normal file
215
vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go
generated
vendored
Normal 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
419
vendor/github.com/cockroachdb/pebble/vfs/vfs.go
generated
vendored
Normal 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")
|
||||
Reference in New Issue
Block a user