Help us shape the future of Go! Take the 2026 Go Developer Survey

pool

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 26, 2026 License: BSD-3-Clause Imports: 9 Imported by: 1

README

pool

Go Reference CI

A pure-Go, copy-on-write pooled volume manager — a small, ZFS-inspired alternative to LVM thin provisioning. No cgo, no root, no device-mapper.

A pool owns a flat array of fixed-size physical blocks backed by a single file. Volumes are logical block devices carved out of the pool; snapshots are immutable, reference-counted captures of a volume. Writes are copy-on-write, so overwriting a block shared with a snapshot allocates a fresh block and leaves the snapshot untouched.

Why not LVM?

LVM thin snapshots can corrupt when the pool fills past a threshold: ext4 (or XFS) overwrites in place and assumes its blocks are durable, but the thin pool needs a new block for the copy-on-write — and if none is free, the write fails mid-operation and the filesystem/snapshot is left inconsistent.

pool is built so that can't happen:

  • A snapshot is immutable — its blocks are reference-counted and never rewritten, so it can't be corrupted by the live volume filling up.
  • When the pool is full, a copy-on-write write fails cleanly and atomically with ErrPoolFull, before any on-disk state changes. A filesystem mounted on the volume sees a normal ENOSPC/EIO (which ext4/XFS handle) rather than silent corruption.

This is the ZFS behaviour (refuse the write, keep snapshots intact), brought to non-CoW filesystems via a block layer underneath them.

Filesystem-agnostic

A Volume implements ReadAt, WriteAt, Sync, Size, Truncate, Close — the same block-backend shape the go-filesystems ext4/xfs drivers accept (OpenFromDevice). So you can format and mount ext4, XFS, or any block-based driver straight onto a pool volume, and it composes with go-fde for encryption (pool → fde → fs).

Note: XFS historically had no free integrated volume manager (SGI's XLV/XVM were proprietary), so a CoW pool under XFS fills a real gap.

Verified end-to-end: a real ext4 filesystem (the go-filesystems/ext4 driver) was run live on a pool volume via OpenFromDevice, a file was written, the volume snapshotted, then the live file overwritten and a new file added. Read back through ext4, the live volume showed the new state while the snapshot showed the exact pre-snapshot filesystem — ext4 never knew it was on a CoW pool, and the snapshot was fully isolated.

Usage

p, _ := pool.Create("data.pool", 64<<20) // 64 MiB pool
defer p.Close()

vol, _ := p.CreateVolume("root", 32<<20)
vol.WriteAt(data, off)                   // copy-on-write

p.Snapshot("root", "before-upgrade")     // immutable snapshot
// ... keep writing to vol; the snapshot is undisturbed ...

snap, _ := p.OpenVolume("before-upgrade")
snap.ReadAt(buf, off)                    // original bytes

// Instant, space-shared writable clone (ZFS-style).
clone, _ := p.Clone("before-upgrade", "vm1")
clone.WriteAt(data, off)                 // diverges from the origin, CoW

Raw images (e.g. Apple Virtualization.framework / vz)

Some consumers only accept raw disk files and do block I/O on them at the kernel level — they can't call a Go block backend. Apple's Virtualization.framework (vz) is the prime example. The pool bridges to them: keep golden images + thin CoW clones + snapshots in the pool, then export a clone to a sparse raw file for the consumer, and import the modified file back to capture changes.

clone, _ := p.Clone("golden", "vm1")
clone.ExportRawFile("vm1.raw")           // sparse raw image for vz to boot
// ... vz runs and writes vm1.raw ...
p.ImportRawFile("vm1-after", "vm1.raw")  // back under pool management; snapshot it

This gives raw-only hypervisors what they lack natively — instant golden-image clones, thin provisioning, and safe snapshots — while they still see just a raw file.

Status / limitations

  • Single backing file (multi-device pooling / RAID is a planned follow-up).
  • Fixed volume size at creation; online grow/shrink not yet implemented.
  • Linear free-block search (fine for moderate pools; a free bitmap is a follow-up).

Documentation

Overview

Package pool is a copy-on-write, pooled block-volume manager — a small, ZFS-inspired alternative to LVM thin provisioning.

A pool owns a flat array of fixed-size physical blocks backed by a single file. Volumes are logical block devices carved out of the pool; each keeps a logical→physical block map. Writes are copy-on-write: overwriting a block that is shared with a snapshot allocates a fresh physical block, so the snapshot's data is never disturbed.

The defining property — and the reason it is not LVM — is failure behaviour: a snapshot is immutable (its blocks are reference-counted and never rewritten), so it can never be corrupted by the live volume filling up. When the pool runs out of free blocks, a copy-on-write write fails *cleanly* with ErrPoolFull, atomically, before any on-disk state changes. The caller (or a filesystem mounted on the volume) sees a normal ENOSPC/EIO rather than silent corruption.

Any block-device consumer can sit on a Volume: it implements ReadAt, WriteAt, Sync, Size, Truncate and Close, the same shape the go-filesystems ext4/xfs drivers accept as a BlockBackend.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrPoolFull is returned by a write that needs to allocate a physical
	// block when none are free. The write makes no on-disk change, so
	// snapshots and already-written data stay intact.
	ErrPoolFull = errors.New("pool: no free blocks (pool full)")

	// ErrBadMagic is returned when opening a file that is not a pool.
	ErrBadMagic = errors.New("pool: bad magic")

	// ErrUnsupportedVersion is returned for an unknown on-disk version.
	ErrUnsupportedVersion = errors.New("pool: unsupported version")

	// ErrNotFound is returned when a named volume/snapshot does not exist.
	ErrNotFound = errors.New("pool: volume not found")

	// ErrExists is returned when creating a volume/snapshot whose name is taken.
	ErrExists = errors.New("pool: name already exists")

	// ErrReadOnly is returned by writes to a snapshot or a read-only volume.
	ErrReadOnly = errors.New("pool: volume is read-only")

	// ErrCorrupt is returned when on-disk metadata fails a sanity check.
	ErrCorrupt = errors.New("pool: corrupt metadata")
)

