Browse Source
feat: Phase 5 CP5-2 -- CoW snapshots, 10 tests
feat: Phase 5 CP5-2 -- CoW snapshots, 10 tests
Sparse delta-file snapshots with copy-on-write in the flusher. Zero write-path overhead when no snapshot is active. New: snapshot.go (SnapshotBitmap, SnapshotHeader, delta file I/O) Modified: flusher.go (flushMu, CoW phase in FlushOnce, PauseAndFlush) Modified: blockvol.go (Create/Read/Delete/Restore/ListSnapshots, recovery) Modified: wal_writer.go (Reset for snapshot restore) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>feature/sw-block
5 changed files with 1197 additions and 22 deletions
-
275weed/storage/blockvol/blockvol.go
-
128weed/storage/blockvol/flusher.go
-
296weed/storage/blockvol/snapshot.go
-
512weed/storage/blockvol/snapshot_test.go
-
8weed/storage/blockvol/wal_writer.go
@ -0,0 +1,296 @@ |
|||||
|
package blockvol |
||||
|
|
||||
|
import ( |
||||
|
"encoding/binary" |
||||
|
"errors" |
||||
|
"fmt" |
||||
|
"io" |
||||
|
"os" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
// Snapshot delta file constants.
|
||||
|
const ( |
||||
|
SnapMagic = "SWBS" |
||||
|
SnapVersion = 1 |
||||
|
SnapHeaderSize = 4096 |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrSnapshotNotFound = errors.New("blockvol: snapshot not found") |
||||
|
ErrSnapshotExists = errors.New("blockvol: snapshot already exists") |
||||
|
ErrSnapshotBadMagic = errors.New("blockvol: snapshot bad magic") |
||||
|
ErrSnapshotBadVersion = errors.New("blockvol: snapshot unsupported version") |
||||
|
ErrSnapshotBadParent = errors.New("blockvol: snapshot parent UUID mismatch") |
||||
|
ErrSnapshotRoleReject = errors.New("blockvol: snapshots only allowed on primary or standalone") |
||||
|
) |
||||
|
|
||||
|
// SnapshotBitmap is a dense bitset tracking which blocks have been CoW'd.
|
||||
|
// No internal locking -- callers (flusher, CreateSnapshot) serialize access.
|
||||
|
type SnapshotBitmap struct { |
||||
|
data []byte |
||||
|
bits uint64 // total number of bits (= VolumeSize/BlockSize)
|
||||
|
} |
||||
|
|
||||
|
// NewSnapshotBitmap creates a zero-initialized bitmap for totalBlocks blocks.
|
||||
|
func NewSnapshotBitmap(totalBlocks uint64) *SnapshotBitmap { |
||||
|
byteLen := (totalBlocks + 7) / 8 |
||||
|
return &SnapshotBitmap{ |
||||
|
data: make([]byte, byteLen), |
||||
|
bits: totalBlocks, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Get returns true if the bit at position lba is set.
|
||||
|
func (b *SnapshotBitmap) Get(lba uint64) bool { |
||||
|
if lba >= b.bits { |
||||
|
return false |
||||
|
} |
||||
|
return b.data[lba/8]&(1<<(lba%8)) != 0 |
||||
|
} |
||||
|
|
||||
|
// Set sets the bit at position lba.
|
||||
|
func (b *SnapshotBitmap) Set(lba uint64) { |
||||
|
if lba >= b.bits { |
||||
|
return |
||||
|
} |
||||
|
b.data[lba/8] |= 1 << (lba % 8) |
||||
|
} |
||||
|
|
||||
|
// ByteSize returns the number of bytes in the bitmap.
|
||||
|
func (b *SnapshotBitmap) ByteSize() int { |
||||
|
return len(b.data) |
||||
|
} |
||||
|
|
||||
|
// WriteTo writes the bitmap data to w at the given offset.
|
||||
|
func (b *SnapshotBitmap) WriteTo(w io.WriterAt, offset int64) error { |
||||
|
_, err := w.WriteAt(b.data, offset) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// ReadFrom reads bitmap data from r at the given offset.
|
||||
|
func (b *SnapshotBitmap) ReadFrom(r io.ReaderAt, offset int64) error { |
||||
|
_, err := r.ReadAt(b.data, offset) |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
// CountSet returns the number of set bits (CoW'd blocks).
|
||||
|
func (b *SnapshotBitmap) CountSet() uint64 { |
||||
|
var count uint64 |
||||
|
for _, v := range b.data { |
||||
|
count += uint64(popcount8(v)) |
||||
|
} |
||||
|
return count |
||||
|
} |
||||
|
|
||||
|
// popcount8 returns the number of set bits in a byte.
|
||||
|
func popcount8(x byte) int { |
||||
|
x = x - ((x >> 1) & 0x55) |
||||
|
x = (x & 0x33) + ((x >> 2) & 0x33) |
||||
|
return int((x + (x >> 4)) & 0x0F) |
||||
|
} |
||||
|
|
||||
|
// SnapshotHeader is the on-disk header for a snapshot delta file.
|
||||
|
type SnapshotHeader struct { |
||||
|
Magic [4]byte |
||||
|
Version uint16 |
||||
|
SnapshotID uint32 |
||||
|
BaseLSN uint64 |
||||
|
VolumeSize uint64 |
||||
|
BlockSize uint32 |
||||
|
BitmapSize uint64 |
||||
|
DataOffset uint64 // = SnapHeaderSize + BitmapSize, aligned to BlockSize
|
||||
|
CreatedAt uint64 |
||||
|
ParentUUID [16]byte |
||||
|
} |
||||
|
|
||||
|
// WriteTo serializes the header as a SnapHeaderSize-byte block to w.
|
||||
|
func (h *SnapshotHeader) WriteTo(w io.Writer) (int64, error) { |
||||
|
buf := make([]byte, SnapHeaderSize) |
||||
|
endian := binary.LittleEndian |
||||
|
off := 0 |
||||
|
off += copy(buf[off:], h.Magic[:]) |
||||
|
endian.PutUint16(buf[off:], h.Version) |
||||
|
off += 2 |
||||
|
endian.PutUint32(buf[off:], h.SnapshotID) |
||||
|
off += 4 |
||||
|
endian.PutUint64(buf[off:], h.BaseLSN) |
||||
|
off += 8 |
||||
|
endian.PutUint64(buf[off:], h.VolumeSize) |
||||
|
off += 8 |
||||
|
endian.PutUint32(buf[off:], h.BlockSize) |
||||
|
off += 4 |
||||
|
endian.PutUint64(buf[off:], h.BitmapSize) |
||||
|
off += 8 |
||||
|
endian.PutUint64(buf[off:], h.DataOffset) |
||||
|
off += 8 |
||||
|
endian.PutUint64(buf[off:], h.CreatedAt) |
||||
|
off += 8 |
||||
|
copy(buf[off:], h.ParentUUID[:]) |
||||
|
n, err := w.Write(buf) |
||||
|
return int64(n), err |
||||
|
} |
||||
|
|
||||
|
// ReadSnapshotHeader reads a SnapshotHeader from r.
|
||||
|
func ReadSnapshotHeader(r io.Reader) (*SnapshotHeader, error) { |
||||
|
buf := make([]byte, SnapHeaderSize) |
||||
|
if _, err := io.ReadFull(r, buf); err != nil { |
||||
|
return nil, fmt.Errorf("blockvol: read snapshot header: %w", err) |
||||
|
} |
||||
|
|
||||
|
endian := binary.LittleEndian |
||||
|
h := &SnapshotHeader{} |
||||
|
off := 0 |
||||
|
copy(h.Magic[:], buf[off:off+4]) |
||||
|
off += 4 |
||||
|
if string(h.Magic[:]) != SnapMagic { |
||||
|
return nil, ErrSnapshotBadMagic |
||||
|
} |
||||
|
h.Version = endian.Uint16(buf[off:]) |
||||
|
off += 2 |
||||
|
if h.Version != SnapVersion { |
||||
|
return nil, fmt.Errorf("%w: got %d, want %d", ErrSnapshotBadVersion, h.Version, SnapVersion) |
||||
|
} |
||||
|
h.SnapshotID = endian.Uint32(buf[off:]) |
||||
|
off += 4 |
||||
|
h.BaseLSN = endian.Uint64(buf[off:]) |
||||
|
off += 8 |
||||
|
h.VolumeSize = endian.Uint64(buf[off:]) |
||||
|
off += 8 |
||||
|
h.BlockSize = endian.Uint32(buf[off:]) |
||||
|
off += 4 |
||||
|
h.BitmapSize = endian.Uint64(buf[off:]) |
||||
|
off += 8 |
||||
|
h.DataOffset = endian.Uint64(buf[off:]) |
||||
|
off += 8 |
||||
|
h.CreatedAt = endian.Uint64(buf[off:]) |
||||
|
off += 8 |
||||
|
copy(h.ParentUUID[:], buf[off:off+16]) |
||||
|
return h, nil |
||||
|
} |
||||
|
|
||||
|
// activeSnapshot represents an open snapshot delta file with its in-memory bitmap.
|
||||
|
type activeSnapshot struct { |
||||
|
id uint32 |
||||
|
fd *os.File |
||||
|
header SnapshotHeader |
||||
|
bitmap *SnapshotBitmap |
||||
|
dataOffset uint64 |
||||
|
dirty bool // bitmap changed since last persist
|
||||
|
} |
||||
|
|
||||
|
// Close closes the delta file.
|
||||
|
func (s *activeSnapshot) Close() error { |
||||
|
return s.fd.Close() |
||||
|
} |
||||
|
|
||||
|
// deltaFilePath returns the path for a snapshot delta file.
|
||||
|
func deltaFilePath(volPath string, id uint32) string { |
||||
|
return fmt.Sprintf("%s.snap.%d", volPath, id) |
||||
|
} |
||||
|
|
||||
|
// createDeltaFile creates a new snapshot delta file and returns an activeSnapshot.
|
||||
|
func createDeltaFile(path string, id uint32, vol *BlockVol, baseLSN uint64) (*activeSnapshot, error) { |
||||
|
totalBlocks := vol.super.VolumeSize / uint64(vol.super.BlockSize) |
||||
|
bitmap := NewSnapshotBitmap(totalBlocks) |
||||
|
bitmapSize := uint64(bitmap.ByteSize()) |
||||
|
|
||||
|
// Align DataOffset to BlockSize.
|
||||
|
dataOffset := uint64(SnapHeaderSize) + bitmapSize |
||||
|
rem := dataOffset % uint64(vol.super.BlockSize) |
||||
|
if rem != 0 { |
||||
|
dataOffset += uint64(vol.super.BlockSize) - rem |
||||
|
} |
||||
|
|
||||
|
hdr := SnapshotHeader{ |
||||
|
Version: SnapVersion, |
||||
|
SnapshotID: id, |
||||
|
BaseLSN: baseLSN, |
||||
|
VolumeSize: vol.super.VolumeSize, |
||||
|
BlockSize: vol.super.BlockSize, |
||||
|
BitmapSize: bitmapSize, |
||||
|
DataOffset: dataOffset, |
||||
|
CreatedAt: uint64(time.Now().Unix()), |
||||
|
ParentUUID: vol.super.UUID, |
||||
|
} |
||||
|
copy(hdr.Magic[:], SnapMagic) |
||||
|
|
||||
|
fd, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_EXCL, 0644) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("blockvol: create delta file: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Truncate to full size (sparse file -- only header+bitmap consume disk).
|
||||
|
totalSize := int64(dataOffset + vol.super.VolumeSize) |
||||
|
if err := fd.Truncate(totalSize); err != nil { |
||||
|
fd.Close() |
||||
|
os.Remove(path) |
||||
|
return nil, fmt.Errorf("blockvol: truncate delta: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Write header.
|
||||
|
if _, err := hdr.WriteTo(fd); err != nil { |
||||
|
fd.Close() |
||||
|
os.Remove(path) |
||||
|
return nil, fmt.Errorf("blockvol: write snapshot header: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Write zero bitmap.
|
||||
|
if err := bitmap.WriteTo(fd, SnapHeaderSize); err != nil { |
||||
|
fd.Close() |
||||
|
os.Remove(path) |
||||
|
return nil, fmt.Errorf("blockvol: write snapshot bitmap: %w", err) |
||||
|
} |
||||
|
|
||||
|
// Fsync delta file.
|
||||
|
if err := fd.Sync(); err != nil { |
||||
|
fd.Close() |
||||
|
os.Remove(path) |
||||
|
return nil, fmt.Errorf("blockvol: sync delta: %w", err) |
||||
|
} |
||||
|
|
||||
|
return &activeSnapshot{ |
||||
|
id: id, |
||||
|
fd: fd, |
||||
|
header: hdr, |
||||
|
bitmap: bitmap, |
||||
|
dataOffset: dataOffset, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// openDeltaFile opens an existing snapshot delta file, reads its header and bitmap.
|
||||
|
func openDeltaFile(path string) (*activeSnapshot, error) { |
||||
|
fd, err := os.OpenFile(path, os.O_RDWR, 0644) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("blockvol: open delta file: %w", err) |
||||
|
} |
||||
|
|
||||
|
hdr, err := ReadSnapshotHeader(fd) |
||||
|
if err != nil { |
||||
|
fd.Close() |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
totalBlocks := hdr.VolumeSize / uint64(hdr.BlockSize) |
||||
|
bitmap := NewSnapshotBitmap(totalBlocks) |
||||
|
if err := bitmap.ReadFrom(fd, SnapHeaderSize); err != nil { |
||||
|
fd.Close() |
||||
|
return nil, fmt.Errorf("blockvol: read snapshot bitmap: %w", err) |
||||
|
} |
||||
|
|
||||
|
return &activeSnapshot{ |
||||
|
id: hdr.SnapshotID, |
||||
|
fd: fd, |
||||
|
header: *hdr, |
||||
|
bitmap: bitmap, |
||||
|
dataOffset: hdr.DataOffset, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// SnapshotInfo contains read-only snapshot metadata for listing.
|
||||
|
type SnapshotInfo struct { |
||||
|
ID uint32 |
||||
|
BaseLSN uint64 |
||||
|
CreatedAt time.Time |
||||
|
CoWBlocks uint64 // number of CoW'd blocks (bitmap.CountSet())
|
||||
|
} |
||||
@ -0,0 +1,512 @@ |
|||||
|
package blockvol |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
"sync" |
||||
|
"testing" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
func TestSnapshots(t *testing.T) { |
||||
|
tests := []struct { |
||||
|
name string |
||||
|
run func(t *testing.T) |
||||
|
}{ |
||||
|
{name: "bitmap_get_set", run: testSnap_BitmapGetSet}, |
||||
|
{name: "bitmap_persist_reload", run: testSnap_BitmapPersistReload}, |
||||
|
{name: "header_roundtrip", run: testSnap_HeaderRoundtrip}, |
||||
|
{name: "create_and_read", run: testSnap_CreateAndRead}, |
||||
|
{name: "multiple_snapshots", run: testSnap_MultipleSnapshots}, |
||||
|
{name: "delete_removes_file", run: testSnap_DeleteRemovesFile}, |
||||
|
{name: "cow_only_touched_blocks", run: testSnap_CoWOnlyTouchedBlocks}, |
||||
|
{name: "during_active_writes", run: testSnap_DuringActiveWrites}, |
||||
|
{name: "survives_recovery", run: testSnap_SurvivesRecovery}, |
||||
|
{name: "restore_rewinds", run: testSnap_RestoreRewinds}, |
||||
|
} |
||||
|
for _, tt := range tests { |
||||
|
t.Run(tt.name, tt.run) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_BitmapGetSet(t *testing.T) { |
||||
|
bm := NewSnapshotBitmap(1024) |
||||
|
|
||||
|
// All bits start as zero.
|
||||
|
for i := uint64(0); i < 1024; i++ { |
||||
|
if bm.Get(i) { |
||||
|
t.Fatalf("bit %d should be 0", i) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Set some bits.
|
||||
|
bm.Set(0) |
||||
|
bm.Set(7) |
||||
|
bm.Set(8) |
||||
|
bm.Set(1023) |
||||
|
|
||||
|
if !bm.Get(0) { |
||||
|
t.Fatal("bit 0 not set") |
||||
|
} |
||||
|
if !bm.Get(7) { |
||||
|
t.Fatal("bit 7 not set") |
||||
|
} |
||||
|
if !bm.Get(8) { |
||||
|
t.Fatal("bit 8 not set") |
||||
|
} |
||||
|
if !bm.Get(1023) { |
||||
|
t.Fatal("bit 1023 not set") |
||||
|
} |
||||
|
if bm.Get(1) { |
||||
|
t.Fatal("bit 1 should not be set") |
||||
|
} |
||||
|
|
||||
|
if bm.CountSet() != 4 { |
||||
|
t.Fatalf("CountSet = %d, want 4", bm.CountSet()) |
||||
|
} |
||||
|
|
||||
|
// Out-of-range: Get returns false, Set is a no-op.
|
||||
|
if bm.Get(1024) { |
||||
|
t.Fatal("out-of-range Get should return false") |
||||
|
} |
||||
|
bm.Set(1024) // no panic
|
||||
|
|
||||
|
// ByteSize.
|
||||
|
if bm.ByteSize() != 128 { // 1024/8 = 128
|
||||
|
t.Fatalf("ByteSize = %d, want 128", bm.ByteSize()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_BitmapPersistReload(t *testing.T) { |
||||
|
dir := t.TempDir() |
||||
|
path := filepath.Join(dir, "bitmap.dat") |
||||
|
|
||||
|
bm := NewSnapshotBitmap(256) |
||||
|
bm.Set(0) |
||||
|
bm.Set(100) |
||||
|
bm.Set(255) |
||||
|
|
||||
|
// Write to file.
|
||||
|
fd, err := os.Create(path) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if err := bm.WriteTo(fd, 0); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
fd.Close() |
||||
|
|
||||
|
// Read back.
|
||||
|
fd2, err := os.Open(path) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
defer fd2.Close() |
||||
|
|
||||
|
bm2 := NewSnapshotBitmap(256) |
||||
|
if err := bm2.ReadFrom(fd2, 0); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
if !bm2.Get(0) || !bm2.Get(100) || !bm2.Get(255) { |
||||
|
t.Fatal("reloaded bitmap missing set bits") |
||||
|
} |
||||
|
if bm2.Get(1) || bm2.Get(99) || bm2.Get(254) { |
||||
|
t.Fatal("reloaded bitmap has spurious bits") |
||||
|
} |
||||
|
if bm2.CountSet() != 3 { |
||||
|
t.Fatalf("CountSet = %d, want 3", bm2.CountSet()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_HeaderRoundtrip(t *testing.T) { |
||||
|
hdr := SnapshotHeader{ |
||||
|
Version: SnapVersion, |
||||
|
SnapshotID: 42, |
||||
|
BaseLSN: 1000, |
||||
|
VolumeSize: 1 << 30, |
||||
|
BlockSize: 4096, |
||||
|
BitmapSize: 32768, |
||||
|
DataOffset: 36864, |
||||
|
CreatedAt: uint64(time.Now().Unix()), |
||||
|
} |
||||
|
copy(hdr.Magic[:], SnapMagic) |
||||
|
copy(hdr.ParentUUID[:], "0123456789abcdef") |
||||
|
|
||||
|
var buf bytes.Buffer |
||||
|
if _, err := hdr.WriteTo(&buf); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if buf.Len() != SnapHeaderSize { |
||||
|
t.Fatalf("header size = %d, want %d", buf.Len(), SnapHeaderSize) |
||||
|
} |
||||
|
|
||||
|
hdr2, err := ReadSnapshotHeader(&buf) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
if hdr2.SnapshotID != 42 { |
||||
|
t.Fatalf("SnapshotID = %d, want 42", hdr2.SnapshotID) |
||||
|
} |
||||
|
if hdr2.BaseLSN != 1000 { |
||||
|
t.Fatalf("BaseLSN = %d, want 1000", hdr2.BaseLSN) |
||||
|
} |
||||
|
if hdr2.VolumeSize != 1<<30 { |
||||
|
t.Fatalf("VolumeSize = %d, want %d", hdr2.VolumeSize, 1<<30) |
||||
|
} |
||||
|
if hdr2.BlockSize != 4096 { |
||||
|
t.Fatalf("BlockSize = %d, want 4096", hdr2.BlockSize) |
||||
|
} |
||||
|
if hdr2.BitmapSize != 32768 { |
||||
|
t.Fatalf("BitmapSize = %d", hdr2.BitmapSize) |
||||
|
} |
||||
|
if hdr2.DataOffset != 36864 { |
||||
|
t.Fatalf("DataOffset = %d", hdr2.DataOffset) |
||||
|
} |
||||
|
if hdr2.ParentUUID != hdr.ParentUUID { |
||||
|
t.Fatalf("ParentUUID mismatch") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_CreateAndRead(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
// Write 'A' to LBA 0.
|
||||
|
if err := v.WriteLBA(0, makeBlock('A')); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if err := v.SyncCache(); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// Create snapshot 1.
|
||||
|
if err := v.CreateSnapshot(1); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// Write 'B' to LBA 0 (after snapshot).
|
||||
|
if err := v.WriteLBA(0, makeBlock('B')); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if err := v.SyncCache(); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
// Force flush so CoW happens.
|
||||
|
v.flusher.FlushOnce() |
||||
|
|
||||
|
// Live read should see 'B'.
|
||||
|
live, err := v.ReadLBA(0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if live[0] != 'B' { |
||||
|
t.Fatalf("live read: got %c, want B", live[0]) |
||||
|
} |
||||
|
|
||||
|
// Snapshot read should see 'A'.
|
||||
|
snapData, err := v.ReadSnapshot(1, 0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if snapData[0] != 'A' { |
||||
|
t.Fatalf("snapshot read: got %c, want A", snapData[0]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_MultipleSnapshots(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
// Write 'A' to LBA 5, snapshot S1.
|
||||
|
if err := v.WriteLBA(5, makeBlock('A')); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
v.SyncCache() |
||||
|
if err := v.CreateSnapshot(1); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// Write 'B' to LBA 5, snapshot S2.
|
||||
|
if err := v.WriteLBA(5, makeBlock('B')); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
v.SyncCache() |
||||
|
v.flusher.FlushOnce() // CoW 'A' to S1
|
||||
|
if err := v.CreateSnapshot(2); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// Write 'C' to LBA 5.
|
||||
|
if err := v.WriteLBA(5, makeBlock('C')); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
v.SyncCache() |
||||
|
v.flusher.FlushOnce() // CoW 'B' to S2, S1 already done
|
||||
|
|
||||
|
// S1 sees 'A', S2 sees 'B', live sees 'C'.
|
||||
|
s1, err := v.ReadSnapshot(1, 5, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if s1[0] != 'A' { |
||||
|
t.Fatalf("S1: got %c, want A", s1[0]) |
||||
|
} |
||||
|
|
||||
|
s2, err := v.ReadSnapshot(2, 5, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if s2[0] != 'B' { |
||||
|
t.Fatalf("S2: got %c, want B", s2[0]) |
||||
|
} |
||||
|
|
||||
|
live, err := v.ReadLBA(5, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if live[0] != 'C' { |
||||
|
t.Fatalf("live: got %c, want C", live[0]) |
||||
|
} |
||||
|
|
||||
|
// List should show 2 snapshots.
|
||||
|
infos := v.ListSnapshots() |
||||
|
if len(infos) != 2 { |
||||
|
t.Fatalf("ListSnapshots: got %d, want 2", len(infos)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_DeleteRemovesFile(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
v.WriteLBA(0, makeBlock('X')) |
||||
|
v.SyncCache() |
||||
|
v.CreateSnapshot(1) |
||||
|
|
||||
|
deltaPath := deltaFilePath(v.path, 1) |
||||
|
if _, err := os.Stat(deltaPath); err != nil { |
||||
|
t.Fatalf("delta file should exist: %v", err) |
||||
|
} |
||||
|
|
||||
|
if err := v.DeleteSnapshot(1); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
if _, err := os.Stat(deltaPath); !os.IsNotExist(err) { |
||||
|
t.Fatalf("delta file should be removed, got err: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Reading deleted snapshot returns error.
|
||||
|
if _, err := v.ReadSnapshot(1, 0, 4096); err != ErrSnapshotNotFound { |
||||
|
t.Fatalf("expected ErrSnapshotNotFound, got %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(v.ListSnapshots()) != 0 { |
||||
|
t.Fatal("ListSnapshots should be empty") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_CoWOnlyTouchedBlocks(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
// Write LBAs 0, 1, 2.
|
||||
|
for i := uint64(0); i < 3; i++ { |
||||
|
v.WriteLBA(i, makeBlock(byte('A'+i))) |
||||
|
} |
||||
|
v.SyncCache() |
||||
|
v.CreateSnapshot(1) |
||||
|
|
||||
|
// Only modify LBA 1.
|
||||
|
v.WriteLBA(1, makeBlock('Z')) |
||||
|
v.SyncCache() |
||||
|
v.flusher.FlushOnce() |
||||
|
|
||||
|
// Check bitmap: only LBA 1 should be CoW'd.
|
||||
|
v.snapMu.RLock() |
||||
|
snap := v.snapshots[1] |
||||
|
cowCount := snap.bitmap.CountSet() |
||||
|
v.snapMu.RUnlock() |
||||
|
|
||||
|
if cowCount != 1 { |
||||
|
t.Fatalf("CoW count = %d, want 1", cowCount) |
||||
|
} |
||||
|
|
||||
|
// Snapshot should still see original values.
|
||||
|
for i := uint64(0); i < 3; i++ { |
||||
|
data, err := v.ReadSnapshot(1, i, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
expected := byte('A' + i) |
||||
|
if data[0] != expected { |
||||
|
t.Fatalf("LBA %d: got %c, want %c", i, data[0], expected) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_DuringActiveWrites(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
// Write initial data.
|
||||
|
v.WriteLBA(0, makeBlock('A')) |
||||
|
v.SyncCache() |
||||
|
|
||||
|
// Start concurrent writes and create snapshot.
|
||||
|
var wg sync.WaitGroup |
||||
|
errCh := make(chan error, 20) |
||||
|
|
||||
|
// Writer goroutine: continuously write to LBA 10.
|
||||
|
wg.Add(1) |
||||
|
go func() { |
||||
|
defer wg.Done() |
||||
|
for i := 0; i < 10; i++ { |
||||
|
if err := v.WriteLBA(10, makeBlock(byte('0'+i))); err != nil { |
||||
|
errCh <- err |
||||
|
return |
||||
|
} |
||||
|
time.Sleep(1 * time.Millisecond) |
||||
|
} |
||||
|
}() |
||||
|
|
||||
|
// Snapshot creation mid-writes.
|
||||
|
time.Sleep(5 * time.Millisecond) |
||||
|
if err := v.CreateSnapshot(1); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
wg.Wait() |
||||
|
close(errCh) |
||||
|
for err := range errCh { |
||||
|
t.Fatalf("concurrent write error: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Snapshot should exist and be readable.
|
||||
|
_, err := v.ReadSnapshot(1, 0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_SurvivesRecovery(t *testing.T) { |
||||
|
dir := t.TempDir() |
||||
|
path := filepath.Join(dir, "test.blockvol") |
||||
|
|
||||
|
// Create volume, write, snapshot.
|
||||
|
v, err := CreateBlockVol(path, CreateOptions{ |
||||
|
VolumeSize: 1 * 1024 * 1024, |
||||
|
BlockSize: 4096, |
||||
|
WALSize: 256 * 1024, |
||||
|
}) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
v.WriteLBA(0, makeBlock('A')) |
||||
|
v.SyncCache() |
||||
|
v.CreateSnapshot(1) |
||||
|
|
||||
|
// Write new data after snapshot.
|
||||
|
v.WriteLBA(0, makeBlock('B')) |
||||
|
v.SyncCache() |
||||
|
v.flusher.FlushOnce() |
||||
|
|
||||
|
// Close and reopen.
|
||||
|
v.Close() |
||||
|
|
||||
|
v2, err := OpenBlockVol(path) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
defer v2.Close() |
||||
|
|
||||
|
// Snapshot should survive.
|
||||
|
if len(v2.ListSnapshots()) != 1 { |
||||
|
t.Fatalf("expected 1 snapshot after recovery, got %d", len(v2.ListSnapshots())) |
||||
|
} |
||||
|
|
||||
|
// Snapshot data should read 'A'.
|
||||
|
snapData, err := v2.ReadSnapshot(1, 0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if snapData[0] != 'A' { |
||||
|
t.Fatalf("snapshot after recovery: got %c, want A", snapData[0]) |
||||
|
} |
||||
|
|
||||
|
// Live data should read 'B'.
|
||||
|
live, err := v2.ReadLBA(0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if live[0] != 'B' { |
||||
|
t.Fatalf("live after recovery: got %c, want B", live[0]) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func testSnap_RestoreRewinds(t *testing.T) { |
||||
|
v := createTestVol(t) |
||||
|
defer v.Close() |
||||
|
|
||||
|
// Write 'A' to LBA 0 and 'X' to LBA 1.
|
||||
|
v.WriteLBA(0, makeBlock('A')) |
||||
|
v.WriteLBA(1, makeBlock('X')) |
||||
|
v.SyncCache() |
||||
|
v.CreateSnapshot(1) |
||||
|
|
||||
|
// Write 'B' to LBA 0 (overwrites 'A').
|
||||
|
v.WriteLBA(0, makeBlock('B')) |
||||
|
v.SyncCache() |
||||
|
v.flusher.FlushOnce() |
||||
|
|
||||
|
// Live should be 'B'.
|
||||
|
live, _ := v.ReadLBA(0, 4096) |
||||
|
if live[0] != 'B' { |
||||
|
t.Fatalf("pre-restore live: got %c, want B", live[0]) |
||||
|
} |
||||
|
|
||||
|
// Restore snapshot 1.
|
||||
|
if err := v.RestoreSnapshot(1); err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
|
||||
|
// Live should now be 'A' (reverted).
|
||||
|
live2, err := v.ReadLBA(0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if live2[0] != 'A' { |
||||
|
t.Fatalf("post-restore LBA 0: got %c, want A", live2[0]) |
||||
|
} |
||||
|
|
||||
|
// LBA 1 should still be 'X' (unchanged, not CoW'd).
|
||||
|
live3, err := v.ReadLBA(1, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if live3[0] != 'X' { |
||||
|
t.Fatalf("post-restore LBA 1: got %c, want X", live3[0]) |
||||
|
} |
||||
|
|
||||
|
// All snapshots should be gone.
|
||||
|
if len(v.ListSnapshots()) != 0 { |
||||
|
t.Fatalf("snapshots should be empty after restore, got %d", len(v.ListSnapshots())) |
||||
|
} |
||||
|
|
||||
|
// Volume should still be writable.
|
||||
|
if err := v.WriteLBA(0, makeBlock('C')); err != nil { |
||||
|
t.Fatalf("write after restore: %v", err) |
||||
|
} |
||||
|
v.SyncCache() |
||||
|
data, err := v.ReadLBA(0, 4096) |
||||
|
if err != nil { |
||||
|
t.Fatal(err) |
||||
|
} |
||||
|
if data[0] != 'C' { |
||||
|
t.Fatalf("read after restore write: got %c, want C", data[0]) |
||||
|
} |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue