Check the mirrors as ADR 0356 says, and prove a removal asks for the lease
mesh/merge-gate head 35c7662f895f merged into trunk dc36536e199e — pass: the change touches no module of the mesh's graph
mesh/repo-check head 35c7662f895f merged into trunk dc36536e199e — fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheContro…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 35c7662f895f merged into trunk dc36536e199e — pass: the change touches no module of the mesh's graph
mesh/repo-check head 35c7662f895f merged into trunk dc36536e199e — fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheContro…
mesh/delivery superseded: a newer head of the same pull request
Reviews 251 of mesh-controller#315 and ADR 0356 v4 (hq issue 652): a removal without the lease removes nothing; a module no longer registered loses its declared-config key with its event; a listed mirror is never reported as undeclared state; only the audit and a mirror's own writer name a mirror's bucket; and the mirrors' list and the writers table agree. The store test's line moves where it merges with the integration branch's own new lines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,3 +125,56 @@ func TestWhatTheControllerAskedIsBounded(t *testing.T) {
|
|||||||
t.Errorf("history %d, age %s, bytes %d", status.History(), status.TTL(), info.Config.MaxBytes)
|
t.Errorf("history %d, age %s, bytes %d", status.History(), status.TTL(), info.Config.MaxBytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A listed mirror is no undeclared bucket** (ADR 0356 §3, ADR 0201 §16): the controller's list of its mirrors
|
||||||
|
// stands for their declaration, so the report of state nothing declares never names one.
|
||||||
|
func TestAListedMirrorIsNotReportedAsUndeclared(t *testing.T) {
|
||||||
|
names := []string{"gone_state"}
|
||||||
|
for _, m := range ControllerMirrors() {
|
||||||
|
names = append(names, BucketName(ControllerSeat, m))
|
||||||
|
}
|
||||||
|
undeclared, err := RaiseBuckets(fakeBuckets{names: names}, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !slices.Equal(undeclared, []string{"gone_state"}) {
|
||||||
|
t.Fatalf("the report of undeclared state names %v", undeclared)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// **The mirrors' list and the writers table agree** (ADR 0356 §3, to-be 45 §1): every listed mirror is a row of
|
||||||
|
// the table whose one writer is the controller, on its bucket alone; and every row that writes a bucket of the
|
||||||
|
// controller's named as a mirror is on the list.
|
||||||
|
func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
|
||||||
|
controller := Principal{Kind: KindController}
|
||||||
|
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
|
||||||
|
rowed := map[string]bool{}
|
||||||
|
for _, row := range WritersTable {
|
||||||
|
if !strings.HasSuffix(row.State, " mirror") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, subject := range row.Subjects {
|
||||||
|
bucket := strings.TrimSuffix(strings.TrimPrefix(subject, "$KV."), ".>")
|
||||||
|
rowed[bucket] = true
|
||||||
|
if row.Writes == nil || !row.Writes(controller, subject) || row.Writes(module, subject) {
|
||||||
|
t.Errorf("%q is not written by the controller alone", row.State)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listed := map[string]bool{}
|
||||||
|
for _, m := range ControllerMirrors() {
|
||||||
|
bucket := BucketName(ControllerSeat, m)
|
||||||
|
listed[bucket] = true
|
||||||
|
if !rowed[bucket] {
|
||||||
|
t.Errorf("the mirror %s is listed and no row of the writers table writes it", bucket)
|
||||||
|
}
|
||||||
|
if !IsControllerBucket(bucket) || !slices.Contains(ControllerBuckets(), bucket) {
|
||||||
|
t.Errorf("the mirror %s is listed and not asserted as the controller's own", bucket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for bucket := range rowed {
|
||||||
|
if !listed[bucket] {
|
||||||
|
t.Errorf("the writers table writes the mirror %s, which the list does not name", bucket)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -155,4 +155,24 @@ func TestNatsAnAssignmentAndARegistrationEachChangeOneKeyAndRaiseOneEvent(t *tes
|
|||||||
if len(said) != 1 || said[0].Key != "anchor.ui" || said[0].Change != Removed {
|
if len(said) != 1 || said[0].Key != "anchor.ui" || said[0].Change != Removed {
|
||||||
t.Errorf("an unassignment raised %+v", said)
|
t.Errorf("an unassignment raised %+v", said)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A module no longer registered** has its key removed, said once as `removed` (ADR 0356 §4).
|
||||||
|
if err := inv.ForgetModule(ctx, "notes"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
declaredWrites = writes(t, api, DeclaredConfig.Bucket)
|
||||||
|
declaredSaid = len(heardDeclared.all())
|
||||||
|
if _, rd := pass(); rd != (Result{Removed: 1}) {
|
||||||
|
t.Errorf("a module forgotten did %+v", rd)
|
||||||
|
}
|
||||||
|
if got := writes(t, api, DeclaredConfig.Bucket) - declaredWrites; got != 1 {
|
||||||
|
t.Errorf("a module forgotten changed %d keys of the declared config", got)
|
||||||
|
}
|
||||||
|
if _, err := declared.KV.Get(ctx, "notes"); err == nil {
|
||||||
|
t.Error("a module forgotten keeps its key")
|
||||||
|
}
|
||||||
|
said = settle(heardDeclared, declaredSaid+1)[declaredSaid:]
|
||||||
|
if len(said) != 1 || said[0].Key != "notes" || said[0].Change != Removed {
|
||||||
|
t.Errorf("a module forgotten raised %+v", said)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -312,6 +312,31 @@ func TestNatsAWriterWithoutTheLeaseWritesNothing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A removal asks for the lease too** (ADR 0289 §9): a writer that held the lease when its pass began and lost
|
||||||
|
// it before its first removal purges nothing, and says it does not hold the lease.
|
||||||
|
func TestNatsARemovalWithoutTheLeaseRemovesNothing(t *testing.T) {
|
||||||
|
js, api := aMirrorBus(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
held := aWriter(t, js, api, Assignments)
|
||||||
|
if _, err := held.Sync(ctx, map[string][]byte{"anchor.gitea": []byte(`{"a":1}`)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
before := writes(t, api, Assignments.Bucket)
|
||||||
|
asked := 0
|
||||||
|
w := aWriter(t, js, api, Assignments)
|
||||||
|
w.Holds = func() bool { asked++; return asked == 1 }
|
||||||
|
r, err := w.Sync(ctx, map[string][]byte{})
|
||||||
|
if !errors.Is(err, ErrNotHeld) || r != (Result{}) || asked != 2 {
|
||||||
|
t.Errorf("a removal without the lease did %+v, %v (the lease asked %d times)", r, err, asked)
|
||||||
|
}
|
||||||
|
if got := writes(t, api, Assignments.Bucket) - before; got != 0 {
|
||||||
|
t.Errorf("a removal without the lease wrote the bucket %d times", got)
|
||||||
|
}
|
||||||
|
if _, err := w.KV.Get(ctx, "anchor.gitea"); err != nil {
|
||||||
|
t.Errorf("the key was removed without the lease: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// **A key the bus cannot hold is refused alone**, and the others are written.
|
// **A key the bus cannot hold is refused alone**, and the others are written.
|
||||||
func TestAKeyTheBusCannotHoldIsRefusedAlone(t *testing.T) {
|
func TestAKeyTheBusCannotHoldIsRefusedAlone(t *testing.T) {
|
||||||
if validKey("anchor.gitea") != nil || validKey("bad key") == nil || validKey(".lead") == nil ||
|
if validKey("anchor.gitea") != nil || validKey("bad key") == nil || validKey(".lead") == nil ||
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package recordmirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/token"
|
||||||
|
"io/fs"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/broker"
|
||||||
|
)
|
||||||
|
|
||||||
|
// mayNameAMirror are the files of the controller that may name a record mirror's bucket outside their tests:
|
||||||
|
// where the buckets are defined, asserted and granted (internal/broker), the list a reader names in `reads`
|
||||||
|
// (internal/catalogue/state.go), and each mirror's own writer (this package and the loop that runs it). The
|
||||||
|
// audit of a mirror against its store (ADR 0289 §4, issue 419) joins this list when it is built.
|
||||||
|
var mayNameAMirror = []string{
|
||||||
|
"cmd/mesh-controller/record_mirrors.go",
|
||||||
|
"internal/broker/controller_buckets.go",
|
||||||
|
"internal/broker/writers.go",
|
||||||
|
"internal/catalogue/state.go",
|
||||||
|
}
|
||||||
|
|
||||||
|
// **Nothing in the controller reads a mirror to decide anything** (ADR 0289 §19 as ADR 0356 §2 changes it): no
|
||||||
|
// file of the controller's packages outside its tests names a mirror's bucket, by its Go name or its bus name,
|
||||||
|
// but the audit and a mirror's own writer. A reader elsewhere fails here, naming the file and what it named.
|
||||||
|
func TestOnlyTheAuditAndAMirrorsWriterReadAMirror(t *testing.T) {
|
||||||
|
names := []string{"AssignmentsBucket", "DeclaredConfigBucket", "MirrorAssignments", "MirrorDeclaredConfig",
|
||||||
|
"ControllerMirrors", "Assignments", "DeclaredConfig", "Mirrors"}
|
||||||
|
var buckets []string
|
||||||
|
for _, m := range broker.ControllerMirrors() {
|
||||||
|
buckets = append(buckets, broker.BucketName(broker.ControllerSeat, m))
|
||||||
|
}
|
||||||
|
root := filepath.Join("..", "..")
|
||||||
|
var found []string
|
||||||
|
for _, dir := range []string{"cmd", "internal"} {
|
||||||
|
err := filepath.WalkDir(filepath.Join(root, dir), func(path string, e fs.DirEntry, err error) error {
|
||||||
|
if err != nil || e.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rel = filepath.ToSlash(rel)
|
||||||
|
if strings.HasPrefix(rel, "internal/recordmirror/") || slices.Contains(mayNameAMirror, rel) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := parser.ParseFile(token.NewFileSet(), path, nil, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ast.Inspect(f, func(n ast.Node) bool {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
pkg, ok := n.X.(*ast.Ident)
|
||||||
|
if ok && (pkg.Name == "broker" || pkg.Name == "recordmirror") && slices.Contains(names, n.Sel.Name) {
|
||||||
|
found = append(found, rel+": "+pkg.Name+"."+n.Sel.Name)
|
||||||
|
}
|
||||||
|
case *ast.BasicLit:
|
||||||
|
for _, b := range buckets {
|
||||||
|
if n.Kind == token.STRING && strings.Contains(n.Value, b) {
|
||||||
|
found = append(found, rel+": "+n.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(found) > 0 {
|
||||||
|
t.Fatalf("a package of the controller reads a record mirror, which only the audit and the mirror's "+
|
||||||
|
"writer may (ADR 0289 §19, ADR 0356 §2):\n %s", strings.Join(found, "\n "))
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -49,6 +49,7 @@
|
|||||||
./internal/inventory TestACredentialIssuedOutsideIsNeverMade
|
./internal/inventory TestACredentialIssuedOutsideIsNeverMade
|
||||||
./internal/inventory TestOneAcceptForAProviderIsSealedToEveryConsumer
|
./internal/inventory TestOneAcceptForAProviderIsSealedToEveryConsumer
|
||||||
./internal/inventory TestASharedOwnSecretIssuedOutsideIsNeverMade
|
./internal/inventory TestASharedOwnSecretIssuedOutsideIsNeverMade
|
||||||
|
./internal/recordmirror TestNatsAnAssignmentAndARegistrationEachChangeOneKeyAndRaiseOneEvent
|
||||||
./cmd/mesh-controller TestOneAcceptForAProviderIsPlacedForEveryConsumer
|
./cmd/mesh-controller TestOneAcceptForAProviderIsPlacedForEveryConsumer
|
||||||
./cmd/mesh-controller TestReplay164
|
./cmd/mesh-controller TestReplay164
|
||||||
./cmd/mesh-controller TestAnApprovedLayerOpensTheWalkThatSendsIt
|
./cmd/mesh-controller TestAnApprovedLayerOpensTheWalkThatSendsIt
|
||||||
@@ -61,4 +62,3 @@
|
|||||||
./cmd/mesh-controller TestAProviderElsewhereServesItsOwnComposedName
|
./cmd/mesh-controller TestAProviderElsewhereServesItsOwnComposedName
|
||||||
./cmd/mesh-controller TestAReusedCheckBuildIsRegisteredByItsDigestAndGatedAsABuiltOne
|
./cmd/mesh-controller TestAReusedCheckBuildIsRegisteredByItsDigestAndGatedAsABuiltOne
|
||||||
./internal/inventory TestAReportOfABusyContainerIsKeptAndReadBack
|
./internal/inventory TestAReportOfABusyContainerIsKeptAndReadBack
|
||||||
./internal/recordmirror TestNatsAnAssignmentAndARegistrationEachChangeOneKeyAndRaiseOneEvent
|
|
||||||
|
|||||||
Reference in New Issue
Block a user