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 ¶
- type Call
- type CrashConfig
- type FS
- func (f *FS) Add(r Rule) *FS
- func (f *FS) Calls() []Call
- func (f *FS) Close() error
- func (f *FS) Crash() *FS
- func (f *FS) CrashWith(cfg CrashConfig) *FS
- func (f *FS) Durable(name string) ([]byte, bool)
- func (f *FS) Kill() *FS
- func (f *FS) Link(oldname, newname string) error
- func (f *FS) MkdirAll(name string, perm fs.FileMode) error
- func (f *FS) OpenFile(name string, flag int, perm fs.FileMode) (vfs.File, error)
- func (f *FS) ReadDir(name string) ([]fs.DirEntry, error)
- func (f *FS) ReadFile(name string) ([]byte, error)
- func (f *FS) Remove(name string) error
- func (f *FS) Rename(oldname, newname string) error
- func (f *FS) Reset()
- func (f *FS) Stat(name string) (fs.FileInfo, error)
- func (f *FS) SyncDir(name string) error
- func (f *FS) Tear(name string, keep int) error
- type Gate
- type Op
- type Rule
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Call ¶
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 (*FS) Add ¶
Add installs a rule. Rules are consulted in order; the first to match an operation decides it.
func (*FS) Crash ¶
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 ¶
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 ¶
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 (*FS) Reset ¶
func (f *FS) Reset()
Reset removes every rule, leaving the filesystem's contents and its recorded calls in place.
func (*FS) SyncDir ¶
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 ¶
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 (*Gate) Await ¶
Await blocks until a matching operation reaches the gate, returning it. The operation stays suspended until Gate.Release.
type Op ¶
type Op int
Op is the filesystem operation a Rule matches.
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.