Files
seaweedfs/weed/storage/blockvol/lease_grant_test.go
T
Ping QiuandClaude Opus 4.6 9acd187587 feat: Phase 8 complete -- CP8-5 stability gate, lease grant fix, Docker e2e, 13 chaos scenarios
Phase 8 closes with all 6 checkpoints done (CP8-1 through CP8-5 + CP8-3-1):
- CP8-5: 12/12 enterprise QA scenarios PASS on real hardware (m01/M02)
- Master-authoritative lease grants (BUG-CP85-11): master renews primary
  write leases on every heartbeat response, replacing retain-until-confirmed
  assignment queue semantics that caused 30s lease expiry
- Post-rebuild WAL shipping gap fix (BUG-CP85-1): syncLSNAfterRebuild
  advances replica nextLSN so WAL entries are accepted after rebuild
- Block heartbeat startup race fix (BUG-CP85-10): dynamic blockService
  check on each tick instead of one-shot at loop start
- 8 new tests: 4 engine lease grant + 4 registry lease grant
- 13 new YAML scenarios: chaos (kill-loop, partition, disk-full),
  database integrity (sqlite crash, ext4 fsck), perf baseline,
  metrics verify, snapshot stress, expand-failover, session storm,
  role flap, 24h soak
- 12 new testrunner actions (database, fsck, grep_log, write_loop_bg,
  stop_bg, assert_metric_gt/eq/lt) + phase repeat support
- Docker compose setup + getting-started guide for block storage users
- 960+ cumulative unit tests, 24 YAML scenarios

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 21:30:14 -08:00

171 lines
5.2 KiB
Go

package blockvol
import (
"errors"
"testing"
"time"
)
// TestLeaseGrant tests the explicit lease grant mechanism.
func TestLeaseGrant(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T)
}{
{name: "keepalive_longevity", run: testLeaseKeepaliveLongevity},
{name: "heartbeat_loss_lease_expires", run: testHeartbeatLossLeaseExpires},
{name: "old_primary_cannot_renew_after_promotion", run: testOldPrimaryCannotRenew},
{name: "stale_epoch_grant_rejected", run: testStaleEpochGrantRejected},
}
for _, tt := range tests {
t.Run(tt.name, tt.run)
}
}
// Test 1: Lease keepalive longevity — writes continue past TTL with healthy heartbeat.
func testLeaseKeepaliveLongevity(t *testing.T) {
v := createTestVol(t)
defer v.Close()
// Assign as primary with short 200ms lease.
if err := v.HandleAssignment(1, RolePrimary, 200*time.Millisecond); err != nil {
t.Fatalf("HandleAssignment: %v", err)
}
// Write should succeed immediately.
data := make([]byte, v.Info().BlockSize)
data[0] = 0xAA
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("write before TTL: %v", err)
}
// Simulate periodic lease grants (like master heartbeat responses).
// Grant every 100ms for 500ms total — well past the 200ms TTL.
// Uses HandleAssignment (the real production path) instead of a direct
// lease.Grant() — same epoch + same role = lease refresh.
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
// Lease grant via HandleAssignment same-role refresh path.
if err := v.HandleAssignment(1, RolePrimary, 200*time.Millisecond); err != nil {
t.Fatalf("lease grant at iteration %d: %v", i, err)
}
// Write should still succeed because lease was renewed.
data[0] = byte(i + 1)
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("write at iteration %d (t=%dms): %v", i, (i+1)*100, err)
}
}
// Final verification: we wrote past 500ms with a 200ms TTL lease.
if !v.lease.IsValid() {
t.Error("lease should still be valid after continuous renewal")
}
}
// Test 2: Heartbeat loss — writes fail after TTL expiry.
func testHeartbeatLossLeaseExpires(t *testing.T) {
v := createTestVol(t)
defer v.Close()
// Assign as primary with short 100ms lease.
if err := v.HandleAssignment(1, RolePrimary, 100*time.Millisecond); err != nil {
t.Fatalf("HandleAssignment: %v", err)
}
// Write should succeed immediately.
data := make([]byte, v.Info().BlockSize)
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("write before expiry: %v", err)
}
// Do NOT renew the lease — simulate heartbeat loss.
time.Sleep(150 * time.Millisecond)
// Write should fail with ErrLeaseExpired.
err := v.WriteLBA(0, data)
if err == nil {
t.Fatal("expected write to fail after lease expiry, got nil")
}
if !errors.Is(err, ErrLeaseExpired) {
t.Fatalf("expected ErrLeaseExpired, got: %v", err)
}
}
// Test 3: Old primary cannot keep renewing after promotion elsewhere.
// After demotion, lease grants with old epoch must not revive writes.
func testOldPrimaryCannotRenew(t *testing.T) {
v := createTestVol(t)
defer v.Close()
// Start as primary at epoch 1.
if err := v.HandleAssignment(1, RolePrimary, 30*time.Second); err != nil {
t.Fatalf("HandleAssignment: %v", err)
}
data := make([]byte, v.Info().BlockSize)
if err := v.WriteLBA(0, data); err != nil {
t.Fatalf("write as primary: %v", err)
}
// Demote: master sends Stale assignment with epoch 2.
if err := v.HandleAssignment(2, RoleStale, 0); err != nil {
t.Fatalf("demote: %v", err)
}
// Write must fail — no longer primary.
writeErr := v.WriteLBA(0, data)
if writeErr == nil {
t.Fatal("expected write to fail after demotion, got nil")
}
if !errors.Is(writeErr, ErrNotPrimary) {
t.Fatalf("expected ErrNotPrimary, got: %v", writeErr)
}
// Old primary tries to re-assign as Primary with stale epoch.
// After demotion to Stale, Stale->Primary is an invalid transition
// (must go through rebuild). Even if it succeeded, writeGate checks role.
err := v.HandleAssignment(1, RolePrimary, 30*time.Second)
if err == nil {
t.Fatal("expected error for Stale->Primary transition, got nil")
}
}
// Test 4: Mixed epochs — renewal for stale epoch is rejected by HandleAssignment.
func testStaleEpochGrantRejected(t *testing.T) {
v := createTestVol(t)
defer v.Close()
// Primary at epoch 5.
if err := v.HandleAssignment(5, RolePrimary, 30*time.Second); err != nil {
t.Fatalf("HandleAssignment: %v", err)
}
// Lease grant with epoch 3 (stale) via HandleAssignment — must be rejected.
err := v.HandleAssignment(3, RolePrimary, 30*time.Second)
if err == nil {
t.Fatal("expected error for stale epoch, got nil")
}
if !errors.Is(err, ErrEpochRegression) {
t.Fatalf("expected ErrEpochRegression, got: %v", err)
}
// Epoch should remain 5.
if v.Epoch() != 5 {
t.Errorf("epoch should remain 5, got %d", v.Epoch())
}
// Lease grant with matching epoch 5 should succeed.
if err := v.HandleAssignment(5, RolePrimary, 30*time.Second); err != nil {
t.Fatalf("same-epoch refresh should succeed: %v", err)
}
// Lease grant with epoch 6 (bump) should also succeed.
if err := v.HandleAssignment(6, RolePrimary, 30*time.Second); err != nil {
t.Fatalf("epoch bump should succeed: %v", err)
}
if v.Epoch() != 6 {
t.Errorf("epoch should be 6 after bump, got %d", v.Epoch())
}
}