This release adds facetcache, a caching FileSystem wrapper for slow backends. The package sits between a remote backend and any protocol server. Every protocol package benefits without change.
The facetcache package
facetcache.Cache wraps a backend and serves reads from local disk:
cache := &facetcache.Cache{Backend: backend, Dir: "/var/cache/facet"}
fsys, err := cache.FileSystem()
srv := &nfs4.Server{FileSystem: fsys}The wrapper keeps two caches:
- An attribute cache answers
StatandLstatfrom memory with a short TTL and negative entries. EveryReaddirbatch populates it for free. NFS costs several stats per read, so this cache matters more than the bytes against a high-latency backend. - A content cache stores file bytes in one sparse file per object, tracks exactly which byte ranges are present, fills misses from the backend with adaptive read-ahead, and parks concurrent readers of one region behind a single bounded fetch.
Content persists across restarts. A size and mtime fingerprint detects a backend object replaced behind the same name and discards its stale ranges. Metadata is written by atomic rename, and the data file is fdatasynced before any metadata publish that claims new ranges, so a crash can not turn holes into silently served zeros.
Writes go through: the backend must acknowledge a write before the cache stores it. There is no dirty state, so eviction never waits on uploads and a crash never loses writes.
A single janitor owns the disk budget with live accounting: TTL eviction, LRU eviction by size, and, on Linux, macOS, and Windows, hole punching behind the read head of open streams when whole-file eviction cannot reach the budget. The read path re-validates presence after every pread, so a racing punch causes a refetch, never zeros. On Windows, cache files are marked sparse at creation; a non-sparse NTFS file would physically zero-fill below far-offset writes.
The wrapper preserves the backend's capability set exactly. A generated matrix exposes SymlinkFS, LinkFS, RemoveFS, SetStatFS, and StatVFSFS only when the backend implements them, and cached files keep io.ReaderAt, io.WriterAt, and Sync under the same rule. The servers detect these by assertion; claiming or hiding one silently changes protocol behavior.
Performance
The warm path adds no measurable cost: a warm 64 KiB ReadAt matches a direct pread with zero allocations, a warm Stat is 69 ns with zero allocations, and the stateless-NFS-shaped OpenFile+Close is 180 ns with one allocation (Apple M1 Pro).
Against a backend charged 5 ms per call, through a real Linux kernel NFS client with attribute caching disabled: warm stats and reads improve 117-166x and match the fast-backend numbers, writes improve 1.8x, readdir improves 5x. Over a fast backend the cached server is 7-16% faster on reads and stats than the uncached one, because the attribute cache absorbs the client's stat storm.
Verification
- Formatting,
go vet, CGO-free tests and build - Full race detector suite, including concurrent open/close/eviction and download deduplication
- Property-based range-set tests against a bitmap model
- Crash-consistency tests: corrupt metadata, interrupted flushes, orphan files
- APFS hole punching verified by block count; punch-then-read refetch verified end to end
- Linux, Windows, and FreeBSD cross-compilation
- Real Linux kernel NFS client acceptance through the containerized benchmark harness