Functions

This section is empty.

Types

type Backing

type Backing interface {
	io.ReaderAt // ReadAt(p []byte, off int64) (n int, err error)
	io.WriterAt // WriteAt(p []byte, off int64) (n int, err error)

	// Truncate sets the store's size in bytes (growing reserves space, the way
	// Create reserves the data region; shrinking discards the tail).
	Truncate(size int64) error

	// Sync durably commits buffered writes.
	Sync() error

	io.Closer // Close releases the store.
}

Backing is the physical byte store a Pool's data and metadata live on: a single local file today, an S3-backed chunk store or an NBD export tomorrow. The copy-on-write volume engine is identical regardless of where the bytes physically reside — only the Backing changes.

*os.File satisfies Backing directly, so the file-backed Create / Open paths need no adapter. To run a pool on a different store, implement Backing and open the pool with CreateWith / OpenWith.

type Pool

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

Pool is an open copy-on-write block pool backed by a single file.

On-disk layout:

[0, blockSize)                 header
[blockSize, blockSize+N*BS)    data region: N physical blocks
[metaOffset, metaOffset+metaLen) gob-encoded metadata (refcounts + volumes)

The data region is fixed; metadata is rewritten after the data region on every Sync (its offset/length live in the header).

func Create

func Create(path string, capacityBytes int64) (*Pool, error)

Create makes a new pool at path with room for capacityBytes of data, rounded down to whole blocks. An existing file is overwritten.

func CreateBlock

func CreateBlock(path string, capacityBytes, blockSize int64) (*Pool, error)

CreateBlock is Create with an explicit block size (power of two ≥ 512).

func CreateWith

func CreateWith(b Backing, capacityBytes, blockSize int64) (*Pool, error)

CreateWith makes a new pool on an arbitrary Backing — a memory buffer, an S3-backed chunk store, an NBD export — rather than a local file. The backing is assumed empty; CreateWith reserves the data region on it. On success the pool owns the backing and Pool.Close closes it; on error the caller keeps ownership of b.

func Open

func Open(path string) (*Pool, error)

Open opens an existing file-backed pool.

func OpenWith

func OpenWith(b Backing) (*Pool, error)

OpenWith opens an existing pool that lives on an arbitrary Backing. On success the pool owns the backing and Pool.Close closes it; on error the caller keeps ownership of b.

func (*Pool) Capacity

func (p *Pool) Capacity() (free, total uint32)

Capacity returns (freeBlocks, totalBlocks).

