Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/floor.go
T
jschoubben 39db267630 D1 (builder): the builder names its own module and buckets from its credential's module
Its states' buckets, its finding observer, its build log entries and its step asks' asker take the credential's module (tools: the module the runtime serves them as) instead of constants of its name; builds.StateBucket/BuildsBucket replace the Bucket constants. The readers of the builds bucket (checker, view, grants) still name the builder: the next D1 subtask finds it by the seat mesh-builds.

Builds: design 47 G A-296, §428

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 22:34:08 +01:00

332 lines
11 KiB
Go

package versions
import (
"context"
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"sync"
"time"
)
// FloorSources are the three sources of a name's floor (design 51 *The floor*): each a tool call that answers the
// versions it knows of a name, or an error when it does not answer.
type FloorSources struct {
// Seat is `seat:<registry>.versions` of the registry seat the name is published to.
Seat func(ctx context.Context, seat, name string) ([]string, error)
// Tags is the seat git's tags: the git tags of the repository the name is built from, under prefix.
Tags func(ctx context.Context, repository, prefix string) ([]string, error)
// Requirements are the versions of a package the catalog half's entries require (design 47 G A-119).
Requirements func(ctx context.Context, name string) ([]string, error)
}
// FloorOf names what a floor is asked for: the name on its seat, and the repository and tag prefix of its git tags.
type FloorOf struct{ Seat, Name, Repository, Prefix string }
// Ask asks the three sources of a name; a source that errs, or is not wired, is named in Silent with its error in why.
func (s FloorSources) Ask(ctx context.Context, of FloorOf) (Floor, map[string]string) {
seat, name := of.Seat, of.Name
var f Floor
why := map[string]string{}
ask := func(source string, call func() ([]string, error), into *[]string) {
if call == nil {
f.Silent, why[source] = append(f.Silent, source), "not asked: the builder has no path to it"
return
}
got, err := call()
if err != nil {
f.Silent, why[source] = append(f.Silent, source), err.Error()
return
}
*into = got
}
var seatCall, tagsCall, reqCall func() ([]string, error)
if s.Seat != nil {
seatCall = func() ([]string, error) { return s.Seat(ctx, seat, name) }
}
if s.Tags != nil {
tagsCall = func() ([]string, error) { return s.Tags(ctx, of.Repository, of.Prefix) }
}
if s.Requirements != nil {
reqCall = func() ([]string, error) { return s.Requirements(ctx, name) }
}
ask(SourceSeat, seatCall, &f.Seat)
ask(SourceTags, tagsCall, &f.Tags)
ask(SourceRequirements, reqCall, &f.Requirements)
return f, why
}
// What wakes a name waiting on its floor, never a timer (design 51, DLV-B3b): the source that was silent saying
// something, or its module's health condition clearing.
const (
// WakePublished is the holder's `published` on the builder's durable consumer of `<seat>.events`.
WakePublished = "published"
// WakeRevision is a catalog revision the builder hears (the catalog half's entry changed): tags and requirements are asked again.
WakeRevision = "catalog-revision"
// WakeAccepted is the next `manifest-accepted` of the seat mesh-catalog.
WakeAccepted = "manifest-accepted"
// WakeCleared is the condition keeper's clear of a silent module's health condition on `mesh-conditions`.
WakeCleared = "condition-cleared"
// WakeByHand is builder_floor_again, a hand act.
WakeByHand = "by hand"
)
// sourceOfWake is the floor source each event says something of; a clear names the source by its module.
var sourceOfWake = map[string]string{WakePublished: SourceSeat, WakeAccepted: SourceRequirements}
// SourceSeats are the seats whose holders are the floor's tag and requirement sources (design 47 G A-82): the seat git (tags)
// and the seat mesh-catalog (requirements), the holders read from the seat list and the catalog half.
var SourceSeats = map[string]string{SourceTags: "git", SourceRequirements: "mesh-catalog"}
// FloorWait is one name waiting on its floor: the sources still silent and why, since when; the finding the builder
// writes names them, cleared once all three answer.
type FloorWait struct {
Name string `json:"name"`
Seat string `json:"seat"`
Silent []string `json:"silent"`
Why map[string]string `json:"why,omitempty"`
Since time.Time `json:"since"`
// Holder is the module holding the name's seat: its condition's clear wakes the seat source.
Holder string `json:"holder,omitempty"`
}
// Finding is the floor finding's words.
func (w FloorWait) Finding() string {
var parts []string
for _, s := range w.Silent {
p := s
if why := w.Why[s]; why != "" {
p += " (" + why + ")"
}
parts = append(parts, p)
}
return fmt.Sprintf("the floor of %s is not known since %s: %s did not answer; nothing of it is numbered until all three answer",
w.Name, w.Since.UTC().Format(time.RFC3339), strings.Join(parts, ", "))
}
// The builder's state floor-waits (DLV-B4): each name waiting on its floor, keyed by its escaped name, so a restarted
// builder still knows which names its silent sources' events wake; builder_floor_waits reads it.
const (
FloorWaitsState = "floor-waits"
FloorWaitsTool = "builder_floor_waits"
)
// FloorWaitKey is a wait's key in floor-waits.
func FloorWaitKey(name string) string { return EscapeToken(name) }
// FloorKeeper keeps the waits on the bus: Put a wait's value, Delete its key.
type FloorKeeper interface {
Put(key string, value []byte) error
Delete(key string) error
}
// FloorWaits are the names waiting on their floor. Wake answers which names to ask again for an event, Answered drops
// a name once all three answered; the builder's finding for each is the wait's Finding. Once Kept, every change is
// written to the state floor-waits as well.
type FloorWaits struct {
mu sync.Mutex
waits map[string]FloorWait
keeper FloorKeeper
// Logf says a wait that could not be kept; nil says nothing.
Logf func(string, ...any)
// Holder finds the module holding a seat (the catalog half's claim); a source's module is the holder of its SourceSeats seat.
Holder func(seat string) (string, bool)
// Said is told each wait recorded (raised) and each answered (not raised): the builder's finding floor-silent.
Said func(w FloorWait, raised bool)
}
// FloorSilent is the kind of the floor finding.
const FloorSilent = "floor-silent"
// Keep binds the state: the waits it holds that are not known here are taken in (a restarted builder's), every wait
// known here is written to it, and every change from now on is.
func (fw *FloorWaits) Keep(k FloorKeeper, held []FloorWait) {
fw.mu.Lock()
if fw.waits == nil {
fw.waits = map[string]FloorWait{}
}
for _, w := range held {
if _, had := fw.waits[w.Name]; !had && w.Name != "" {
fw.waits[w.Name] = w
}
}
fw.keeper = k
all := make([]FloorWait, 0, len(fw.waits))
for _, w := range fw.waits {
all = append(all, w)
}
fw.mu.Unlock()
for _, w := range all {
fw.put(k, w)
}
}
func (fw *FloorWaits) put(k FloorKeeper, w FloorWait) {
if k == nil {
return
}
data, err := json.Marshal(w)
if err == nil {
err = k.Put(FloorWaitKey(w.Name), data)
}
if err != nil && fw.Logf != nil {
fw.Logf("the floor wait of %s is not kept in %s (a restart forgets it until its next claim): %v", w.Name, FloorWaitsState, err)
}
}
// DecodeFloorWait reads one value of floor-waits.
func DecodeFloorWait(raw []byte) (FloorWait, error) {
var w FloorWait
if err := json.Unmarshal(raw, &w); err != nil {
return FloorWait{}, err
}
if w.Name == "" {
return FloorWait{}, errors.New("a floor wait names no name")
}
return w, nil
}
// Wait records that name's floor is not known.
func (fw *FloorWaits) Wait(name, seat, holder string, f Floor, why map[string]string, now time.Time) FloorWait {
fw.mu.Lock()
if fw.waits == nil {
fw.waits = map[string]FloorWait{}
}
w, had := fw.waits[name]
if !had {
w.Since = now
}
w.Name, w.Seat, w.Holder, w.Silent, w.Why = name, seat, holder, slices.Clone(f.Silent), why
fw.waits[name] = w
k := fw.keeper
fw.mu.Unlock()
fw.put(k, w)
if fw.Said != nil {
fw.Said(w, true)
}
return w
}
// Answered drops name: its floor is known. It says whether the name waited, so its finding is cleared.
func (fw *FloorWaits) Answered(name string) bool {
fw.mu.Lock()
w, had := fw.waits[name]
delete(fw.waits, name)
k := fw.keeper
fw.mu.Unlock()
if had && fw.Said != nil {
fw.Said(w, false)
}
if had && k != nil {
if err := k.Delete(FloorWaitKey(name)); err != nil && fw.Logf != nil {
fw.Logf("the floor wait of %s is not cleared from %s: %v", name, FloorWaitsState, err)
}
}
return had
}
// FloorWaitsAnswer is what builder_floor_waits answers: every name waiting on its floor, oldest first, with its finding.
type FloorWaitsAnswer struct {
Waits []FloorWaitRead `json:"waits"`
}
// FloorWaitRead is one wait as builder_floor_waits shows it.
type FloorWaitRead struct {
FloorWait
Finding string `json:"finding"`
}
// AnswerFloorWaits is builder_floor_waits, read from the state floor-waits.
func AnswerFloorWaits(r Reader) (FloorWaitsAnswer, error) {
keys, err := r.Keys()
if err != nil {
return FloorWaitsAnswer{}, err
}
out := FloorWaitsAnswer{Waits: []FloorWaitRead{}}
for _, k := range keys {
raw, ok, err := r.Get(k)
if err != nil {
return FloorWaitsAnswer{}, err
}
if !ok {
continue
}
w, err := DecodeFloorWait(raw)
if err != nil {
return FloorWaitsAnswer{}, fmt.Errorf("%s.%s: %w", FloorWaitsState, k, err)
}
out.Waits = append(out.Waits, FloorWaitRead{FloorWait: w, Finding: w.Finding()})
}
sort.Slice(out.Waits, func(i, j int) bool {
if !out.Waits[i].Since.Equal(out.Waits[j].Since) {
return out.Waits[i].Since.Before(out.Waits[j].Since)
}
return out.Waits[i].Name < out.Waits[j].Name
})
return out, nil
}
// FloorWaitsDescription is builder_floor_waits' description.
const FloorWaitsDescription = "The names waiting on their floor (the builder's state floor-waits, design 51 *The floor*), oldest first: " +
"each name, its registry seat, the sources still silent and why, since when, and the finding the builder says for it. " +
"Nothing of a waiting name is numbered until all three sources answer; builder_floor_again asks one again by hand. Read-only."
// Wake answers the names to ask their floor again on an event: kind one of the Wake constants; seat the seat a
// `published` came on; module the module whose condition cleared. A name is woken only when the event says something
// of a source it waits on; WakeByHand with a name wakes that name alone.
func (fw *FloorWaits) Wake(kind, seat, module, name string) []string {
fw.mu.Lock()
defer fw.mu.Unlock()
var out []string
for n, w := range fw.waits {
switch kind {
case WakeByHand:
if n == name {
out = append(out, n)
}
case WakeCleared:
for _, s := range w.Silent {
if fw.holds(SourceSeats[s], module) || (s == SourceSeat && w.Holder != "" && w.Holder == module) {
out = append(out, n)
break
}
}
default:
src := sourceOfWake[kind]
if kind == WakeRevision {
if slices.Contains(w.Silent, SourceTags) || slices.Contains(w.Silent, SourceRequirements) {
out = append(out, n)
}
} else if slices.Contains(w.Silent, src) && (kind != WakePublished || seat == "" || seat == w.Seat) {
out = append(out, n)
}
}
}
sort.Strings(out)
return out
}
// Waiting is every wait, by name.
func (fw *FloorWaits) Waiting() []FloorWait {
fw.mu.Lock()
defer fw.mu.Unlock()
out := make([]FloorWait, 0, len(fw.waits))
for _, w := range fw.waits {
out = append(out, w)
}
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
return out
}
// holds is whether module holds seat on the catalog half.
func (fw *FloorWaits) holds(seat, module string) bool {
if seat == "" || module == "" || fw.Holder == nil {
return false
}
holder, ok := fw.Holder(seat)
return ok && holder == module
}