faultfs

package
v0.45.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 7, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package faultfs is a vfs.FS that models what a machine loses when it stops without warning, so a durability claim can be tested instead of argued.

A real filesystem makes two separate promises, and the bugs live in the gap between them: a file's bytes reach the disk on fsync, while the *name* that reaches those bytes reaches the disk only when its directory is synced. Code that writes a temp file, fsyncs it and renames it into place has done the first and not the second — after a power cut the bytes are there and nothing names them. This package keeps the two apart and, on FS.Crash, returns the filesystem a pessimistic-but-legal machine would come back with.

Pessimistic is the point: where a real disk *may* have persisted an unsynced write, Crash decides it did not. That is the worst outcome the POSIX contract permits, so code that survives it survives any filesystem, and a test that passes here does not depend on the ordering a particular device happened to give it.

It also injects faults, in the shape [faultbackend] uses one level up: a Rule fails or suspends the operations it matches, so an error path or an interleaving is stated by the test rather than raced for.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Call

type Call struct {
	Op   Op
	Name string
	To   string
}

Call is one filesystem operation offered to a Rule. To for a rename or link is the destination.

type CrashConfig

type CrashConfig struct {
	// UnsyncedPercent is the chance, drawn per 4 KiB data block and per pending directory entry,
	// that a real disk had already written it out when the power went. 0 keeps exactly what was
	// synced; 100 keeps the whole pre-crash state. Values outside [0, 100] are clamped.
	UnsyncedPercent int
	// Seed fixes every draw. The same seed against the same filesystem gives the same result, so a
	// failure is replayable from the seed alone.
	Seed uint64
}

CrashConfig asks FS.CrashWith for a crash that kept some of what was never synced. The zero value is FS.Crash: nothing unsynced survives.

type FS

type FS struct {
	// contains filtered or unexported fields
}

FS is an in-memory vfs.FS with a durability model and fault injection. The zero value is not usable; call New.

func New

func New() *FS

New returns an empty filesystem.

func (*FS) Add

func (f *FS) Add(r Rule) *FS

Add installs a rule. Rules are consulted in order; the first to match an operation decides it.

func (*FS) Calls

func (f *FS) Calls() []Call

Calls returns the operations performed so far, in order.

func (*FS) Close

func (f *FS) Close() error

Close implements vfs.FS.

func (*FS) Crash

func (f *FS) Crash() *FS

Crash returns the filesystem a machine would come back with after losing power now: every name whose directory was synced, holding the bytes of that file's last vfs.File.Sync. Everything else — unsynced writes, and names created, renamed or removed without a FS.SyncDir — is gone.

Where a real disk *may* have kept an unsynced write, this keeps none of it. That is the worst outcome POSIX permits, so passing here means passing on any filesystem; it also makes the result deterministic, which a test that samples the possible outcomes could not be.

The returned FS is fresh: no rules, no recorded calls, nothing pending. The receiver is left alone, so one state can be crashed more than once (before and after a repair, say).

func (*FS) CrashWith

func (f *FS) CrashWith(cfg CrashConfig) *FS

CrashWith is FS.Crash with a disk that may have got ahead of the syncs.

Writeback is per block, not per file: a device is free to have written block n and not block n-1, and the surviving file then comes back *longer* than its synced length with a zero-filled gap where the lost block was. A replayer that reads a run of zeros as the end of the log walks past live records; the deterministic FS.Crash cannot produce that state, because losing a prefix is all it can do. Directory entries are drawn the same way — an unsynced create or rename may or may not have landed, independently of the bytes it names.

Synced data is never taken away: the draws only add. So a run over many seeds explores the space between FS.Crash (nothing extra) and FS.Kill (everything), and every point in it is a state the POSIX contract permits.

func (*FS) Durable

func (f *FS) Durable(name string) ([]byte, bool)

Durable reports the bytes name would come back with after a FS.Crash, and whether it would come back at all. It is the assertion the durability tests are written against.

func (*FS) Kill

func (f *FS) Kill() *FS

