Files
mesh-controller/internal/chain/chain.go
T
jschoubben c795915315
mesh/merge-gate head c79591531575 merged into trunk aa02e7bf4e78 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head c79591531575 merged into trunk aa02e7bf4e78 — pass: its merge-check.sh passed
mesh/delivery delivered
Keep builds on the bus under their module, so no lookup reads the bucket whole (issue 1121, ADR 0427)
Milestone 5 moved 8,599 build records onto the bus under bare build ids, each
carrying its module's whole manifest. Every build lookup then read the whole
bucket, and a delivery plan's send did that per module per machine while it
held the plans and the machine's declaration: the send of build-agent never
finished, and every other act and the status waited behind it.

Builds now live under <module>.build.<id> (requests under req.<id>, id.<id>
names a build's module), with pointer keys newest, worked, held,
commit.<sha> and source.<reuse key> that move only to a newer build and are
read as hints, repaired when wrong. Manifests are kept once in the same
bucket under manifest.sha256-<hex>, so no grant the running bus lacks is
needed. The old keys are laid out once at start, idempotently, and left
unread. The calls a plan look makes on the silent clocks while it holds the
plans are bounded, since the vendored kv.Keys ignores its context.
2026-10-18 13:32:31 +02:00

278 lines
8.3 KiB
Go

package chain
import (
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"github.com/novox/mesh-controller/internal/broker"
)
// Bucket is one kind of record of the chain, typed. Reads need nothing; **every write asks first whether this
// controller holds the lease** and writes nothing when it does not (ADR 0408 §2).
type Bucket[T any] struct {
b Backend
holds Holds
}
// Holds says whether this controller may write now: nil when it holds the lease, and why not when it does not. The
// serving controller answers from its lease, and a command from the holder's (the epoch gate).
type Holds func(ctx context.Context) error
// Kept is a record as the bucket holds it: with its revision, the one a write must find.
type Kept[T any] struct {
Key string
Value T
Revision uint64
}
// validKey is what the bus takes as a key: letters, digits and - _ / = . with no dot at either end.
var validKey = regexp.MustCompile(`^[-/_=.a-zA-Z0-9]+$`)
// ErrBadKey is a key the bus does not take: refused loudly, never rewritten into another.
var ErrBadKey = errors.New("the bus takes no key of that spelling")
// ValidKey says the bus takes the key: letters, digits and - _ / = . with no dot at either end.
func ValidKey(key string) bool {
return validKey.MatchString(key) && !strings.HasPrefix(key, ".") && !strings.HasSuffix(key, ".")
}
func (s Bucket[T]) guard(ctx context.Context, key string, value []byte) error {
if s.holds == nil {
return ErrNotHeld
}
if err := s.holds(ctx); err != nil {
return fmt.Errorf("%w: %v", ErrNotHeld, err)
}
if !ValidKey(key) {
return fmt.Errorf("%w: %q", ErrBadKey, key)
}
if len(value) > broker.ChainValueBytes {
return fmt.Errorf("%w: %d bytes, the most is %d", ErrTooLarge, len(value), broker.ChainValueBytes)
}
return nil
}
func decode[T any](e Entry) (Kept[T], error) {
var v T
if err := json.Unmarshal(e.Value, &v); err != nil {
return Kept[T]{}, fmt.Errorf("the value of %s cannot be read: %w", e.Key, err)
}
return Kept[T]{Key: e.Key, Value: v, Revision: e.Revision}, nil
}
// Get is one record; false when the key holds none.
func (s Bucket[T]) Get(ctx context.Context, key string) (Kept[T], bool, error) {
e, ok, err := s.b.Get(ctx, key)
if err != nil || !ok {
return Kept[T]{}, false, err
}
e.Key = key
k, err := decode[T](e)
return k, err == nil, err
}
// Create writes a key that holds nothing: ErrExists when it holds something. It returns the revision.
func (s Bucket[T]) Create(ctx context.Context, key string, v T) (uint64, error) {
body, err := json.Marshal(v)
if err != nil {
return 0, err
}
if err := s.guard(ctx, key, body); err != nil {
return 0, err
}
return s.b.Create(ctx, key, body)
}
// Update writes a key at the revision it was read at: ErrMoved when it moved, the key unchanged.
func (s Bucket[T]) Update(ctx context.Context, key string, v T, revision uint64) (uint64, error) {
body, err := json.Marshal(v)
if err != nil {
return 0, err
}
if err := s.guard(ctx, key, body); err != nil {
return 0, err
}
return s.b.Update(ctx, key, body, revision)
}
// All is every record, by key, read in one pass of a watch. An error is an error: never an empty bucket.
func (s Bucket[T]) All(ctx context.Context) (map[string]Kept[T], error) {
ctx, stop := context.WithCancel(ctx)
defer stop()
w, err := s.b.Watch(ctx)
if err != nil {
return nil, err
}
out := map[string]Kept[T]{}
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("reading the bucket: %w", ctx.Err())
case e, open := <-w:
if !open {
return nil, errors.New("the bucket stopped answering before it was read whole")
}
if e.Caught {
return out, nil
}
k, err := decode[T](e)
if err != nil {
return nil, err
}
out[e.Key] = k
}
}
}
// Filtered is every record whose key matches a subject pattern, by key, read in one pass of a watch the server filters.
func (s Bucket[T]) Filtered(ctx context.Context, filter string) (map[string]Kept[T], error) {
ctx, stop := context.WithCancel(ctx)
defer stop()
w, err := s.b.WatchFiltered(ctx, filter)
if err != nil {
return nil, err
}
out := map[string]Kept[T]{}
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("reading %s: %w", filter, ctx.Err())
case e, open := <-w:
if !open {
return nil, errors.New("the bucket stopped answering before it was read whole")
}
if e.Caught {
return out, nil
}
k, err := decode[T](e)
if err != nil {
return nil, err
}
out[e.Key] = k
}
}
}
// FilteredRaw is Filtered for keys that hold bytes of their own (a pointer holds a build's id), by key.
func (s Bucket[T]) FilteredRaw(ctx context.Context, filter string) (map[string]Entry, error) {
ctx, stop := context.WithCancel(ctx)
defer stop()
w, err := s.b.WatchFiltered(ctx, filter)
if err != nil {
return nil, err
}
out := map[string]Entry{}
for {
select {
case <-ctx.Done():
return nil, fmt.Errorf("reading %s: %w", filter, ctx.Err())
case e, open := <-w:
if !open {
return nil, errors.New("the bucket stopped answering before it was read whole")
}
if e.Caught {
return out, nil
}
out[e.Key] = e
}
}
}
// History is a key's earlier records and its current one, oldest first.
func (s Bucket[T]) History(ctx context.Context, key string) ([]Kept[T], error) {
all, err := s.b.History(ctx, key)
if err != nil {
return nil, err
}
out := make([]Kept[T], 0, len(all))
for _, e := range all {
e.Key = key
k, err := decode[T](e)
if err != nil {
return nil, err
}
out = append(out, k)
}
return out, nil
}
// Keys is every key of the bucket.
func (s Bucket[T]) Keys(ctx context.Context) ([]string, error) { return s.b.Keys(ctx) }
// Watch is the bucket's raw entries: its current values, a Caught mark, then each write.
func (s Bucket[T]) Watch(ctx context.Context) (<-chan Entry, error) { return s.b.Watch(ctx) }
// GetRaw is one key's value as the bucket holds it, for a writer that changes a field of a record and must keep the rest
// exactly as it is.
func (s Bucket[T]) GetRaw(ctx context.Context, key string) (Entry, bool, error) {
e, ok, err := s.b.Get(ctx, key)
e.Key = key
return e, ok, err
}
// CreateRaw writes a key's first value as bytes, with the same guards as Create.
func (s Bucket[T]) CreateRaw(ctx context.Context, key string, value []byte) (uint64, error) {
if err := s.guard(ctx, key, value); err != nil {
return 0, err
}
return s.b.Create(ctx, key, value)
}
// UpdateRaw writes a key's value as bytes, at the revision it was read at, with the same guards as Update.
func (s Bucket[T]) UpdateRaw(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) {
if err := s.guard(ctx, key, value); err != nil {
return 0, err
}
return s.b.Update(ctx, key, value, revision)
}
// Chain is the three buckets of the chain.
type Chain struct {
Builds Bucket[Build]
Rollouts Bucket[Rollout]
Assignments Bucket[Assignment]
// Manifests holds each build's manifest once, under the digest of its bytes (ManifestKey); Kept says it is on the bus. A
// chain without it keeps the manifest in the build's record, as the tests' chains do.
Manifests Bucket[json.RawMessage]
Kept bool
rollouts Backend
holds Holds
}
// WithManifests gives the chain its manifests bucket.
func (c *Chain) WithManifests(b Backend) *Chain {
c.Manifests = Bucket[json.RawMessage]{b: b, holds: c.holds}
c.Kept = true
return c
}
// RolloutsOf is the rollouts bucket typed as the controller's plan, which carries more than the chain's Rollout names:
// the same keys and values, read and written as the plan.
func RolloutsOf[T any](c *Chain) Bucket[T] { return Bucket[T]{c.rollouts, c.holds} }
// New is a chain over three backends, written only while holds says this controller may write. A nil holds writes nothing.
func New(builds, rollouts, assignments Backend, holds Holds) *Chain {
return &Chain{
Builds: Bucket[Build]{builds, holds},
Rollouts: Bucket[Rollout]{rollouts, holds},
Assignments: Bucket[Assignment]{assignments, holds},
rollouts: rollouts,
holds: holds,
}
}
// InMemory is a chain held in this process, for tests, written whenever holds says so; nil always holds.
func InMemory(holds Holds) *Chain {
if holds == nil {
holds = func(context.Context) error { return nil }
}
builds := NewMemory()
return New(builds, NewMemory(), NewMemory(), holds).WithManifests(builds)
}