func (*Pool) Clone

func (p *Pool) Clone(src, clone string) (*Volume, error)

Clone creates a new *writable* volume that shares src's blocks copy-on-write (an instant, space-shared branch — ZFS-style). Writes to the clone or to src diverge independently. src is typically a snapshot, but any volume works.

func (*Pool) Close

func (p *Pool) Close() error

Close syncs and closes the pool.

func (*Pool) CreateVolume

func (p *Pool) CreateVolume(name string, sizeBytes int64) (*Volume, error)

CreateVolume creates a new, empty (all-holes) volume of sizeBytes.

func (*Pool) ImportRaw

func (p *Pool) ImportRaw(name string, r io.ReaderAt, size int64) (*Volume, error)

ImportRaw creates a new volume named `name` of the given size and populates it from the raw image r. All-zero blocks are left unallocated (holes), so a sparse or zero-padded raw image imports thinly. Useful to bring a raw image (e.g. one a vz guest wrote to) back under pool management — snapshot it afterwards to capture a point in time.

func (*Pool) ImportRawFile

func (p *Pool) ImportRawFile(name, path string) (*Volume, error)

ImportRawFile is ImportRaw reading from a file at path.

func (*Pool) OpenVolume

func (p *Pool) OpenVolume(name string) (*Volume, error)

OpenVolume returns a handle to an existing volume or snapshot.

func (*Pool) Snapshot

func (p *Pool) Snapshot(src, snap string) (*Volume, error)

Snapshot freezes the current state of volume `src` under name `snap`. The snapshot is read-only and immutable: its blocks are reference-counted, so later writes to the live volume copy-on-write rather than disturb it.

func (*Pool) Sync

func (p *Pool) Sync() error

Sync flushes metadata and data to disk.

func (*Pool) Volumes

func (p *Pool) Volumes() []string

Volumes lists all volume and snapshot names.

type Volume

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

Volume is a logical block device carved out of a Pool. It implements the block-backend shape (ReadAt, WriteAt, Sync, Size, Truncate, Close) that the go-filesystems ext4/xfs drivers accept, so a filesystem can be formatted and mounted directly on it.

func (*Volume) Close

func (v *Volume) Close() error

Close is a no-op for a volume handle; the pool owns the file. Sync the pool (or Close it) to persist.

func (*Volume) ExportRaw

func (v *Volume) ExportRaw(w io.WriterAt) error

ExportRaw writes the volume's logical contents to w as a flat ("raw") image. Only allocated blocks are written; unallocated (hole) blocks are skipped, so when w is a freshly-truncated file the result is a sparse raw image. This is the bridge to consumers that only accept raw files — e.g. Apple's Virtualization.framework (vz), which attaches raw disk images at the kernel level and cannot call a Go block backend. Clone a golden volume, ExportRaw the clone, hand the raw file to vz; ImportRaw the modified file back to capture changes.

func (*Volume) ExportRawFile

func (v *Volume) ExportRawFile(path string) error

ExportRawFile writes the volume to a sparse raw file at path (overwriting it), truncated to the volume's logical size. The file is suitable to attach to a VM that requires raw images.

func (*Volume) Name

func (v *Volume) Name() string

Name returns the volume name.

func (*Volume) ReadAt

func (v *Volume) ReadAt(b []byte, off int64) (int, error)

ReadAt reads len(b) bytes at byte offset off. Unallocated regions read as zeros (sparse).

func (*Volume) ReadOnly

func (v *Volume) ReadOnly() bool

ReadOnly reports whether the volume is read-only (snapshots always are).

func (*Volume) Size

func (v *Volume) Size() (int64, error)

Size returns the volume's logical size in bytes.

func (*Volume) Sync

func (v *Volume) Sync() error

Sync flushes the backing pool.

func (*Volume) Truncate

func (v *Volume) Truncate(size int64) error

Truncate is accepted for block-backend compatibility but the volume size is fixed at creation; growing/shrinking is not supported in this version.

func (*Volume) WriteAt

func (v *Volume) WriteAt(b []byte, off int64) (int, error)

WriteAt writes b at byte offset off, copy-on-write. A write that must allocate a new physical block when the pool is full fails with ErrPoolFull without changing any on-disk state — so snapshots and prior data are never corrupted.

Jump to

Keyboard shortcuts

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