mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
- qa_chap_test.go: 16 tests (empty secret, replay, missing fields, hex cases) - qa_resize_test.go: 12 tests (concurrent, reopen, replica reject, alignment) - qa_snapshot_test.go: 21 tests (concurrent create, CoW, recovery, role checks) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
718 lines
18 KiB
Go
718 lines
18 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestQASnapshot runs adversarial tests for CP5-2 CoW snapshots.
|
|
func TestQASnapshot(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(t *testing.T)
|
|
}{
|
|
// Group A: Race conditions and concurrency
|
|
{name: "concurrent_create_same_id", run: testQASnap_ConcurrentCreateSameID},
|
|
{name: "concurrent_create_different_ids", run: testQASnap_ConcurrentCreateDifferentIDs},
|
|
{name: "delete_during_flush_cow", run: testQASnap_DeleteDuringFlushCoW},
|
|
{name: "concurrent_read_snapshot_during_cow", run: testQASnap_ConcurrentReadDuringCoW},
|
|
{name: "create_snapshot_during_close", run: testQASnap_CreateDuringClose},
|
|
|
|
// Group B: Role and state rejection
|
|
{name: "create_on_replica_rejected", run: testQASnap_CreateOnReplicaRejected},
|
|
{name: "create_on_stale_rejected", run: testQASnap_CreateOnStaleRejected},
|
|
{name: "create_on_rebuilding_rejected", run: testQASnap_CreateOnRebuildingRejected},
|
|
{name: "create_duplicate_id_rejected", run: testQASnap_CreateDuplicateIDRejected},
|
|
|
|
// Group C: Edge cases
|
|
{name: "read_unmodified_block_returns_zeros", run: testQASnap_ReadUnmodifiedBlockZeros},
|
|
{name: "cow_trim_block_preserves_old_data", run: testQASnap_CoWTrimBlockPreservesOldData},
|
|
{name: "delete_nonexistent_snapshot", run: testQASnap_DeleteNonexistent},
|
|
{name: "read_nonexistent_snapshot", run: testQASnap_ReadNonexistent},
|
|
{name: "restore_nonexistent_snapshot", run: testQASnap_RestoreNonexistent},
|
|
{name: "snapshot_at_boundary_lba", run: testQASnap_BoundaryLBA},
|
|
|
|
// Group D: Stress and lifecycle
|
|
{name: "create_delete_many_snapshots", run: testQASnap_CreateDeleteMany},
|
|
{name: "restore_then_new_snapshot", run: testQASnap_RestoreThenNewSnapshot},
|
|
{name: "multiple_flush_cycles_cow_idempotent", run: testQASnap_MultiFlushCoWIdempotent},
|
|
{name: "snapshot_recovery_after_post_cow_writes", run: testQASnap_RecoveryAfterPostCoWWrites},
|
|
|
|
// Group E: Restore correctness
|
|
{name: "restore_with_multiple_snapshots", run: testQASnap_RestoreWithMultiple},
|
|
{name: "restore_volume_writable_after", run: testQASnap_RestoreWritableAfter},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, tt.run)
|
|
}
|
|
}
|
|
|
|
// --- Group A: Race conditions and concurrency ---
|
|
|
|
// testQASnap_ConcurrentCreateSameID: two goroutines race to create
|
|
// the same snapshot ID. Exactly one must succeed, the other must get
|
|
// ErrSnapshotExists (or O_EXCL file error). No double-registration.
|
|
func testQASnap_ConcurrentCreateSameID(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
|
|
var wg sync.WaitGroup
|
|
var successes atomic.Int32
|
|
var failures atomic.Int32
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
err := v.CreateSnapshot(42)
|
|
if err == nil {
|
|
successes.Add(1)
|
|
} else {
|
|
failures.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
// Exactly one should succeed.
|
|
if s := successes.Load(); s != 1 {
|
|
t.Fatalf("expected exactly 1 success, got %d", s)
|
|
}
|
|
|
|
// Snapshot map should have exactly one entry.
|
|
v.snapMu.RLock()
|
|
n := len(v.snapshots)
|
|
v.snapMu.RUnlock()
|
|
if n != 1 {
|
|
t.Fatalf("expected 1 snapshot in map, got %d", n)
|
|
}
|
|
|
|
// Delta file should exist exactly once.
|
|
deltaPath := deltaFilePath(v.path, 42)
|
|
if _, err := os.Stat(deltaPath); err != nil {
|
|
t.Fatalf("delta file missing: %v", err)
|
|
}
|
|
}
|
|
|
|
// testQASnap_ConcurrentCreateDifferentIDs: multiple goroutines create
|
|
// different snapshot IDs concurrently. All should succeed.
|
|
func testQASnap_ConcurrentCreateDifferentIDs(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
|
|
const n = 5
|
|
var wg sync.WaitGroup
|
|
errs := make([]error, n)
|
|
for i := 0; i < n; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
errs[id] = v.CreateSnapshot(uint32(id + 1))
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
for i, err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("snapshot %d failed: %v", i+1, err)
|
|
}
|
|
}
|
|
|
|
infos := v.ListSnapshots()
|
|
if len(infos) != n {
|
|
t.Fatalf("expected %d snapshots, got %d", n, len(infos))
|
|
}
|
|
}
|
|
|
|
// testQASnap_DeleteDuringFlushCoW: tries to trigger the race where
|
|
// the flusher copies snapshots into a local slice, then DeleteSnapshot
|
|
// closes the fd while flusher is still using it.
|
|
func testQASnap_DeleteDuringFlushCoW(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Write initial data and create snapshot.
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
if err := v.CreateSnapshot(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write new data to trigger CoW on next flush.
|
|
v.WriteLBA(0, makeBlock('B'))
|
|
v.SyncCache()
|
|
|
|
// Race: flush (which does CoW) vs delete.
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
v.flusher.FlushOnce()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// Small delay to let flush start.
|
|
time.Sleep(100 * time.Microsecond)
|
|
v.DeleteSnapshot(1)
|
|
}()
|
|
wg.Wait()
|
|
|
|
// Volume should still be functional (no panic, no EBADF crash).
|
|
if err := v.WriteLBA(0, makeBlock('C')); err != nil {
|
|
t.Fatalf("write after race: %v", err)
|
|
}
|
|
}
|
|
|
|
// testQASnap_ConcurrentReadDuringCoW: reads from a snapshot while the
|
|
// flusher is actively performing CoW on other blocks.
|
|
func testQASnap_ConcurrentReadDuringCoW(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Write blocks 0-9.
|
|
for i := uint64(0); i < 10; i++ {
|
|
v.WriteLBA(i, makeBlock(byte('A'+i)))
|
|
}
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
// Modify blocks 5-9 to trigger CoW.
|
|
for i := uint64(5); i < 10; i++ {
|
|
v.WriteLBA(i, makeBlock('Z'))
|
|
}
|
|
v.SyncCache()
|
|
|
|
// Race: flush (CoW) vs snapshot read.
|
|
var wg sync.WaitGroup
|
|
var readErr error
|
|
var readData []byte
|
|
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
v.flusher.FlushOnce()
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
// Read block 0 from snapshot (not being CoW'd).
|
|
readData, readErr = v.ReadSnapshot(1, 0, 4096)
|
|
}()
|
|
wg.Wait()
|
|
|
|
if readErr != nil {
|
|
t.Fatalf("snapshot read during CoW: %v", readErr)
|
|
}
|
|
if readData[0] != 'A' {
|
|
t.Fatalf("snapshot read: got %c, want A", readData[0])
|
|
}
|
|
}
|
|
|
|
// testQASnap_CreateDuringClose: CreateSnapshot should fail cleanly on
|
|
// a closed volume, not panic or deadlock.
|
|
func testQASnap_CreateDuringClose(t *testing.T) {
|
|
v := createTestVol(t)
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
|
|
// Close the volume.
|
|
v.Close()
|
|
|
|
// Creating a snapshot on a closed volume should return error.
|
|
err := v.CreateSnapshot(1)
|
|
if err == nil {
|
|
t.Fatal("expected error creating snapshot on closed volume")
|
|
}
|
|
}
|
|
|
|
// --- Group B: Role and state rejection ---
|
|
|
|
func testQASnap_CreateOnReplicaRejected(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Transition: None -> Replica
|
|
v.SetRole(RoleReplica)
|
|
|
|
err := v.CreateSnapshot(1)
|
|
if err != ErrSnapshotRoleReject {
|
|
t.Fatalf("expected ErrSnapshotRoleReject, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testQASnap_CreateOnStaleRejected(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// None -> Replica -> (need to get to Stale)
|
|
// Stale is only reachable from Draining. Use role.Store directly for test.
|
|
v.role.Store(uint32(RoleStale))
|
|
|
|
err := v.CreateSnapshot(1)
|
|
if err != ErrSnapshotRoleReject {
|
|
t.Fatalf("expected ErrSnapshotRoleReject, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testQASnap_CreateOnRebuildingRejected(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Force to Rebuilding for test.
|
|
v.role.Store(uint32(RoleRebuilding))
|
|
|
|
err := v.CreateSnapshot(1)
|
|
if err != ErrSnapshotRoleReject {
|
|
t.Fatalf("expected ErrSnapshotRoleReject, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testQASnap_CreateDuplicateIDRejected(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
|
|
if err := v.CreateSnapshot(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
err := v.CreateSnapshot(1)
|
|
if err != ErrSnapshotExists {
|
|
t.Fatalf("expected ErrSnapshotExists, got %v", err)
|
|
}
|
|
}
|
|
|
|
// --- Group C: Edge cases ---
|
|
|
|
// testQASnap_ReadUnmodifiedBlockZeros: reading a block from a snapshot
|
|
// that was never written to should return zeros (from extent).
|
|
func testQASnap_ReadUnmodifiedBlockZeros(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Create snapshot without writing anything.
|
|
if err := v.CreateSnapshot(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Read block 100 (never written).
|
|
data, err := v.ReadSnapshot(1, 100, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for i, b := range data {
|
|
if b != 0 {
|
|
t.Fatalf("byte %d: got %d, want 0", i, b)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testQASnap_CoWTrimBlockPreservesOldData: snapshot preserves data that
|
|
// was present before a TRIM operation.
|
|
func testQASnap_CoWTrimBlockPreservesOldData(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Write 'X' to LBA 3.
|
|
v.WriteLBA(3, makeBlock('X'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce() // flush to extent
|
|
|
|
// Snapshot captures 'X' at LBA 3.
|
|
if err := v.CreateSnapshot(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// TRIM LBA 3 (zeros it).
|
|
v.Trim(3, 4096)
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce() // CoW 'X' to delta, then zero extent
|
|
|
|
// Live read should be zeros.
|
|
live, err := v.ReadLBA(3, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if live[0] != 0 {
|
|
t.Fatalf("live after TRIM: got %d, want 0", live[0])
|
|
}
|
|
|
|
// Snapshot should still see 'X'.
|
|
snap, err := v.ReadSnapshot(1, 3, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if snap[0] != 'X' {
|
|
t.Fatalf("snapshot after TRIM: got %c, want X", snap[0])
|
|
}
|
|
}
|
|
|
|
func testQASnap_DeleteNonexistent(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
err := v.DeleteSnapshot(999)
|
|
if err != ErrSnapshotNotFound {
|
|
t.Fatalf("expected ErrSnapshotNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testQASnap_ReadNonexistent(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
_, err := v.ReadSnapshot(999, 0, 4096)
|
|
if err != ErrSnapshotNotFound {
|
|
t.Fatalf("expected ErrSnapshotNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
func testQASnap_RestoreNonexistent(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
err := v.RestoreSnapshot(999)
|
|
if err != ErrSnapshotNotFound {
|
|
t.Fatalf("expected ErrSnapshotNotFound, got %v", err)
|
|
}
|
|
}
|
|
|
|
// testQASnap_BoundaryLBA: test CoW at LBA 0 and the last valid LBA.
|
|
func testQASnap_BoundaryLBA(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// Volume is 1MB = 256 blocks (LBA 0..255).
|
|
lastLBA := uint64(255)
|
|
|
|
v.WriteLBA(0, makeBlock('F'))
|
|
v.WriteLBA(lastLBA, makeBlock('L'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
// Overwrite both boundaries.
|
|
v.WriteLBA(0, makeBlock('f'))
|
|
v.WriteLBA(lastLBA, makeBlock('l'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Snapshot should see originals.
|
|
s0, _ := v.ReadSnapshot(1, 0, 4096)
|
|
sL, _ := v.ReadSnapshot(1, lastLBA, 4096)
|
|
if s0[0] != 'F' {
|
|
t.Fatalf("LBA 0: got %c, want F", s0[0])
|
|
}
|
|
if sL[0] != 'L' {
|
|
t.Fatalf("LBA %d: got %c, want L", lastLBA, sL[0])
|
|
}
|
|
}
|
|
|
|
// --- Group D: Stress and lifecycle ---
|
|
|
|
// testQASnap_CreateDeleteMany: create and delete many snapshots to
|
|
// stress the lifecycle. No leaks, no stale entries.
|
|
func testQASnap_CreateDeleteMany(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
|
|
for i := uint32(1); i <= 20; i++ {
|
|
if err := v.CreateSnapshot(i); err != nil {
|
|
t.Fatalf("create snapshot %d: %v", i, err)
|
|
}
|
|
// Write after each snapshot to force CoW.
|
|
v.WriteLBA(0, makeBlock(byte('A'+i%26)))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
}
|
|
|
|
if len(v.ListSnapshots()) != 20 {
|
|
t.Fatalf("expected 20 snapshots, got %d", len(v.ListSnapshots()))
|
|
}
|
|
|
|
// Delete odd-numbered snapshots.
|
|
for i := uint32(1); i <= 20; i += 2 {
|
|
if err := v.DeleteSnapshot(i); err != nil {
|
|
t.Fatalf("delete snapshot %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
if len(v.ListSnapshots()) != 10 {
|
|
t.Fatalf("expected 10 snapshots after deletion, got %d", len(v.ListSnapshots()))
|
|
}
|
|
|
|
// Verify delta files of deleted snapshots are gone.
|
|
for i := uint32(1); i <= 20; i += 2 {
|
|
p := deltaFilePath(v.path, i)
|
|
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
|
t.Fatalf("delta file for snapshot %d still exists", i)
|
|
}
|
|
}
|
|
|
|
// Verify remaining snapshots are readable.
|
|
for i := uint32(2); i <= 20; i += 2 {
|
|
if _, err := v.ReadSnapshot(i, 0, 4096); err != nil {
|
|
t.Fatalf("read snapshot %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testQASnap_RestoreThenNewSnapshot: after restore, creating a new
|
|
// snapshot should work cleanly (no leftover state).
|
|
func testQASnap_RestoreThenNewSnapshot(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
v.WriteLBA(0, makeBlock('B'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Restore back to 'A'.
|
|
v.RestoreSnapshot(1)
|
|
|
|
// All snapshots gone.
|
|
if len(v.ListSnapshots()) != 0 {
|
|
t.Fatal("snapshots should be empty after restore")
|
|
}
|
|
|
|
// Create a new snapshot on the restored state.
|
|
if err := v.CreateSnapshot(10); err != nil {
|
|
t.Fatalf("create after restore: %v", err)
|
|
}
|
|
|
|
// Write new data.
|
|
v.WriteLBA(0, makeBlock('C'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// New snapshot should see 'A' (the restored state).
|
|
data, err := v.ReadSnapshot(10, 0, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data[0] != 'A' {
|
|
t.Fatalf("snapshot after restore: got %c, want A", data[0])
|
|
}
|
|
|
|
// Live should see 'C'.
|
|
live, _ := v.ReadLBA(0, 4096)
|
|
if live[0] != 'C' {
|
|
t.Fatalf("live after restore+write: got %c, want C", live[0])
|
|
}
|
|
}
|
|
|
|
// testQASnap_MultiFlushCoWIdempotent: flushing multiple times after a
|
|
// snapshot should only CoW each block once (bitmap prevents double CoW).
|
|
func testQASnap_MultiFlushCoWIdempotent(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
// Write 'B', flush (CoW 'A' to delta).
|
|
v.WriteLBA(0, makeBlock('B'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Write 'C', flush (should NOT re-CoW -- bitmap[0] already set).
|
|
v.WriteLBA(0, makeBlock('C'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Write 'D', flush again.
|
|
v.WriteLBA(0, makeBlock('D'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Snapshot should still see 'A' (the first CoW), not 'B' or 'C'.
|
|
data, err := v.ReadSnapshot(1, 0, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data[0] != 'A' {
|
|
t.Fatalf("snapshot after multi-flush: got %c, want A", data[0])
|
|
}
|
|
|
|
// Bitmap should have exactly 1 bit set.
|
|
v.snapMu.RLock()
|
|
cowCount := v.snapshots[1].bitmap.CountSet()
|
|
v.snapMu.RUnlock()
|
|
if cowCount != 1 {
|
|
t.Fatalf("CoW count = %d, want 1 (idempotent)", cowCount)
|
|
}
|
|
}
|
|
|
|
// testQASnap_RecoveryAfterPostCoWWrites: create snapshot, do CoW writes,
|
|
// close, reopen. Verify snapshot data and live data are both correct.
|
|
func testQASnap_RecoveryAfterPostCoWWrites(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "test.blockvol")
|
|
|
|
v, err := CreateBlockVol(path, CreateOptions{
|
|
VolumeSize: 1 * 1024 * 1024,
|
|
BlockSize: 4096,
|
|
WALSize: 256 * 1024,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Write multiple blocks.
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.WriteLBA(1, makeBlock('B'))
|
|
v.WriteLBA(2, makeBlock('C'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
// Overwrite all three blocks.
|
|
v.WriteLBA(0, makeBlock('X'))
|
|
v.WriteLBA(1, makeBlock('Y'))
|
|
v.WriteLBA(2, makeBlock('Z'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Write even more data.
|
|
v.WriteLBA(0, makeBlock('1'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
v.Close()
|
|
|
|
// Reopen.
|
|
v2, err := OpenBlockVol(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer v2.Close()
|
|
|
|
// Snapshot should see original A, B, C.
|
|
for i, expected := range []byte{'A', 'B', 'C'} {
|
|
data, err := v2.ReadSnapshot(1, uint64(i), 4096)
|
|
if err != nil {
|
|
t.Fatalf("LBA %d: %v", i, err)
|
|
}
|
|
if data[0] != expected {
|
|
t.Fatalf("LBA %d snapshot: got %c, want %c", i, data[0], expected)
|
|
}
|
|
}
|
|
|
|
// Live should see '1', 'Y', 'Z'.
|
|
for i, expected := range []byte{'1', 'Y', 'Z'} {
|
|
data, err := v2.ReadLBA(uint64(i), 4096)
|
|
if err != nil {
|
|
t.Fatalf("LBA %d: %v", i, err)
|
|
}
|
|
if data[0] != expected {
|
|
t.Fatalf("LBA %d live: got %c, want %c", i, data[0], expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Group E: Restore correctness ---
|
|
|
|
// testQASnap_RestoreWithMultiple: with two snapshots S1 and S2, restoring
|
|
// S1 should revert to S1's state and remove both snapshots.
|
|
func testQASnap_RestoreWithMultiple(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
// A -> S1 -> B -> S2 -> C
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
v.WriteLBA(0, makeBlock('B'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
v.CreateSnapshot(2)
|
|
|
|
v.WriteLBA(0, makeBlock('C'))
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
// Restore S1 (should revert to 'A').
|
|
if err := v.RestoreSnapshot(1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
data, _ := v.ReadLBA(0, 4096)
|
|
if data[0] != 'A' {
|
|
t.Fatalf("after restore S1: got %c, want A", data[0])
|
|
}
|
|
|
|
// Both snapshots should be removed.
|
|
if len(v.ListSnapshots()) != 0 {
|
|
t.Fatal("all snapshots should be removed after restore")
|
|
}
|
|
|
|
// Delta files for both should be gone.
|
|
for _, id := range []uint32{1, 2} {
|
|
p := deltaFilePath(v.path, id)
|
|
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
|
t.Fatalf("delta file for snapshot %d still exists", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// testQASnap_RestoreWritableAfter: after restore, verify the volume can
|
|
// handle a full write-read-flush cycle and nextLSN is correct.
|
|
func testQASnap_RestoreWritableAfter(t *testing.T) {
|
|
v := createTestVol(t)
|
|
defer v.Close()
|
|
|
|
v.WriteLBA(0, makeBlock('A'))
|
|
v.SyncCache()
|
|
v.CreateSnapshot(1)
|
|
|
|
// Many writes to advance LSN.
|
|
for i := 0; i < 50; i++ {
|
|
v.WriteLBA(0, makeBlock(byte('0'+i%10)))
|
|
}
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
v.RestoreSnapshot(1)
|
|
|
|
// nextLSN should be baseLSN+1 (low number, reset).
|
|
currentLSN := v.nextLSN.Load()
|
|
if currentLSN > 10 {
|
|
t.Fatalf("nextLSN after restore too high: %d (expected reset to ~baseLSN+1)", currentLSN)
|
|
}
|
|
|
|
// Write-read cycle should work.
|
|
if err := v.WriteLBA(0, makeBlock('W')); err != nil {
|
|
t.Fatalf("write after restore: %v", err)
|
|
}
|
|
v.SyncCache()
|
|
v.flusher.FlushOnce()
|
|
|
|
data, err := v.ReadLBA(0, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if data[0] != 'W' {
|
|
t.Fatalf("read after restore write: got %c, want W", data[0])
|
|
}
|
|
}
|