Add the chain package and its buckets: builds, rollouts and assignments with history 64 (issue 1089)

internal/chain holds typed Build, Rollout and Assignment and a Backend with no delete or purge, in memory and on the bus. The builds and rollouts buckets are asserted with history 64, no age and values up to just under a mebibyte; the assignments bucket gets the same, keeping its marker age because a stream cannot be made to disallow message ages. Migration 0104 makes the one-row chain_moved switch. The controller keeps no consolidated schema, so 0104 is the whole schema change. Nothing reads the buckets yet.
This commit is contained in:
2026-10-18 02:39:11 +02:00
parent 54070f5bc3
commit fc55785956
15 changed files with 1175 additions and 9 deletions
+52 -1
View File
@@ -4,6 +4,8 @@ import (
"slices"
"strings"
"testing"
"github.com/nats-io/nats.go/jetstream"
)
// **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a
@@ -118,7 +120,7 @@ func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
rowed := map[string]bool{}
for _, row := range WritersTable {
if !strings.HasSuffix(row.State, " mirror") {
if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") {
continue
}
for _, subject := range row.Subjects {
@@ -254,3 +256,52 @@ func TestTheControllerMayAskMeshConfigInItsOwnName(t *testing.T) {
// **Delivering this change loses no merge** (review of issue 973): a merge published under the old filter of the
// `controller` consumer and never acknowledged by it is delivered by the merges consumer once the assertion has
// made it, because it starts where `controller` has acknowledged up to, not from now.
// **The chain's buckets keep sixty-four revisions and no age** (ADR 0408 §4 to §6, issue 1089): the bus's largest
// history, no age that could forget a key, a value up to just under the server's largest message, on disk.
func TestTheChainBucketsKeepSixtyFourRevisionsAndNoAge(t *testing.T) {
if want := []string{"mesh-controller_builds", "mesh-controller_rollouts", "mesh-controller_assignments"}; !slices.Equal(ChainBuckets(), want) {
t.Fatalf("the chain's buckets are %v, not %v", ChainBuckets(), want)
}
for _, bucket := range ChainBuckets() {
cfg := chainBucket(bucket, "why", ChainBuildsBytes, 0)
if cfg.History != 64 || cfg.TTL != 0 || cfg.Storage != jetstream.FileStorage {
t.Errorf("%s keeps %d revisions, age %v, on %v", bucket, cfg.History, cfg.TTL, cfg.Storage)
}
if cfg.MaxValueSize <= MirrorValueBytes || cfg.MaxValueSize >= 1<<20 {
t.Errorf("%s takes a value of %d bytes, not just under a mebibyte", bucket, cfg.MaxValueSize)
}
if cfg.Replicas > 1 {
t.Errorf("%s is replicated %d times", bucket, cfg.Replicas)
}
}
}
// **Every bucket of the chain is the controller's, listed for a reader, and has a writer row** (ADR 0408 §3).
func TestEveryChainBucketIsOnTheControllersListAndHasAWriterRow(t *testing.T) {
controller := Principal{Kind: KindController}
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
for _, bucket := range ChainBuckets() {
if !IsControllerBucket(bucket) || !slices.Contains(ControllerBuckets(), bucket) {
t.Errorf("%s is not on the controller's list of buckets", bucket)
}
local := strings.TrimPrefix(bucket, ControllerSeat+"_")
if !slices.Contains(ControllerMirrors(), local) {
t.Errorf("a module cannot name %s in `reads`", bucket)
}
rows := 0
for _, row := range WritersTable {
for _, subject := range row.Subjects {
if subject == "$KV."+bucket+".>" {
rows++
if row.Writes == nil || !row.Writes(controller, subject) || row.Writes(module, subject) {
t.Errorf("%q is not written by the controller alone", row.State)
}
}
}
}
if rows != 1 {
t.Errorf("%s has %d writer rows", bucket, rows)
}
}
}