Two controllers could both act (issue 204), a reconcile's report could overtake the apply after it and the digest decided (issue 267), and a grant could make a second writer of a machine's report. - The lease (internal/lease, ADR 0229): mesh-controller_lease key `holder`, 15 s age, renewed every 5 s by compare-and-set; the epoch is the revision it was taken at. The gate is the clock (stops 3 s before expiry); a refused renewal is a loss and the process exits; a holder that stops gives it back. serve takes it before asserting the bus. Epochs kept in the store (migration 0068 controller_epoch) as a floor: a bucket raised from nothing is compacted past it. Unleased (no epoch, S12 urgent) only when nobody holds it and the bus will not let it be written. A shell command acts under the holder's epoch, or its own lease when none. - Declarations carry `epoch` inside the signed envelope, only to a machine whose latest account carried a report_sequence (mesh-host #35); would-send is composed with the epoch last sent. Allot and the send both pass the gate. - Reports: contract in internal/link/order.go (epoch, sequence, report_sequence, older_than, refused_older). Accounts kept by epoch, then sequence, then report sequence; older refused, counted; unordered reports keep the digest rule. Plans by compare-and-set on a revision, with epoch. Conditions and calls carry the epoch and are not written off the lease. - S12 and S13 (naming the writer by epoch) watched, D5 run; reset of the bucket said. Writers table compiled in and enforced in PermissionsFor; the controller no longer publishes mesh.control.>. A contract per consumed kind, and the empty-on-error lint over the repository. - mesh-host pinned to its main with the epoch in the validator (D1 validates the envelope as sent). Needs mesh-host's genesis lock with the lease grant (mesh-host PR) for TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose.
69 lines
2.0 KiB
Go
69 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/lease"
|
|
)
|
|
|
|
// A command run at a shell (novox/hq to-be 45 §6): under the holder's epoch while a controller holds the
|
|
// lease, read at the moment it acts; under a lease of its own while none does, given back as it ends.
|
|
func TestACommandActsUnderTheHoldersEpochOrItsOwn(t *testing.T) {
|
|
url, kv := aBusForTheLease(t)
|
|
t.Setenv(broker.NATSVar, url)
|
|
ctx := t.Context()
|
|
|
|
// Nobody holds it: the command takes it, and gives it back.
|
|
cmd := &actor{}
|
|
own, err := cmd.epoch(ctx)
|
|
if err != nil || own == 0 {
|
|
t.Fatalf("a command with nobody holding the lease acts as %d (%v)", own, err)
|
|
}
|
|
if h, found, _ := lease.Current(ctx, kv); !found || h.Epoch != own {
|
|
t.Fatalf("the command's lease is not on the bus: %+v", h)
|
|
}
|
|
cmd.release()
|
|
if _, found, _ := lease.Current(ctx, kv); found {
|
|
t.Fatal("the command did not give its lease back as it ended")
|
|
}
|
|
|
|
// A controller holds it: a command acts under that epoch.
|
|
l, err := lease.Open(ctx, mustJetStream(t, url), broker.LeaseBucket, lease.Options{Holder: lease.Holder{Instance: "serving"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
held, err := l.TryTake(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
borrower := &actor{}
|
|
defer borrower.release()
|
|
if got, err := borrower.epoch(ctx); err != nil || got != held {
|
|
t.Fatalf("a command acts as %d (%v), want the holder's %d", got, err, held)
|
|
}
|
|
// The holder lets go: the command does not go on under an epoch nobody holds.
|
|
l.Release(ctx)
|
|
if _, err := borrower.epoch(ctx); err == nil {
|
|
t.Fatal("a command acted under an epoch nobody holds any more")
|
|
}
|
|
}
|
|
|
|
// mustJetStream is a connection of its own to the test bus.
|
|
func mustJetStream(t *testing.T, url string) jetstream.JetStream {
|
|
t.Helper()
|
|
conn, err := nats.Connect(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(conn.Close)
|
|
js, err := jetstream.New(conn)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return js
|
|
}
|