Skip to content
quarry Artifact [9cf5a1361e]
Login

Artifact [9cf5a1361e]

Artifact 9cf5a1361edceb4749966e84e650144e5f42f1b0ac085ed4cedef7582c241075:


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(Token) 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(Token) 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(Token) 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(Token) 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(Token) 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(Token) 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(Token) error { return nil }); err != nil {
		t.Fatal(err)
	}
	ev.Close() // coordinator down
	err = a.Guard("repo", func(Token) error {
		t.Fatal("write ran with no coordinator")
		return nil
	})
	if err == nil {
		t.Fatal("write not refused with coordinator down")
	}
}

// The write closure receives the token that proves it runs under the
// lease; a token made any other way than Guard or Unguarded is refused.
func TestGuardHandsTheWriteAToken(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)
	if err := a.Guard("repo", func(tok Token) error { return tok.Err() }); err != nil {
		t.Fatalf("FAIL: the guard's token is not usable: %v", err)
	}
	if err := (Token{}).Err(); !errors.Is(err, ErrNoToken) {
		t.Fatalf("FAIL: a zero token answered %v, want ErrNoToken", err)
	}
	if err := Unguarded("test: no coordinator in this path").Err(); err != nil {
		t.Fatalf("FAIL: an Unguarded token is not usable: %v", err)
	}
}