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
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.
278 lines
8.3 KiB
Go
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)
|
|
}
|