ceremonyclient/pebble/vfs/mem_fs.go

833 lines
20 KiB
Go
Raw Permalink Normal View History

2024-01-03 07:31:42 +00:00
// 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"
"github.com/cockroachdb/pebble/shims/slices"
)
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) {
if !y.strict {
panic("SetIgnoreSyncs can only be used on a strict MemFS")
}
y.mu.Lock()
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 {
panic("ResetToSyncedState can only be used on a strict MemFS")
}
y.mu.Lock()
y.root.resetToSyncedState()
y.mu.Unlock()
}
// walk walks the directory tree for the fullname, calling f at each step. If
// f returns an error, the walk will be aborted and return that same error.
//
// Each walk is atomic: y's mutex is held for the entire operation, including
// all calls to f.
//
// dir is the directory at that step, frag is the name fragment, and final is
// whether it is the final step. For example, walking "/foo/bar/x" will result
// in 3 calls to f:
// - "/", "foo", false
// - "/foo/", "bar", false
// - "/foo/bar/", "x", true
//
// Similarly, walking "/y/z/", with a trailing slash, will result in 3 calls to f:
// - "/", "y", false
// - "/y/", "z", false
// - "/y/z/", "", true
func (y *MemFS) walk(fullname string, f func(dir *memNode, frag string, final bool) error) error {
y.mu.Lock()
defer y.mu.Unlock()
// For memfs, the current working directory is the same as the root directory,
// so we strip off any leading "/"s to make fullname a relative path, and
// the walk starts at y.root.
for len(fullname) > 0 && fullname[0] == sep[0] {
fullname = fullname[1:]
}
dir := y.root
for {
frag, remaining := fullname, ""
i := strings.IndexRune(fullname, rune(sep[0]))
final := i < 0
if !final {
frag, remaining = fullname[:i], fullname[i+1:]
for len(remaining) > 0 && remaining[0] == sep[0] {
remaining = remaining[1:]
}
}
if err := f(dir, frag, final); err != nil {
return err
}
if final {
break
}
child := dir.children[frag]
if child == nil {
return &os.PathError{
Op: "open",
Path: fullname,
Err: oserror.ErrNotExist,
}
}
if !child.isDir {
return &os.PathError{
Op: "open",
Path: fullname,
Err: errors.New("not a directory"),
}
}
dir, fullname = child, remaining
}
return nil
}
// Create implements FS.Create.
func (y *MemFS) Create(fullname string) (File, error) {
var ret *memFile
err := y.walk(fullname, func(dir *memNode, frag string, final bool) error {
if final {
if frag == "" {
return errors.New("pebble/vfs: empty file name")
}
n := &memNode{name: frag}
dir.children[frag] = n
ret = &memFile{
n: n,
fs: y,
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 = slices.Clone(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 {
return nil
}
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 = slices.Clone(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()
}