Kill returns the filesystem a machine would come back with after the *process* died — a panic, a SIGKILL, an OOM — while the machine kept running. Nothing is lost: the writes are in the page cache and the kernel still owns them.

It is here to keep the two failure modes apart. Code that survives Kill but not FS.Crash is crash-safe against a process failure and not against a power cut, and saying which one a durability claim covers is most of the claim.

func (f *FS) Link(oldname, newname string) error

Link implements vfs.FS.

func (*FS) MkdirAll

func (f *FS) MkdirAll(name string, perm fs.FileMode) error

MkdirAll implements vfs.FS.

func (*FS) OpenFile

func (f *FS) OpenFile(name string, flag int, perm fs.FileMode) (vfs.File, error)

OpenFile implements vfs.FS.

func (*FS) ReadDir

func (f *FS) ReadDir(name string) ([]fs.DirEntry, error)

ReadDir implements vfs.FS.

func (*FS) ReadFile

func (f *FS) ReadFile(name string) ([]byte, error)

ReadFile implements vfs.FS.

func (*FS) Remove

func (f *FS) Remove(name string) error

Remove implements vfs.FS.

func (*FS) Rename

func (f *FS) Rename(oldname, newname string) error

Rename implements vfs.FS.

func (*FS) Reset

func (f *FS) Reset()

Reset removes every rule, leaving the filesystem's contents and its recorded calls in place.

func (*FS) Stat

func (f *FS) Stat(name string) (fs.FileInfo, error)

Stat implements vfs.FS.

func (*FS) SyncDir

func (f *FS) SyncDir(name string) error

SyncDir implements vfs.FS: it commits the name changes made in the directory. A name that appeared becomes durable carrying the bytes of its last file sync — nothing more, so a file whose directory was synced but whose own bytes were not comes back empty rather than full.

func (*FS) Tear

func (f *FS) Tear(name string, keep int) error

Tear models a partial write reaching the platter: keep bytes of name's uncommitted tail survive the next FS.Crash, the rest do not. A record framed across that boundary comes back truncated, which is the shape a torn append actually takes and the one a replayer has to tell apart from a clean end of file.

keep is clamped to the uncommitted tail: 0 tears the whole tail away (the default a crash gives), and a keep past the tail's length commits all of it.

type Gate

type Gate struct {
	// contains filtered or unexported fields
}

Gate suspends the filesystem operations matching it until the test releases them, so a test can state an interleaving — a crash *during* a publish, an append landing between a rename and its directory sync — instead of racing for it. The counterpart to faultbackend's gate, one layer down.

func NewGate

func NewGate() *Gate

NewGate returns a gate with no operation suspended.

func (*Gate) Await

func (g *Gate) Await(tb testing.TB) Call

Await blocks until a matching operation reaches the gate, returning it. The operation stays suspended until Gate.Release.

func (*Gate) Release

func (g *Gate) Release()

Release resumes every operation held at the gate, and any that arrive later.

func (*Gate) Rule

func (g *Gate) Rule(op Op, match func(Call) bool) Rule

Rule returns the rule that suspends op's matching operations at this gate.

type Op

type Op int

Op is the filesystem operation a Rule matches.

const (
	OpOpen Op = iota
	OpCreate
	OpRead
	OpWrite
	OpSync
	OpSyncDir
	OpRename
	OpLink
	OpRemove
	OpMkdir
	OpStat
	OpReadDir
)

The operations a rule can match.

func (Op) String

func (o Op) String() string

String implements fmt.Stringer.

type Rule

type Rule struct {
	Op    Op
	Match func(Call) bool
	// Err, when non-nil, is returned instead of performing the operation.
	Err error
	// Before, when non-nil, runs before the operation. It may block, which suspends the calling
	// goroutine inside the filesystem (see [Gate]).
	Before func(Call)
	// Times limits how many operations the rule applies to. Zero ⇒ unlimited.
	Times int
	// contains filtered or unexported fields
}

Rule decides what happens to the operations it matches. A rule with no Match matches every operation of its Op.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL