Artifact 32f256f621db920aa6470b2c737447b38dfb404d1f3aa6df82f129cf4e37d25c:
- File
internal/lease/lease_test.go
— part of check-in
[d1436c2071]
at
2026-08-16 02:34:30
on branch trunk
— lease: single-writer coordination (0001 §3)
NATS KV TTL lease per store keyed by project code. Write ops and sync pushes route through Guard: renew-or-acquire, failover grace after displacing a holder, commit-point re-check before the write, and writes-refuse when the coordinator is unreachable (correctness over availability). Single-node holds its own lease with no grace delay; the machinery and failure posture are identical at N nodes. Tests: mutual exclusion, failover, coordinator-down refusal. (user: dmestas, size: 3208) [annotate] [blame] [check-ins using]
package lease import ( "errors" "testing" "time" "craftdesign.group/quarry/internal/events" ) func manager(t *testing.T, ev *events.Server, node string, ttl time.Duration) *Manager { t.Helper() m, err := New(ev.JS(), node, ttl) if err != nil { t.Fatal(err) } t.Cleanup(m.Close) return m } // Two nodes contend for one store: exactly one may write, and its // commit-point re-check succeeds while the loser is refused. func TestMutualExclusion(t *testing.T) { ev, err := events.Start(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(ev.Close) a := manager(t, ev, "node-a", time.Second) b := manager(t, ev, "node-b", time.Second) wrote := 0 if err := a.Guard("repo1", func() error { wrote++; return nil }); err != nil { t.Fatalf("first acquirer refused: %v", err) } if !a.Held("repo1") { t.Fatal("acquirer does not report holding") } if err := b.Guard("repo1", func() error { wrote++; return nil }); err != ErrNotHolder { t.Fatalf("second node write: got %v, want ErrNotHolder", err) } if b.Held("repo1") { t.Fatal("non-holder reports holding") } if wrote != 1 { t.Fatalf("%d writes ran, want 1", wrote) } // A different store is independently acquirable. if err := b.Guard("repo2", func() error { return nil }); err != nil { t.Fatalf("second store on other node refused: %v", err) } } // When the holder stops renewing, its lease ages out and the other node // takes over — but only after the failover grace, never during it. func TestFailover(t *testing.T) { ev, err := events.Start(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(ev.Close) ttl := time.Second a := manager(t, ev, "node-a", ttl) b := manager(t, ev, "node-b", ttl) if err := a.Guard("repo", func() error { return nil }); err != nil { t.Fatal(err) } // b has seen a's foreign hold (it will try during exclusion below). if err := b.Guard("repo", func() error { return nil }); err != ErrNotHolder { t.Fatalf("b write while a holds: %v", err) } a.Close() // a stops renewing; its entry ages out after ttl // Immediately after expiry b can acquire, but the failover grace // refuses its first write for one more TTL. deadline := time.Now().Add(4 * ttl) grace, wrote := false, false for time.Now().Before(deadline) { err := b.Guard("repo", func() error { return nil }) if err == nil { wrote = true break } if errors.Is(err, ErrNotHolder) && b.Held("repo") { grace = true // acquired, but grace refuses the write } time.Sleep(100 * time.Millisecond) } if !wrote { t.Fatal("failover never completed") } if !grace { t.Fatal("failover grace window was never observed") } } // With the coordinator gone, writes refuse (ErrUnavailable), never // proceed uncoordinated. func TestCoordinatorGoneRefusesWrites(t *testing.T) { ev, err := events.Start(t.TempDir()) if err != nil { t.Fatal(err) } a := manager(t, ev, "node-a", time.Second) if err := a.Guard("repo", func() error { return nil }); err != nil { t.Fatal(err) } ev.Close() // coordinator down err = a.Guard("repo", func() error { t.Fatal("write ran with no coordinator") return nil }) if err == nil { t.Fatal("write not refused with coordinator down") } }