Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/versions.go
T
jschoubben 64b918c64c DLV-B1, DLV-B2: the builder's state versions, numbering under compare-and-set per fingerprint or source revision, and builder_versions
The builder keeps one key per name, tag and version (design 51 *The versions*, ADR 0606), in its old state
section (ADR 0619), history one, 64 MiB with the 80 % mark. A package is numbered per source revision and keeps a
version held published, publishing nothing; one numbered but never stored is published again under it. An image,
bundle or archive keeps its fingerprint's number. An unknown floor builds an image, bundle or archive unnumbered and
makes a package wait (ADR 0617 §1). Merge order and tagged versions below the next version are the package's
helpers. builder_versions answers a name's versions and the holder's question (ADR 0605 §4).

Four claim tests merged into two, so the new table test adds no function (test budget).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 11:39:17 +01:00

409 lines
14 KiB
Go

// Package versions is the builder's state `versions` (novox/hq design 51 *The versions*, ADR 0606): one key per name,
// tag and version, written only by the builder under compare-and-set, read by others only through the builder's tool
// builder_versions or its events (ADR 0596 §2). It holds what each version is (its fingerprint, commit, source revision
// and digest) and where it stands: numbered, published, refused with its reason, or removed with its time, reason and the
// event that removed it.
//
// The state is declared in the builder's old `state` section (ADR 0619: the builder is one of the foundation's nine
// parts), history one; its cap is 64 MiB, the most the template state allows (ADR 0595 §3), and past 80 % of it the
// builder writes a finding, cleared below it (Fill).
package versions
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
// State is the builder state's name; Bucket the bucket the bus keeps it in.
const (
State = "versions"
Bucket = "mesh-delivery-builder_versions"
// Cap is the bucket's cap in bytes (64 MiB); FullPercent the share of it past which the builder writes its finding.
Cap = 64 << 20
FullPercent = 80
// DefaultSeries is the series of a name whose manifest declares none (design 32).
DefaultSeries = "0.1"
)
// The states a version is in.
const (
Numbered = "numbered"
Published = "published"
Refused = "refused"
Removed = "removed"
)
// The kinds the builder numbers: a package per source revision, the rest per fingerprint (design 51).
const (
KindPackage = "package"
KindImage = "image"
KindBundle = "bundle"
KindArchive = "archive"
)
// Entry is one key of the state: a version of a name, tagged or not.
type Entry struct {
Name string `json:"name"`
Kind string `json:"kind"`
Series string `json:"series"`
Version string `json:"version"`
// Tag is the cleaned tag of a tagged version (`feat-x`); empty for a trunk version.
Tag string `json:"tag,omitempty"`
Module string `json:"module"`
Fingerprint string `json:"fingerprint"`
Commit string `json:"commit,omitempty"`
// Revision is the source revision a package's version is numbered for (one version per revision, design 51).
Revision string `json:"revision,omitempty"`
// Digest is the digest the build recorded (an image's manifest, a blob's, Go's h1: hash).
Digest string `json:"digest,omitempty"`
// Seat is the registry seat it is published to.
Seat string `json:"seat,omitempty"`
State string `json:"state"`
At time.Time `json:"at"`
// Reason is why it was refused or removed: the holder's reason, a branch's end, retention, the sweep.
Reason string `json:"reason,omitempty"`
// RemovedBy is the id of the event that removed it.
RemovedBy string `json:"removed_by,omitempty"`
// Merge is the forge bridge's sequence of the merge that numbered it, for merge order.
Merge uint64 `json:"merge,omitempty"`
}
// Valid says what is wrong with an entry, or nil.
func (e Entry) Valid() error {
switch {
case strings.TrimSpace(e.Name) == "":
return errors.New("a version names no name")
case e.Version == "":
return fmt.Errorf("%s: a version with no version", e.Name)
}
switch e.State {
case Numbered, Published, Refused, Removed:
default:
return fmt.Errorf("%s@%s: unknown state %q", e.Name, e.Version, e.State)
}
switch e.Kind {
case KindPackage, KindImage, KindBundle, KindArchive:
default:
return fmt.Errorf("%s@%s: unknown kind %q", e.Name, e.Version, e.Kind)
}
if _, _, err := SplitSeries(e.Series); err != nil {
return fmt.Errorf("%s@%s: %w", e.Name, e.Version, err)
}
return nil
}
// Encode and Decode are the value's bytes.
func Encode(e Entry) ([]byte, error) {
if err := e.Valid(); err != nil {
return nil, err
}
return json.Marshal(e)
}
func Decode(raw []byte) (Entry, error) {
var e Entry
if err := json.Unmarshal(raw, &e); err != nil {
return Entry{}, err
}
return e, e.Valid()
}
// Key is the state's key of a name's version: two tokens, the name and the version each escaped (EscapeToken), so a
// name's versions are the keys under NameFilter and no name's keys fall under another's.
func Key(name, version string) string { return EscapeToken(name) + "." + EscapeToken(version) }
// NameFilter is the key filter of every version of name.
func NameFilter(name string) string { return EscapeToken(name) + ".*" }
// EscapeToken makes s one token of a bus key: letters, digits, '-', '_' and '/' stay; '=' and every other byte become
// '=' and two hex digits ('.' is "=2E", '@' "=40"). Two different strings never escape alike.
func EscapeToken(s string) string {
var b strings.Builder
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z', c >= '0' && c <= '9', c == '-', c == '_', c == '/':
b.WriteByte(c)
default:
fmt.Fprintf(&b, "=%02X", c)
}
}
return b.String()
}
// SplitSeries reads a series `<major>.<minor>`.
func SplitSeries(series string) (major, minor int, err error) {
a, b, ok := strings.Cut(series, ".")
if !ok {
return 0, 0, fmt.Errorf("series %q is not <major>.<minor>", series)
}
if major, err = strconv.Atoi(a); err != nil || major < 0 {
return 0, 0, fmt.Errorf("series %q is not <major>.<minor>", series)
}
if minor, err = strconv.Atoi(b); err != nil || minor < 0 {
return 0, 0, fmt.Errorf("series %q is not <major>.<minor>", series)
}
return major, minor, nil
}
// Patch is the count of a trunk version of series (`0.1.14` → 14), read leniently: a leading 'v' and a path prefix
// (`go/v0.1.13`) are dropped; ok is false for a version of another series or a tagged one.
func Patch(series, version string) (int, bool) {
if i := strings.LastIndex(version, "/"); i >= 0 {
version = version[i+1:]
}
version = strings.TrimPrefix(version, "v")
if strings.ContainsAny(version, "-+") {
return 0, false
}
rest, ok := strings.CutPrefix(version, series+".")
if !ok {
return 0, false
}
n, err := strconv.Atoi(rest)
if err != nil || n < 0 {
return 0, false
}
return n, true
}
// Floor is what the three sources answered for a name (design 51 *The floor*, ADR 0606 §3): the seat's versions, the
// package's git tags (the forge bridge's bridge_tags) and the pseudo-versions consumers require (the indexer's
// indexer_requirements). A source not answered is listed in Silent.
type Floor struct {
Seat []string `json:"seat,omitempty"`
Tags []string `json:"tags,omitempty"`
Requirements []string `json:"requirements,omitempty"`
// Silent names the sources that did not answer: "seat", "bridge_tags", "indexer_requirements".
Silent []string `json:"silent,omitempty"`
}
// The floor's sources, as Floor.Silent names them.
const (
SourceSeat = "seat"
SourceTags = "bridge_tags"
SourceRequirements = "indexer_requirements"
)
// Known is whether every source answered.
func (f Floor) Known() bool { return len(f.Silent) == 0 }
// Highest is the highest count of series the floor's three sources name, -1 when none names one. A pseudo-version
// (`v0.1.14-0.2026…-abc`) counts as the version below it was cut from, so its base (`0.1.13`) counts: Go orders it
// before `0.1.14`.
func (f Floor) Highest(series string) int {
high := -1
for _, list := range [][]string{f.Seat, f.Tags, f.Requirements} {
for _, v := range list {
n, ok := Patch(series, v)
if !ok {
n, ok = pseudoBase(series, v)
}
if ok && n > high {
high = n
}
}
}
return high
}
// pseudoBase is the count a Go pseudo-version stands on: `v0.1.14-0.<time>-<hash>` is cut after `v0.1.13`.
func pseudoBase(series, version string) (int, bool) {
version = strings.TrimPrefix(version, "v")
base, rest, ok := strings.Cut(version, "-")
if !ok || !strings.HasPrefix(rest, "0.") || strings.Count(rest, "-") != 1 {
return 0, false
}
n, ok := Patch(series, base)
if !ok || n == 0 {
return 0, false
}
return n - 1, true
}
// Next is the next trunk version of name in series: one above the highest of the trunk versions entries hold and the
// floor (design 51: a name with none starts at `<series>.0`).
func Next(entries []Entry, series string, floor Floor) string {
high := floor.Highest(series)
for _, e := range entries {
if e.Tag != "" || e.Series != series {
continue
}
if n, ok := Patch(series, e.Version); ok && n > high {
high = n
}
}
return fmt.Sprintf("%s.%d", series, high+1)
}
// Decision is what the builder does with a trunk build at its claim.
type Decision struct {
// Version is the version the build is published under; empty when it is built unnumbered (Unnumbered) or waits (Wait).
Version string
// Keep is true when the version is one entries already hold for this fingerprint or source revision.
Keep bool
// Publish is false only for a package's revision held published: it publishes nothing (design 51).
Publish bool
// Unnumbered: an image, bundle or archive whose floor is not known is built and published by its digest, numbered
// once the floor answers (ADR 0617 §1).
Unnumbered bool
// Wait: a package whose floor is not known waits for it (a package is published only under its version).
Wait bool
// Why is said in the build log.
Why string
}
// Decide numbers a trunk build of kind for name in series at fingerprint and source revision, from what entries hold of
// the name and what the floor answered (design 51 *The versions*, ADR 0606, ADR 0617 §1):
// - a package is numbered per source revision: a revision held published keeps its version and publishes nothing; a
// revision numbered or refused but never stored is published again under the same version;
// - an image, bundle or archive is numbered per fingerprint: a fingerprint already numbered keeps its number;
// - otherwise the next version, when the floor is known; else an image, bundle or archive is built unnumbered and a
// package waits.
func Decide(entries []Entry, kind, series, fingerprint, revision string, floor Floor) Decision {
for _, e := range latestFirst(entries) {
if e.Tag != "" || e.State == Removed {
continue
}
switch {
case kind == KindPackage && revision != "" && e.Revision == revision:
if e.State == Published {
return Decision{Version: e.Version, Keep: true, Why: fmt.Sprintf("version %s kept: source revision %s is published under it, and a package's content is its source", e.Version, revision)}
}
return Decision{Version: e.Version, Keep: true, Publish: true, Why: fmt.Sprintf("version %s kept: source revision %s was numbered %s but never stored, so it is published under it", e.Version, revision, e.State)}
case kind != KindPackage && e.Fingerprint == fingerprint:
return Decision{Version: e.Version, Keep: true, Publish: e.State != Published, Why: fmt.Sprintf("version %s kept: fingerprint %s is numbered under it", e.Version, fingerprint)}
}
}
if !floor.Known() {
silent := strings.Join(floor.Silent, ", ")
if kind == KindPackage {
return Decision{Wait: true, Why: "the package waits for its floor: " + silent + " did not answer"}
}
return Decision{Unnumbered: true, Publish: true, Why: "built unnumbered and published by its digest: the floor is not known (" + silent + " did not answer); numbered once it answers"}
}
v := Next(entries, series, floor)
return Decision{Version: v, Publish: true, Why: "numbered " + v}
}
// CleanTag makes a branch or tag name a semver prerelease identifier: lower case, every run of characters other than
// letters and digits one '-', none at either end; "tag" when nothing is left.
func CleanTag(tag string) string {
var b strings.Builder
dash := false
for _, r := range strings.ToLower(tag) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
dash = false
continue
}
if !dash && b.Len() > 0 {
b.WriteByte('-')
dash = true
}
}
out := strings.TrimSuffix(b.String(), "-")
if out == "" {
return "tag"
}
return out
}
// Tagged is the next tagged version of name for tag: below the next trunk version, never the latest
// (`0.1.15-feat-x.2`, design 51 *Tagged versions*); a package new in a group is numbered from its floor
// (`0.1.0-feat-x.1`).
func Tagged(entries []Entry, series, tag string, floor Floor) string {
base := Next(entries, series, floor)
tag = CleanTag(tag)
n := 0
prefix := base + "-" + tag + "."
for _, e := range entries {
if rest, ok := strings.CutPrefix(e.Version, prefix); ok {
if k, err := strconv.Atoi(rest); err == nil && k > n {
n = k
}
}
}
return fmt.Sprintf("%s%d", prefix, n+1)
}
// Order is merge order (design 51): a claim of a later merge of the same name waits until every earlier merge of it
// whose fingerprint the indexer still holds is numbered; a fingerprint replaced by a later merge before its claim is
// skipped. held is the fingerprints the indexer still holds for the name with their merge sequences. WaitsOn answers
// the earliest merge sequence the claim at merge waits on, 0 when it waits on none.
func WaitsOn(entries []Entry, merge uint64, held map[string]uint64) uint64 {
numbered := map[string]bool{}
for _, e := range entries {
if e.Tag == "" {
numbered[e.Fingerprint] = true
}
}
var wait uint64
for fp, seq := range held {
if seq >= merge || numbered[fp] {
continue
}
if wait == 0 || seq < wait {
wait = seq
}
}
return wait
}
// Fill is whether a bucket holding bytes of cap is past its finding's mark.
func Fill(bytes, capacity uint64) (share float64, full bool) {
if capacity == 0 {
return 0, false
}
share = float64(bytes) / float64(capacity)
return share, bytes*100 >= capacity*FullPercent
}
// InOrder sorts a name's entries by version, trunk versions by count and tagged ones after the trunk version they stand
// below: the order builder_versions answers.
func InOrder(entries []Entry) []Entry {
out := append([]Entry(nil), entries...)
sort.SliceStable(out, func(i, j int) bool {
ki, kj := orderKey(out[i]), orderKey(out[j])
if ki.n != kj.n {
return ki.n < kj.n
}
if ki.tagged != kj.tagged {
return ki.tagged // a tagged version stands below the trunk version of its count
}
return out[i].Version < out[j].Version
})
return out
}
type order struct {
n int
tagged bool
}
func orderKey(e Entry) order {
v := e.Version
tagged := false
if i := strings.IndexByte(v, '-'); i >= 0 {
v, tagged = v[:i], true
}
n, ok := Patch(e.Series, v)
if !ok {
n = -1
}
return order{n: n, tagged: tagged}
}
func latestFirst(entries []Entry) []Entry {
out := InOrder(entries)
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out
}