Skip to content
quarry Artifact [4f35ca48df]
Login

Artifact [4f35ca48df]

Artifact 4f35ca48dff2a53b58ba116e2f85de8350ef8209faec124629a55c49d97a03ed:


package lease

import (
	"context"
	"testing"
	"time"

	"github.com/nats-io/nats.go"

	"craftdesign.group/quarry/internal/events"
)

// #245: a write's token is cancelled the moment this node's hold is
// lost, here to another node taking the key mid-write, and not before:
// renewals while the hold lasts leave it live.
func TestATokenEndsWhenTheLeaseIsLost(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	ttl := 300 * time.Millisecond
	a := manager(t, ev, "node-a", ttl)
	kv, err := ev.JS().KeyValue(Bucket)
	if err != nil {
		t.Fatal(err)
	}
	err = a.Guard("repo", func(tok Token) error {
		ctx := tok.Context()
		time.Sleep(3 * ttl) // several renewals
		if ctx.Err() != nil {
			t.Fatal("FAIL: a held lease cancelled its write")
		}
		if _, err := kv.Put("repo", []byte("node-b")); err != nil {
			t.Fatal(err)
		}
		select {
		case <-ctx.Done():
		case <-time.After(5 * ttl):
			t.Fatal("FAIL: the write's context lived on after the lease was lost")
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}

// The coordinator going away mid-write ends the write too: this node
// can no longer renew, so it can no longer say nobody else holds.
func TestATokenEndsWhenTheCoordinatorGoes(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	ttl := 300 * time.Millisecond
	a := manager(t, ev, "node-a", ttl)
	err = a.Guard("repo", func(tok Token) error {
		ev.Close()
		select {
		case <-tok.Context().Done():
		case <-time.After(10 * ttl):
			t.Fatal("FAIL: the write's context lived on with no coordinator")
		}
		return nil
	})
	if err != nil {
		t.Fatal(err)
	}
}

// A lease regained after a loss writes under a fresh context, and an
// Unguarded token, with no lease to lose, never ends.
func TestANewHoldGetsAFreshContext(t *testing.T) {
	ev, err := events.Start(t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	kv, err := ev.JS().CreateKeyValue(&nats.KeyValueConfig{Bucket: Bucket, TTL: time.Minute})
	if err != nil {
		t.Fatal(err)
	}
	m := start(kv, "node-a", time.Minute)
	t.Cleanup(m.Close)
	var first context.Context
	if err := m.Guard("repo", func(tok Token) error { first = tok.Context(); return nil }); err != nil {
		t.Fatal(err)
	}
	m.mu.Lock()
	m.state["repo"].lose()
	m.mu.Unlock()
	if first.Err() == nil {
		t.Fatal("FAIL: losing the hold left its context live")
	}
	if err := m.Guard("repo", func(tok Token) error {
		if tok.Context().Err() != nil {
			t.Fatal("FAIL: a regained hold wrote under the lost one's context")
		}
		return nil
	}); err != nil {
		t.Fatal(err)
	}
	if Unguarded("test").Context().Done() != nil {
		t.Fatal("FAIL: an Unguarded token can be cancelled")
	}
}