mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 13:30:46 +02:00
CP10-3 Tier 1 optimizations (T1-T4): - TCP_NODELAY + 256KB socket buffers on NVMe/TCP connections - Response batching: all C2H data chunks + CapsuleResp in single flush - Tiered buffer pool (4KB/64KB/256KB sync.Pool) for write payloads - Configurable MaxH2CDataLength wiring through controller/IC/chunking BUG-CP103-1: NVMe write retry with jittered backoff for transient WAL pressure - writeWithRetry() with bounded backoff [50/200/800ms] - throttleOnWALPressure() pre-write delay above 90% WAL usage - WALPressureProvider interface + NVMeAdapter.WALPressure() BUG-CP103-2: Volume-level WAL admission control - WALAdmission with counting semaphore (max concurrent writers) - Soft watermark (0.7): small delay to desynchronize herd - Hard watermark (0.9): block until flusher drains - Single-deadline budget shared across watermark wait + semaphore - Close-aware during both watermark and semaphore waits - Wired into BlockVol.WriteLBA() and Trim() Benchmark platform enhancements: - NVMe benchmark actions and scenarios (A/B, CW sweep, IOQ sweep) - Database benchmark actions (SQLite, pgbench) - K8s operator QA reconciler tests - New testrunner scenarios for HA, fault injection, CSI lifecycle Test counts: 213 NVMe + 625 engine + operator + testrunner tests, all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1099 lines
31 KiB
Go
1099 lines
31 KiB
Go
package blockvol
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"errors"
|
|
"net"
|
|
"path/filepath"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestQAPhase4ACP3 tests Phase 4A CP3 (promotion/demotion/rebuild) adversarially.
|
|
func TestQAPhase4ACP3(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
run func(t *testing.T)
|
|
}{
|
|
// --- HandleAssignment ---
|
|
{name: "assign_same_role_epoch_refresh", run: testAssignSameRoleEpochRefresh},
|
|
{name: "assign_same_role_epoch_no_downgrade", run: testAssignSameRoleEpochNoDowngrade},
|
|
{name: "assign_concurrent_serialized", run: testAssignConcurrentSerialized},
|
|
{name: "assign_promote_epoch_before_role", run: testAssignPromoteEpochBeforeRole},
|
|
{name: "assign_promote_write_uses_new_epoch", run: testAssignPromoteWriteUsesNewEpoch},
|
|
{name: "assign_demote_blocks_new_writes", run: testAssignDemoteBlocksNewWrites},
|
|
{name: "assign_demote_drains_inflight", run: testAssignDemoteDrainsInflight},
|
|
{name: "assign_demote_drain_timeout", run: testAssignDemoteDrainTimeout},
|
|
{name: "assign_demote_stops_shipper", run: testAssignDemoteStopsShipper},
|
|
{name: "assign_invalid_transitions", run: testAssignInvalidTransitions},
|
|
|
|
// --- ScanFrom ---
|
|
{name: "scan_empty_wal", run: testScanEmptyWAL},
|
|
{name: "scan_recycled_boundary", run: testScanRecycledBoundary},
|
|
{name: "scan_from_middle_lsn", run: testScanFromMiddleLSN},
|
|
{name: "scan_includes_trims", run: testScanIncludesTrims},
|
|
{name: "scan_skips_padding", run: testScanSkipsPadding},
|
|
{name: "scan_snapshot_isolation", run: testScanSnapshotIsolation},
|
|
|
|
// --- Rebuild Server ---
|
|
{name: "rbsrv_epoch_mismatch", run: testRbSrvEpochMismatch},
|
|
{name: "rbsrv_unknown_type", run: testRbSrvUnknownType},
|
|
{name: "rbsrv_wal_catchup_entries", run: testRbSrvWALCatchUpEntries},
|
|
{name: "rbsrv_wal_catchup_empty", run: testRbSrvWALCatchUpEmpty},
|
|
{name: "rbsrv_full_extent_size", run: testRbSrvFullExtentSize},
|
|
{name: "rbsrv_full_extent_has_data", run: testRbSrvFullExtentHasData},
|
|
{name: "rbsrv_stop_during_stream", run: testRbSrvStopDuringStream},
|
|
|
|
// --- Rebuild Client ---
|
|
{name: "rbcli_wrong_role_rejected", run: testRbCliWrongRole},
|
|
{name: "rbcli_catchup_data_integrity", run: testRbCliCatchUpDataIntegrity},
|
|
{name: "rbcli_wal_recycled_fallback", run: testRbCliWALRecycledFallback},
|
|
{name: "rbcli_full_extent_clears_dirty_map", run: testRbCliFullExtentClearsDirtyMap},
|
|
{name: "rbcli_full_extent_resets_wal", run: testRbCliFullExtentResetsWAL},
|
|
{name: "rbcli_full_extent_persists_superblock", run: testRbCliFullExtentPersistsSuperblock},
|
|
|
|
// --- applyRebuildEntry ---
|
|
{name: "apply_nextlsn_advanced", run: testApplyNextLSNAdvanced},
|
|
{name: "apply_out_of_order_accepted", run: testApplyOutOfOrderAccepted},
|
|
{name: "apply_dirty_map_updated", run: testApplyDirtyMapUpdated},
|
|
|
|
// --- Integration ---
|
|
{name: "lifecycle_promote_write_demote_rebuild", run: testLifecycleFullCycle},
|
|
{name: "dirty_map_clear_concurrent", run: testDirtyMapClearConcurrent},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tt.run(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func cp3Vol(t *testing.T, name string, walSize uint64) *BlockVol {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
cfg := DefaultConfig()
|
|
cfg.FlushInterval = 5 * time.Millisecond
|
|
cfg.WALFullTimeout = 200 * time.Millisecond
|
|
// Relax admission control for tiny test WALs: prevent watermark delays
|
|
// from changing flusher/rebuild timing on 64KB WALs.
|
|
cfg.WALSoftWatermark = 0.95
|
|
cfg.WALHardWatermark = 0.99
|
|
vol, err := CreateBlockVol(filepath.Join(dir, name), CreateOptions{
|
|
VolumeSize: 64 * 1024,
|
|
BlockSize: 4096,
|
|
WALSize: walSize,
|
|
}, cfg)
|
|
if err != nil {
|
|
t.Fatalf("CreateBlockVol(%s): %v", name, err)
|
|
}
|
|
return vol
|
|
}
|
|
|
|
func cp3Primary(t *testing.T, name string, epoch uint64) *BlockVol {
|
|
t.Helper()
|
|
vol := cp3Vol(t, name, 64*1024)
|
|
if err := vol.SetEpoch(epoch); err != nil {
|
|
t.Fatalf("SetEpoch: %v", err)
|
|
}
|
|
vol.SetMasterEpoch(epoch)
|
|
if err := vol.SetRole(RolePrimary); err != nil {
|
|
t.Fatalf("SetRole: %v", err)
|
|
}
|
|
vol.lease.Grant(30 * time.Second)
|
|
return vol
|
|
}
|
|
|
|
// cp3Rebuilding creates a vol in RoleRebuilding via valid state path.
|
|
func cp3Rebuilding(t *testing.T, name string, epoch uint64) *BlockVol {
|
|
t.Helper()
|
|
vol := cp3Vol(t, name, 64*1024)
|
|
// None -> Primary -> Stale -> Rebuilding
|
|
if err := vol.HandleAssignment(epoch, RolePrimary, 1*time.Second); err != nil {
|
|
t.Fatalf("-> Primary: %v", err)
|
|
}
|
|
if err := vol.HandleAssignment(epoch, RoleStale, 0); err != nil {
|
|
t.Fatalf("-> Stale: %v", err)
|
|
}
|
|
if err := vol.HandleAssignment(epoch, RoleRebuilding, 0); err != nil {
|
|
t.Fatalf("-> Rebuilding: %v", err)
|
|
}
|
|
return vol
|
|
}
|
|
|
|
// ========== HandleAssignment ==========
|
|
|
|
func testAssignSameRoleEpochRefresh(t *testing.T) {
|
|
// Reviewer finding #3 regression: same-role assignment must update epoch.
|
|
vol := cp3Primary(t, "epoch_refresh.bv", 1)
|
|
defer vol.Close()
|
|
|
|
if err := vol.HandleAssignment(5, RolePrimary, 30*time.Second); err != nil {
|
|
t.Fatalf("HandleAssignment: %v", err)
|
|
}
|
|
if vol.Epoch() != 5 {
|
|
t.Errorf("Epoch = %d, want 5 (same-role epoch not refreshed)", vol.Epoch())
|
|
}
|
|
if vol.masterEpoch.Load() != 5 {
|
|
t.Errorf("MasterEpoch = %d, want 5", vol.masterEpoch.Load())
|
|
}
|
|
// Lease should be refreshed.
|
|
if !vol.lease.IsValid() {
|
|
t.Error("lease not refreshed after same-role assignment")
|
|
}
|
|
}
|
|
|
|
func testAssignSameRoleEpochNoDowngrade(t *testing.T) {
|
|
// Epoch must not go backwards on same-role assignment — must be rejected.
|
|
vol := cp3Primary(t, "no_downgrade.bv", 5)
|
|
defer vol.Close()
|
|
|
|
// Send assignment with lower epoch — must return epoch regression error.
|
|
err := vol.HandleAssignment(3, RolePrimary, 30*time.Second)
|
|
if err == nil {
|
|
t.Fatalf("expected error for stale epoch, got nil")
|
|
}
|
|
if !errors.Is(err, ErrEpochRegression) {
|
|
t.Fatalf("expected ErrEpochRegression, got: %v", err)
|
|
}
|
|
if vol.Epoch() != 5 {
|
|
t.Errorf("Epoch = %d, want 5 (epoch should not downgrade from 5 to 3)", vol.Epoch())
|
|
}
|
|
}
|
|
|
|
func testAssignConcurrentSerialized(t *testing.T) {
|
|
vol := cp3Vol(t, "concurrent.bv", 32*1024)
|
|
defer vol.Close()
|
|
|
|
var wg sync.WaitGroup
|
|
errs := make([]error, 2)
|
|
|
|
wg.Add(2)
|
|
go func() {
|
|
defer wg.Done()
|
|
errs[0] = vol.HandleAssignment(1, RolePrimary, 10*time.Second)
|
|
}()
|
|
go func() {
|
|
defer wg.Done()
|
|
errs[1] = vol.HandleAssignment(1, RoleReplica, 10*time.Second)
|
|
}()
|
|
|
|
done := make(chan struct{})
|
|
go func() { wg.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("deadlock in concurrent HandleAssignment")
|
|
}
|
|
|
|
succeeded := 0
|
|
for _, e := range errs {
|
|
if e == nil {
|
|
succeeded++
|
|
}
|
|
}
|
|
if succeeded == 0 {
|
|
t.Errorf("both failed: %v, %v", errs[0], errs[1])
|
|
}
|
|
role := vol.Role()
|
|
if role != RolePrimary && role != RoleReplica {
|
|
t.Errorf("role = %s, want Primary or Replica", role)
|
|
}
|
|
}
|
|
|
|
func testAssignPromoteEpochBeforeRole(t *testing.T) {
|
|
// Epoch must be durable BEFORE role becomes Primary.
|
|
// After promote, both epoch and masterEpoch match, writes work.
|
|
vol := cp3Vol(t, "promote_order.bv", 32*1024)
|
|
defer vol.Close()
|
|
vol.SetEpoch(1)
|
|
vol.SetMasterEpoch(1)
|
|
vol.SetRole(RoleReplica)
|
|
|
|
if err := vol.HandleAssignment(5, RolePrimary, 10*time.Second); err != nil {
|
|
t.Fatalf("promote: %v", err)
|
|
}
|
|
if vol.Epoch() != 5 {
|
|
t.Errorf("epoch = %d after promote, want 5", vol.Epoch())
|
|
}
|
|
if vol.masterEpoch.Load() != 5 {
|
|
t.Errorf("masterEpoch = %d, want 5", vol.masterEpoch.Load())
|
|
}
|
|
// Writes must succeed with new epoch.
|
|
if err := vol.WriteLBA(0, makeBlock('P')); err != nil {
|
|
t.Errorf("WriteLBA after promote: %v", err)
|
|
}
|
|
}
|
|
|
|
func testAssignPromoteWriteUsesNewEpoch(t *testing.T) {
|
|
// After promote to epoch=5, WAL entries must carry epoch=5.
|
|
vol := cp3Vol(t, "epoch_wal.bv", 64*1024)
|
|
defer vol.Close()
|
|
vol.SetEpoch(1)
|
|
vol.SetMasterEpoch(1)
|
|
vol.SetRole(RoleReplica)
|
|
|
|
vol.HandleAssignment(5, RolePrimary, 10*time.Second)
|
|
vol.WriteLBA(0, makeBlock('E'))
|
|
|
|
// Scan the WAL for entries with the correct epoch.
|
|
var foundEpoch uint64
|
|
vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 1, func(e *WALEntry) error {
|
|
foundEpoch = e.Epoch
|
|
return nil
|
|
})
|
|
if foundEpoch != 5 {
|
|
t.Errorf("WAL entry epoch = %d, want 5 (write after promote should use new epoch)", foundEpoch)
|
|
}
|
|
}
|
|
|
|
func testAssignDemoteBlocksNewWrites(t *testing.T) {
|
|
// Demote: lease revoke must block writeGate before role transition.
|
|
vol := cp3Primary(t, "demote_block.bv", 1)
|
|
defer vol.Close()
|
|
|
|
vol.WriteLBA(0, makeBlock('B'))
|
|
vol.SyncCache()
|
|
|
|
vol.HandleAssignment(2, RoleStale, 0)
|
|
|
|
if vol.lease.IsValid() {
|
|
t.Error("lease still valid after demote")
|
|
}
|
|
if vol.Role() != RoleStale {
|
|
t.Errorf("role = %s, want Stale", vol.Role())
|
|
}
|
|
if err := vol.WriteLBA(0, makeBlock('X')); err == nil {
|
|
t.Error("WriteLBA succeeded after demote, want error")
|
|
}
|
|
// Reads must still work.
|
|
data, err := vol.ReadLBA(0, 4096)
|
|
if err != nil {
|
|
t.Fatalf("ReadLBA after demote: %v", err)
|
|
}
|
|
if data[0] != 'B' {
|
|
t.Errorf("data = %c, want 'B'", data[0])
|
|
}
|
|
}
|
|
|
|
func testAssignDemoteDrainsInflight(t *testing.T) {
|
|
// In-flight writes must complete before demote finishes.
|
|
vol := cp3Primary(t, "demote_drain.bv", 1)
|
|
defer vol.Close()
|
|
vol.drainTimeout = 2 * time.Second
|
|
|
|
// Start a slow "in-flight op".
|
|
vol.opsOutstanding.Add(1)
|
|
|
|
demoteDone := make(chan error, 1)
|
|
go func() {
|
|
demoteDone <- vol.HandleAssignment(2, RoleStale, 0)
|
|
}()
|
|
|
|
// Demote should be waiting for ops to drain.
|
|
select {
|
|
case <-demoteDone:
|
|
t.Fatal("demote returned immediately despite outstanding op")
|
|
case <-time.After(50 * time.Millisecond):
|
|
}
|
|
|
|
// Finish the op -- demote should complete.
|
|
vol.opsOutstanding.Add(-1)
|
|
|
|
select {
|
|
case err := <-demoteDone:
|
|
if err != nil {
|
|
t.Errorf("demote: %v", err)
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("demote hung after ops drained")
|
|
}
|
|
if vol.Role() != RoleStale {
|
|
t.Errorf("role = %s, want Stale", vol.Role())
|
|
}
|
|
}
|
|
|
|
func testAssignDemoteDrainTimeout(t *testing.T) {
|
|
vol := cp3Primary(t, "drain_to.bv", 1)
|
|
defer vol.Close()
|
|
vol.drainTimeout = 100 * time.Millisecond
|
|
|
|
vol.opsOutstanding.Add(1) // stuck op
|
|
start := time.Now()
|
|
err := vol.HandleAssignment(2, RoleStale, 0)
|
|
elapsed := time.Since(start)
|
|
|
|
if err == nil {
|
|
t.Error("demote should fail with drain timeout")
|
|
}
|
|
if elapsed > 1*time.Second {
|
|
t.Errorf("drain took %v, expected ~100ms", elapsed)
|
|
}
|
|
vol.opsOutstanding.Add(-1) // cleanup
|
|
}
|
|
|
|
func testAssignDemoteStopsShipper(t *testing.T) {
|
|
primary, replica := createReplicaPair(t)
|
|
defer replica.Close()
|
|
|
|
// Demote should stop the shipper.
|
|
primary.drainTimeout = 1 * time.Second
|
|
primary.HandleAssignment(2, RoleStale, 0)
|
|
|
|
// After demote, shipper should be stopped -- Ship is a no-op.
|
|
if primary.shipperGroup != nil && primary.shipperGroup.Len() > 0 {
|
|
shipper := primary.shipperGroup.Shipper(0)
|
|
entry := &WALEntry{LSN: 999, Epoch: 2, Type: EntryTypeWrite, LBA: 0, Length: 4096, Data: makeBlock('X')}
|
|
err := shipper.Ship(entry)
|
|
if err != nil {
|
|
t.Errorf("Ship after stop: %v (should silently drop)", err)
|
|
}
|
|
if shipper.ShippedLSN() == 999 {
|
|
t.Error("shipper advanced LSN after demote -- should be stopped")
|
|
}
|
|
}
|
|
primary.Close()
|
|
}
|
|
|
|
func testAssignInvalidTransitions(t *testing.T) {
|
|
vol := cp3Primary(t, "invalid.bv", 1)
|
|
defer vol.Close()
|
|
|
|
cases := []struct {
|
|
role Role
|
|
desc string
|
|
}{
|
|
{RoleReplica, "Primary -> Replica"},
|
|
{RoleRebuilding, "Primary -> Rebuilding"},
|
|
{RoleDraining, "Primary -> Draining"},
|
|
}
|
|
for _, c := range cases {
|
|
err := vol.HandleAssignment(2, c.role, 10*time.Second)
|
|
if err == nil {
|
|
t.Errorf("%s should fail", c.desc)
|
|
}
|
|
}
|
|
if vol.Role() != RolePrimary {
|
|
t.Errorf("role changed after failed assignment: %s", vol.Role())
|
|
}
|
|
}
|
|
|
|
// ========== ScanFrom ==========
|
|
|
|
func testScanEmptyWAL(t *testing.T) {
|
|
vol := cp3Vol(t, "scan_empty.bv", 32*1024)
|
|
defer vol.Close()
|
|
var count int
|
|
err := vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 1, func(e *WALEntry) error {
|
|
count++
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Errorf("ScanFrom empty: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Errorf("got %d entries, want 0", count)
|
|
}
|
|
}
|
|
|
|
func testScanRecycledBoundary(t *testing.T) {
|
|
vol := cp3Vol(t, "scan_recyc.bv", 32*1024)
|
|
defer vol.Close()
|
|
|
|
// fromLSN < checkpointLSN -> recycled
|
|
err := vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 10, 5, func(*WALEntry) error { return nil })
|
|
if err != ErrWALRecycled {
|
|
t.Errorf("fromLSN<checkpoint: got %v, want ErrWALRecycled", err)
|
|
}
|
|
// fromLSN == checkpointLSN -> recycled
|
|
err = vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 10, 10, func(*WALEntry) error { return nil })
|
|
if err != ErrWALRecycled {
|
|
t.Errorf("fromLSN==checkpoint: got %v, want ErrWALRecycled", err)
|
|
}
|
|
// fromLSN == checkpointLSN+1 -> NOT recycled
|
|
err = vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 10, 11, func(*WALEntry) error { return nil })
|
|
if err == ErrWALRecycled {
|
|
t.Error("fromLSN=checkpoint+1 should not be recycled")
|
|
}
|
|
// checkpointLSN==0, fromLSN==0 -> NOT recycled (fresh)
|
|
err = vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 0, func(*WALEntry) error { return nil })
|
|
if err == ErrWALRecycled {
|
|
t.Error("checkpoint=0, fromLSN=0 should not be recycled")
|
|
}
|
|
}
|
|
|
|
func testScanFromMiddleLSN(t *testing.T) {
|
|
// Write LSN 1-10, scan from LSN=6 -> only LSN 6-10 returned.
|
|
vol := cp3Primary(t, "scan_mid.bv", 1)
|
|
defer vol.Close()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock(byte('A'+i)))
|
|
}
|
|
|
|
var lsns []uint64
|
|
vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 6, func(e *WALEntry) error {
|
|
lsns = append(lsns, e.LSN)
|
|
return nil
|
|
})
|
|
|
|
if len(lsns) == 0 {
|
|
t.Fatal("no entries returned")
|
|
}
|
|
for _, lsn := range lsns {
|
|
if lsn < 6 {
|
|
t.Errorf("ScanFrom(fromLSN=6) returned LSN %d (< 6)", lsn)
|
|
}
|
|
}
|
|
// Should have LSNs 6-10 (5 entries).
|
|
if len(lsns) < 5 {
|
|
t.Errorf("got %d entries, want >= 5 (LSN 6-10)", len(lsns))
|
|
}
|
|
}
|
|
|
|
func testScanIncludesTrims(t *testing.T) {
|
|
// ScanFrom must return TRIM entries (line 279 checks EntryTypeTrim).
|
|
vol := cp3Primary(t, "scan_trim.bv", 1)
|
|
defer vol.Close()
|
|
|
|
vol.WriteLBA(0, makeBlock('W'))
|
|
vol.Trim(0, 4096) // TRIM entry
|
|
|
|
var types []byte
|
|
vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 1, func(e *WALEntry) error {
|
|
types = append(types, e.Type)
|
|
return nil
|
|
})
|
|
|
|
foundTrim := false
|
|
for _, typ := range types {
|
|
if typ == EntryTypeTrim {
|
|
foundTrim = true
|
|
}
|
|
}
|
|
if !foundTrim {
|
|
t.Errorf("ScanFrom returned types %v, want TRIM included", types)
|
|
}
|
|
}
|
|
|
|
func testScanSkipsPadding(t *testing.T) {
|
|
// Write enough entries to cause padding at WAL boundary, verify no padding leaks.
|
|
vol := cp3Vol(t, "scan_pad.bv", 64*1024)
|
|
defer vol.Close()
|
|
vol.SetEpoch(1)
|
|
vol.SetMasterEpoch(1)
|
|
vol.SetRole(RolePrimary)
|
|
vol.lease.Grant(30 * time.Second)
|
|
|
|
written := 0
|
|
for i := 0; i < 20; i++ {
|
|
if err := vol.WriteLBA(uint64(i%16), makeBlock(byte('0'+i%10))); err != nil {
|
|
break
|
|
}
|
|
written++
|
|
}
|
|
if written < 3 {
|
|
t.Fatalf("only wrote %d entries", written)
|
|
}
|
|
|
|
var scanned int
|
|
vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 1, func(e *WALEntry) error {
|
|
if e.Type == EntryTypePadding {
|
|
t.Error("padding entry leaked through ScanFrom")
|
|
}
|
|
scanned++
|
|
return nil
|
|
})
|
|
if scanned == 0 {
|
|
t.Error("no entries returned")
|
|
}
|
|
}
|
|
|
|
func testScanSnapshotIsolation(t *testing.T) {
|
|
// ScanFrom snapshots head/tail. Writes during scan don't affect results.
|
|
vol := cp3Primary(t, "scan_iso.bv", 1)
|
|
defer vol.Close()
|
|
|
|
// Write 3 entries.
|
|
for i := 0; i < 3; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock(byte('A'+i)))
|
|
}
|
|
|
|
// Scan -- callback writes more entries.
|
|
var scanCount int
|
|
vol.wal.ScanFrom(vol.fd, vol.super.WALOffset, 0, 1, func(e *WALEntry) error {
|
|
scanCount++
|
|
// Write during scan -- should NOT appear in this scan.
|
|
vol.WriteLBA(10, makeBlock('Z'))
|
|
return nil
|
|
})
|
|
|
|
// We wrote 3 entries before scan. The entries written DURING scan should
|
|
// not be seen (snapshot isolation). scanCount should be exactly 3.
|
|
if scanCount != 3 {
|
|
t.Errorf("scanCount = %d, want 3 (snapshot isolation: writes during scan should not appear)", scanCount)
|
|
}
|
|
}
|
|
|
|
// ========== Rebuild Server ==========
|
|
|
|
func testRbSrvEpochMismatch(t *testing.T) {
|
|
vol := cp3Primary(t, "rbs_epoch.bv", 5)
|
|
defer vol.Close()
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
|
|
// Stale epoch.
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildWALCatchUp, Epoch: 3}))
|
|
msgType, payload, _ := ReadFrame(conn)
|
|
if msgType != MsgRebuildError || string(payload) != "EPOCH_MISMATCH" {
|
|
t.Errorf("stale epoch: type=0x%x payload=%q, want EPOCH_MISMATCH", msgType, payload)
|
|
}
|
|
|
|
// Future epoch.
|
|
conn2, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn2.Close()
|
|
WriteFrame(conn2, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildWALCatchUp, Epoch: 99}))
|
|
msgType2, payload2, _ := ReadFrame(conn2)
|
|
if msgType2 != MsgRebuildError || string(payload2) != "EPOCH_MISMATCH" {
|
|
t.Errorf("future epoch: type=0x%x payload=%q, want EPOCH_MISMATCH", msgType2, payload2)
|
|
}
|
|
}
|
|
|
|
func testRbSrvUnknownType(t *testing.T) {
|
|
vol := cp3Primary(t, "rbs_unk.bv", 1)
|
|
defer vol.Close()
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: 0xFF, Epoch: 1}))
|
|
msgType, payload, _ := ReadFrame(conn)
|
|
if msgType != MsgRebuildError || string(payload) != "UNKNOWN_TYPE" {
|
|
t.Errorf("unknown type: type=0x%x payload=%q, want UNKNOWN_TYPE", msgType, payload)
|
|
}
|
|
}
|
|
|
|
func testRbSrvWALCatchUpEntries(t *testing.T) {
|
|
vol := cp3Primary(t, "rbs_catchup.bv", 1)
|
|
defer vol.Close()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock(byte('A'+i)))
|
|
}
|
|
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildWALCatchUp, FromLSN: 1, Epoch: 1}))
|
|
|
|
var entries int
|
|
var snapshotLSN uint64
|
|
for {
|
|
msgType, payload, err := ReadFrame(conn)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msgType == MsgRebuildEntry {
|
|
entries++
|
|
} else if msgType == MsgRebuildDone {
|
|
if len(payload) >= 8 {
|
|
snapshotLSN = binary.BigEndian.Uint64(payload[:8])
|
|
}
|
|
break
|
|
} else if msgType == MsgRebuildError {
|
|
t.Fatalf("error: %s", payload)
|
|
}
|
|
}
|
|
if entries < 5 {
|
|
t.Errorf("entries = %d, want >= 5", entries)
|
|
}
|
|
if snapshotLSN < 6 {
|
|
t.Errorf("snapshotLSN = %d, want >= 6", snapshotLSN)
|
|
}
|
|
}
|
|
|
|
func testRbSrvWALCatchUpEmpty(t *testing.T) {
|
|
// Empty WAL -> MsgRebuildDone with nextLSN=1 (no entries).
|
|
vol := cp3Primary(t, "rbs_empty.bv", 1)
|
|
defer vol.Close()
|
|
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildWALCatchUp, FromLSN: 1, Epoch: 1}))
|
|
|
|
msgType, payload, err := ReadFrame(conn)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msgType != MsgRebuildDone {
|
|
t.Errorf("expected MsgRebuildDone on empty WAL, got 0x%x", msgType)
|
|
}
|
|
if len(payload) >= 8 {
|
|
lsn := binary.BigEndian.Uint64(payload[:8])
|
|
if lsn == 0 {
|
|
t.Error("snapshotLSN = 0 on empty WAL, want >= 1 (nextLSN)")
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRbSrvFullExtentSize(t *testing.T) {
|
|
vol := cp3Primary(t, "rbs_ext_sz.bv", 1)
|
|
defer vol.Close()
|
|
|
|
for i := 0; i < 3; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock(byte('X'+i)))
|
|
}
|
|
vol.SyncCache()
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildFullExtent, Epoch: 1}))
|
|
|
|
var totalBytes uint64
|
|
for {
|
|
msgType, payload, err := ReadFrame(conn)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if msgType == MsgRebuildExtent {
|
|
totalBytes += uint64(len(payload))
|
|
} else if msgType == MsgRebuildDone {
|
|
break
|
|
} else if msgType == MsgRebuildError {
|
|
t.Fatalf("error: %s", payload)
|
|
}
|
|
}
|
|
if totalBytes != vol.super.VolumeSize {
|
|
t.Errorf("extent bytes = %d, want %d (VolumeSize)", totalBytes, vol.super.VolumeSize)
|
|
}
|
|
}
|
|
|
|
func testRbSrvFullExtentHasData(t *testing.T) {
|
|
// Write data, flush to extent, stream full extent, verify data present.
|
|
vol := cp3Primary(t, "rbs_ext_data.bv", 1)
|
|
defer vol.Close()
|
|
|
|
vol.WriteLBA(0, makeBlock('D'))
|
|
vol.SyncCache()
|
|
time.Sleep(30 * time.Millisecond) // let flusher write to extent
|
|
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
defer vol.StopRebuildServer()
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildFullExtent, Epoch: 1}))
|
|
|
|
var extentData []byte
|
|
for {
|
|
msgType, payload, _ := ReadFrame(conn)
|
|
if msgType == MsgRebuildExtent {
|
|
extentData = append(extentData, payload...)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// First 4KB of extent = LBA 0 data.
|
|
if len(extentData) < 4096 {
|
|
t.Fatalf("extent data too short: %d", len(extentData))
|
|
}
|
|
if extentData[0] != 'D' {
|
|
t.Errorf("extent LBA 0 = %c, want 'D' (flushed data should be in extent stream)", extentData[0])
|
|
}
|
|
}
|
|
|
|
func testRbSrvStopDuringStream(t *testing.T) {
|
|
vol := cp3Primary(t, "rbs_stop.bv", 1)
|
|
defer vol.Close()
|
|
for i := 0; i < 5; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock('S'))
|
|
}
|
|
vol.StartRebuildServer("127.0.0.1:0")
|
|
|
|
conn, _ := net.Dial("tcp", vol.rebuildServer.Addr())
|
|
defer conn.Close()
|
|
WriteFrame(conn, MsgRebuildReq, EncodeRebuildRequest(RebuildRequest{Type: RebuildFullExtent, Epoch: 1}))
|
|
ReadFrame(conn) // read first chunk
|
|
|
|
vol.StopRebuildServer()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for {
|
|
if _, _, err := ReadFrame(conn); err != nil {
|
|
close(done)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("hung after stop")
|
|
}
|
|
}
|
|
|
|
// ========== Rebuild Client ==========
|
|
|
|
func testRbCliWrongRole(t *testing.T) {
|
|
vol := cp3Primary(t, "rbc_role.bv", 1)
|
|
defer vol.Close()
|
|
err := StartRebuild(vol, "127.0.0.1:1", 0, 1)
|
|
if err == nil {
|
|
t.Error("StartRebuild from Primary should fail")
|
|
}
|
|
}
|
|
|
|
func testRbCliCatchUpDataIntegrity(t *testing.T) {
|
|
// Full E2E: primary writes -> rebuild server -> client catch-up -> data matches.
|
|
primary := cp3Primary(t, "rbc_pri.bv", 1)
|
|
defer primary.Close()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
primary.WriteLBA(uint64(i), makeBlock(byte('A'+i)))
|
|
}
|
|
|
|
primary.StartRebuildServer("127.0.0.1:0")
|
|
defer primary.StopRebuildServer()
|
|
|
|
replica := cp3Rebuilding(t, "rbc_rep.bv", 1)
|
|
defer replica.Close()
|
|
|
|
if err := StartRebuild(replica, primary.rebuildServer.Addr(), 1, 1); err != nil {
|
|
t.Fatalf("StartRebuild: %v", err)
|
|
}
|
|
if replica.Role() != RoleReplica {
|
|
t.Errorf("role = %s, want Replica", replica.Role())
|
|
}
|
|
|
|
// Verify every LBA.
|
|
for i := 0; i < 10; i++ {
|
|
data, err := replica.ReadLBA(uint64(i), 4096)
|
|
if err != nil {
|
|
t.Fatalf("ReadLBA(%d): %v", i, err)
|
|
}
|
|
if data[0] != byte('A'+i) {
|
|
t.Errorf("LBA %d: got %c, want %c", i, data[0], byte('A'+i))
|
|
}
|
|
}
|
|
}
|
|
|
|
func testRbCliWALRecycledFallback(t *testing.T) {
|
|
primary := cp3Primary(t, "rbc_recyc_p.bv", 1)
|
|
defer primary.Close()
|
|
|
|
for i := 0; i < 5; i++ {
|
|
primary.WriteLBA(uint64(i), makeBlock(byte('R'+i)))
|
|
}
|
|
primary.SyncCache()
|
|
time.Sleep(30 * time.Millisecond) // checkpoint advances
|
|
|
|
primary.StartRebuildServer("127.0.0.1:0")
|
|
defer primary.StopRebuildServer()
|
|
|
|
replica := cp3Rebuilding(t, "rbc_recyc_r.bv", 1)
|
|
defer replica.Close()
|
|
|
|
// fromLSN=0 is below checkpoint -> WAL_RECYCLED -> fallback to full extent.
|
|
if err := StartRebuild(replica, primary.rebuildServer.Addr(), 0, 1); err != nil {
|
|
t.Fatalf("StartRebuild (fallback): %v", err)
|
|
}
|
|
if replica.Role() != RoleReplica {
|
|
t.Errorf("role = %s after fallback, want Replica", replica.Role())
|
|
}
|
|
// Data present from extent copy.
|
|
data, _ := replica.ReadLBA(0, 4096)
|
|
if data[0] != 'R' {
|
|
t.Errorf("LBA 0 = %c, want 'R'", data[0])
|
|
}
|
|
}
|
|
|
|
func testRbCliFullExtentClearsDirtyMap(t *testing.T) {
|
|
// After full extent rebuild, dirty map must be empty (old WAL entries invalid).
|
|
primary := cp3Primary(t, "rbc_dm_p.bv", 1)
|
|
defer primary.Close()
|
|
primary.WriteLBA(0, makeBlock('D'))
|
|
primary.SyncCache()
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
primary.StartRebuildServer("127.0.0.1:0")
|
|
defer primary.StopRebuildServer()
|
|
|
|
replica := cp3Rebuilding(t, "rbc_dm_r.bv", 1)
|
|
defer replica.Close()
|
|
|
|
// Pre-populate replica's dirty map with stale entries.
|
|
replica.dirtyMap.Put(999, 0, 1, 4096)
|
|
if replica.dirtyMap.Len() == 0 {
|
|
t.Fatal("dirty map should have entries before rebuild")
|
|
}
|
|
|
|
// Force full extent (fromLSN=0 below checkpoint).
|
|
StartRebuild(replica, primary.rebuildServer.Addr(), 0, 1)
|
|
|
|
if replica.dirtyMap.Len() != 0 {
|
|
t.Errorf("dirty map has %d entries after full extent rebuild, want 0", replica.dirtyMap.Len())
|
|
}
|
|
}
|
|
|
|
func testRbCliFullExtentResetsWAL(t *testing.T) {
|
|
// After full extent rebuild, WAL head and tail must be 0.
|
|
primary := cp3Primary(t, "rbc_wal_p.bv", 1)
|
|
defer primary.Close()
|
|
primary.WriteLBA(0, makeBlock('W'))
|
|
primary.SyncCache()
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
primary.StartRebuildServer("127.0.0.1:0")
|
|
defer primary.StopRebuildServer()
|
|
|
|
replica := cp3Rebuilding(t, "rbc_wal_r.bv", 1)
|
|
defer replica.Close()
|
|
|
|
// Write something to replica's WAL first so it's non-zero.
|
|
// (Can't write as Rebuilding, but we can check head is 0 after rebuild)
|
|
StartRebuild(replica, primary.rebuildServer.Addr(), 0, 1)
|
|
|
|
head := replica.wal.LogicalHead()
|
|
tail := replica.wal.LogicalTail()
|
|
// After full extent, WAL was reset to 0 then second catch-up may have added entries.
|
|
// But with no writes during rebuild, WAL should be clean or have only catch-up entries.
|
|
// The key invariant: head and tail are consistent (head >= tail).
|
|
if head < tail {
|
|
t.Errorf("WAL inconsistent after rebuild: head=%d < tail=%d", head, tail)
|
|
}
|
|
}
|
|
|
|
func testRbCliFullExtentPersistsSuperblock(t *testing.T) {
|
|
// After full extent rebuild, superblock should be persisted with clean WAL state.
|
|
// Reopen the vol and verify.
|
|
primary := cp3Primary(t, "rbc_sb_p.bv", 1)
|
|
defer primary.Close()
|
|
primary.WriteLBA(0, makeBlock('S'))
|
|
primary.SyncCache()
|
|
time.Sleep(30 * time.Millisecond)
|
|
|
|
primary.StartRebuildServer("127.0.0.1:0")
|
|
defer primary.StopRebuildServer()
|
|
|
|
dir := t.TempDir()
|
|
replicaPath := filepath.Join(dir, "rbc_sb_r.bv")
|
|
cfg := DefaultConfig()
|
|
cfg.FlushInterval = 5 * time.Millisecond
|
|
|
|
replica, _ := CreateBlockVol(replicaPath, CreateOptions{
|
|
VolumeSize: 64 * 1024, BlockSize: 4096, WALSize: 64 * 1024,
|
|
}, cfg)
|
|
replica.HandleAssignment(1, RolePrimary, 1*time.Second)
|
|
replica.HandleAssignment(1, RoleStale, 0)
|
|
replica.HandleAssignment(1, RoleRebuilding, 0)
|
|
|
|
StartRebuild(replica, primary.rebuildServer.Addr(), 0, 1)
|
|
replica.Close()
|
|
|
|
// Reopen and check superblock.
|
|
reopened, err := OpenBlockVol(replicaPath, cfg)
|
|
if err != nil {
|
|
t.Fatalf("reopen: %v", err)
|
|
}
|
|
defer reopened.Close()
|
|
|
|
if reopened.super.WALHead != 0 {
|
|
t.Errorf("WALHead after reopen = %d, want 0", reopened.super.WALHead)
|
|
}
|
|
if reopened.super.WALTail != 0 {
|
|
t.Errorf("WALTail after reopen = %d, want 0", reopened.super.WALTail)
|
|
}
|
|
}
|
|
|
|
// ========== applyRebuildEntry ==========
|
|
|
|
func testApplyNextLSNAdvanced(t *testing.T) {
|
|
// applyRebuildEntry must advance nextLSN to max(entry.LSN)+1.
|
|
vol := cp3Vol(t, "apply_lsn.bv", 64*1024)
|
|
defer vol.Close()
|
|
|
|
initial := vol.nextLSN.Load()
|
|
|
|
entry := WALEntry{LSN: 42, Epoch: 0, Type: EntryTypeWrite, LBA: 0, Length: 4096, Data: makeBlock('N')}
|
|
encoded, _ := entry.Encode()
|
|
if err := applyRebuildEntry(vol, encoded); err != nil {
|
|
t.Fatalf("apply: %v", err)
|
|
}
|
|
|
|
got := vol.nextLSN.Load()
|
|
if got != 43 {
|
|
t.Errorf("nextLSN = %d, want 43 (LSN 42 + 1), was %d before", got, initial)
|
|
}
|
|
}
|
|
|
|
func testApplyOutOfOrderAccepted(t *testing.T) {
|
|
// Unlike ReplicaReceiver, rebuild has NO contiguity enforcement.
|
|
// Out-of-order entries are accepted.
|
|
vol := cp3Vol(t, "apply_ooo.bv", 64*1024)
|
|
defer vol.Close()
|
|
|
|
// Apply LSN=10 first, then LSN=5 -- both should succeed.
|
|
e10 := WALEntry{LSN: 10, Epoch: 0, Type: EntryTypeWrite, LBA: 0, Length: 4096, Data: makeBlock('A')}
|
|
enc10, _ := e10.Encode()
|
|
if err := applyRebuildEntry(vol, enc10); err != nil {
|
|
t.Fatalf("apply LSN=10: %v", err)
|
|
}
|
|
|
|
e5 := WALEntry{LSN: 5, Epoch: 0, Type: EntryTypeWrite, LBA: 1, Length: 4096, Data: makeBlock('B')}
|
|
enc5, _ := e5.Encode()
|
|
if err := applyRebuildEntry(vol, enc5); err != nil {
|
|
t.Fatalf("apply LSN=5: %v", err)
|
|
}
|
|
|
|
// nextLSN should be 11 (max of 10+1, 5+1 = 11).
|
|
if vol.nextLSN.Load() != 11 {
|
|
t.Errorf("nextLSN = %d, want 11 (max LSN was 10)", vol.nextLSN.Load())
|
|
}
|
|
|
|
// Both entries should be in dirty map.
|
|
_, _, _, ok0 := vol.dirtyMap.Get(0)
|
|
_, _, _, ok1 := vol.dirtyMap.Get(1)
|
|
if !ok0 || !ok1 {
|
|
t.Errorf("dirty map: LBA 0 ok=%v, LBA 1 ok=%v -- both should be present", ok0, ok1)
|
|
}
|
|
}
|
|
|
|
func testApplyDirtyMapUpdated(t *testing.T) {
|
|
vol := cp3Vol(t, "apply_dm.bv", 64*1024)
|
|
defer vol.Close()
|
|
|
|
// Apply a WRITE entry.
|
|
entry := WALEntry{LSN: 1, Epoch: 0, Type: EntryTypeWrite, LBA: 7, Length: 4096, Data: makeBlock('D')}
|
|
encoded, _ := entry.Encode()
|
|
applyRebuildEntry(vol, encoded)
|
|
|
|
_, lsn, _, ok := vol.dirtyMap.Get(7)
|
|
if !ok {
|
|
t.Fatal("LBA 7 not in dirty map after apply")
|
|
}
|
|
if lsn != 1 {
|
|
t.Errorf("dirty map LSN for LBA 7 = %d, want 1", lsn)
|
|
}
|
|
|
|
// Apply a TRIM entry -- should also update dirty map.
|
|
trimEntry := WALEntry{LSN: 2, Epoch: 0, Type: EntryTypeTrim, LBA: 7, Length: 4096}
|
|
trimEnc, _ := trimEntry.Encode()
|
|
applyRebuildEntry(vol, trimEnc)
|
|
|
|
_, trimLSN, _, ok := vol.dirtyMap.Get(7)
|
|
if !ok {
|
|
t.Fatal("LBA 7 not in dirty map after TRIM apply")
|
|
}
|
|
if trimLSN != 2 {
|
|
t.Errorf("dirty map LSN after TRIM = %d, want 2", trimLSN)
|
|
}
|
|
}
|
|
|
|
// ========== Integration ==========
|
|
|
|
func testLifecycleFullCycle(t *testing.T) {
|
|
// Full lifecycle: None -> Replica -> promote -> write -> demote -> rebuild -> replica.
|
|
vol := cp3Vol(t, "lifecycle.bv", 64*1024)
|
|
defer vol.Close()
|
|
|
|
// 1. None -> Replica
|
|
vol.HandleAssignment(1, RoleReplica, 0)
|
|
if vol.Role() != RoleReplica {
|
|
t.Fatalf("step 1: role = %s", vol.Role())
|
|
}
|
|
|
|
// 2. Promote to Primary
|
|
vol.HandleAssignment(2, RolePrimary, 10*time.Second)
|
|
if vol.Role() != RolePrimary {
|
|
t.Fatalf("step 2: role = %s", vol.Role())
|
|
}
|
|
|
|
// 3. Write data
|
|
for i := 0; i < 5; i++ {
|
|
vol.WriteLBA(uint64(i), makeBlock(byte('L'+i)))
|
|
}
|
|
vol.SyncCache()
|
|
|
|
// 4. Demote
|
|
vol.HandleAssignment(3, RoleStale, 0)
|
|
if vol.Role() != RoleStale {
|
|
t.Fatalf("step 4: role = %s", vol.Role())
|
|
}
|
|
|
|
// 5. Writes rejected
|
|
if err := vol.WriteLBA(0, makeBlock('X')); err == nil {
|
|
t.Error("write after demote should fail")
|
|
}
|
|
|
|
// 6. Reads work
|
|
data, _ := vol.ReadLBA(0, 4096)
|
|
if data[0] != 'L' {
|
|
t.Errorf("LBA 0 = %c, want 'L'", data[0])
|
|
}
|
|
|
|
// 7. Rebuilding
|
|
vol.HandleAssignment(3, RoleRebuilding, 0)
|
|
if vol.Role() != RoleRebuilding {
|
|
t.Fatalf("step 7: role = %s", vol.Role())
|
|
}
|
|
}
|
|
|
|
func testDirtyMapClearConcurrent(t *testing.T) {
|
|
dm := NewDirtyMap(16)
|
|
for i := uint64(0); i < 100; i++ {
|
|
dm.Put(i, i*4096, i+1, 4096)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
var reads atomic.Int64
|
|
|
|
wg.Add(4)
|
|
for g := 0; g < 4; g++ {
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 1000; i++ {
|
|
_, _, _, _ = dm.Get(uint64(i % 100))
|
|
reads.Add(1)
|
|
}
|
|
}()
|
|
}
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for i := 0; i < 10; i++ {
|
|
dm.Clear()
|
|
time.Sleep(100 * time.Microsecond)
|
|
}
|
|
}()
|
|
|
|
done := make(chan struct{})
|
|
go func() { wg.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("deadlock")
|
|
}
|
|
if reads.Load() != 4000 {
|
|
t.Errorf("reads = %d, want 4000", reads.Load())
|
|
}
|
|
}
|