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")
}
}