Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/reserve.go
T
jschoubben 71a35a6376 MV1: move contracts and sdk under modules/, importers follow
The delivery repository's contracts and SDK live under modules/ as
mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths
renamed to match the folders (the TypeScript names already did), and
every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools
and mesh-sdk follows in the same group: a design as it should be, one
path per module, checked by the group's check building every importer.

Builds: design 47 G §427, §88

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 05:09:56 +01:00

560 lines
22 KiB
Go

package versions
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
)
// **The reservation is one key per module and MAJOR.MINOR** (design 47 G §369, §52): the builder keeps in its old state
// section `reserved` the version its build reserved and each artifact's digest once built, written by compare-and-set at
// the build's start. A newer build of the module takes the next after the higher of the highest released and the one
// reserved, so two builds never share a version, and a superseded build's version is never released (a gap in the
// patches). The key is deleted when the catalog says `version-released` for it, or when a version of another MAJOR.MINOR
// of the module is released (the manifest moved on). This is no list of released versions: numbering reads those only from
// the catalog (§348), handed to Reserve by the caller.
const (
// ReservedState is the builder's old state section of the reservations.
ReservedState = "reserved"
// ReservedBucket is its bucket.
ReservedBucket = "mesh-delivery-builder_" + ReservedState
// ReservedTool is builder_reserved, ReservedDropTool builder_reserved_drop.
ReservedTool = "builder_reserved"
ReservedDropTool = "builder_reserved_drop"
// UnreleasedBound is the publish's bound: past it a built reservation with an artifact unpublished is the finding
// version-unreleased (§369).
UnreleasedBound = 10 * time.Minute
// VersionUnreleased is that finding.
VersionUnreleased = "version-unreleased"
)
// ReservedVersion is the value of a reservation key.
type ReservedVersion struct {
V int `json:"v"`
Module string `json:"module"`
Series string `json:"series"` // MAJOR.MINOR
Version string `json:"version"` // MAJOR.MINOR.PATCH
// Artifacts are the module's artifacts at the version: the digest once built, empty before.
Artifacts map[string]string `json:"artifacts"`
// BuiltAt is when the build ended with every digest known; the publish's bound counts from it.
BuiltAt time.Time `json:"built-at,omitzero"`
// Published are the artifacts a registry seat said published at this version with the built digest.
Published map[string]bool `json:"published,omitempty"`
At time.Time `json:"at"`
// Repository and Commit are what the build was made from, and Tag the git tag its release is written as (a Go
// package's, GoTagOf): empty for a module with no Go package (design 47 G §370).
Repository string `json:"repository,omitempty"`
Commit string `json:"commit,omitempty"`
Tag string `json:"tag,omitempty"`
// Fingerprint is the fingerprint of the catalog entry the reservation was made from, so a publish again reads that
// entry (design 47 G A-76).
Fingerprint string `json:"fingerprint,omitempty"`
// Unstored are the artifacts a holder refused as not stored, with its words: said once, and not published again on
// events (design 47 G §588); builder_build_again builds the key again, which reserves anew.
Unstored map[string]string `json:"unstored,omitempty"`
// Branch and Source are the watched branch and the source revision of the catalog revision whose entry the build came from
// (design 47 G §371, A-57): what `package-published` says of the publication.
Branch string `json:"branch,omitempty"`
Source int64 `json:"source,omitempty"`
// Dependencies are, by artifact reference, what the step's result says each artifact resolved (kind 1) and was built with
// (kind 3), kept for `package-published` (design 47 G §466, §673, A-57).
Dependencies map[string][]meshbuilds.Dependency `json:"dependencies,omitempty"`
}
// Source is what a build reserving a version is made from (design 47 G §370): the repository and commit the git tag is
// written at, and whether the module has a Go package, whose version is tagged.
type Source struct {
Repository, Commit string
// Fingerprint is the fingerprint of the catalog entry the build is made from (design 47 G A-76).
Fingerprint string
GoPackage bool
// Branch is the watched branch of the catalog revision whose entry the build came from, and Revision the source revision of
// that catalog revision (design 47 G §371, A-57), kept on the reservation for the publication.
Branch string
Revision int64
}
// ReservedKey is the key of module's reservation of series (MAJOR.MINOR).
func ReservedKey(module, series string) string { return Key(module, series) }
// ErrNotFilled is what Reserve answers before the catalog says versions-filled: nothing is reserved until then (§369).
var ErrNotFilled = errors.New("versions-filled is not heard: nothing is reserved before the catalog's versions are filled")
// ReserveStore is the reservations' bucket: compare-and-set, and deletion.
type ReserveStore interface {
Store
Delete(ctx context.Context, key string) error
}
// Reserver reserves versions.
type Reserver struct {
Store ReserveStore
// ReleasedOf are the patch versions (MAJOR.MINOR.PATCH) the catalog holds released for module, from its verb versions.
ReleasedOf func(ctx context.Context, module string) ([]string, error)
// Filled says the catalog said versions-filled.
Filled func() bool
Now func() time.Time
}
func (r *Reserver) now() time.Time {
if r.Now != nil {
return r.Now().UTC()
}
return time.Now().UTC()
}
// SplitVersion is a MAJOR.MINOR.PATCH version's series and patch.
func SplitVersion(version string) (series string, patch int, err error) {
i := strings.LastIndex(version, ".")
if i < 0 || strings.Count(version, ".") != 2 {
return "", 0, fmt.Errorf("%q is not MAJOR.MINOR.PATCH", version)
}
patch, err = strconv.Atoi(version[i+1:])
if err != nil || patch < 0 {
return "", 0, fmt.Errorf("%q is not MAJOR.MINOR.PATCH", version)
}
return version[:i], patch, nil
}
// Reserve reserves the next version of module's series for a build that starts, for artifacts (their names), and answers
// it. The next patch is the one after the higher of the highest released and the one reserved, 0 when there is neither. A
// conflicting write (another build reserved first) is read again and the next taken.
func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts []string, src Source) (ReservedVersion, error) {
if r.Filled != nil && !r.Filled() {
return ReservedVersion{}, ErrNotFilled
}
released, err := r.ReleasedOf(ctx, module)
if err != nil {
return ReservedVersion{}, fmt.Errorf("the released versions of %s cannot be read: %w", module, err)
}
high := -1
for _, v := range released {
if s, p, err := SplitVersion(v); err == nil && s == series && p > high {
high = p
}
}
key := ReservedKey(module, series)
for {
cur, rev, found, err := r.get(ctx, key)
if err != nil {
return ReservedVersion{}, err
}
next := high
if found {
if _, p, err := SplitVersion(cur.Version); err == nil && p > next {
next = p
}
}
out := ReservedVersion{V: 1, Module: module, Series: series, Version: fmt.Sprintf("%s.%d", series, next+1), Artifacts: map[string]string{}, At: r.now(),
Repository: src.Repository, Commit: src.Commit, Fingerprint: src.Fingerprint, Branch: src.Branch, Source: src.Revision}
if src.GoPackage {
out.Tag = GoTagOf(out.Version)
}
for _, a := range artifacts {
out.Artifacts[a] = ""
}
raw, _ := json.Marshal(out)
if found {
_, err = r.Store.Update(ctx, key, raw, rev)
} else {
_, err = r.Store.Create(ctx, key, raw)
}
if errors.Is(err, ErrConflict) {
continue
}
if err != nil {
return ReservedVersion{}, fmt.Errorf("%s cannot be reserved: %w", key, err)
}
return out, nil
}
}
func (r *Reserver) get(ctx context.Context, key string) (ReservedVersion, uint64, bool, error) {
raw, rev, found, err := r.Store.Get(ctx, key)
if err != nil || !found {
return ReservedVersion{}, 0, false, err
}
var v ReservedVersion
if err := json.Unmarshal(raw, &v); err != nil {
return ReservedVersion{}, 0, false, fmt.Errorf("%s holds what is not a reservation: %w", key, err)
}
return v, rev, true, nil
}
// Built records artifact's digest under the reserved version of module's series; once every artifact has one the build is
// ended (BuiltAt). A reservation moved on to another version (a newer build took the next) is left alone and answers false.
func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest string, deps []meshbuilds.Dependency) (bool, error) {
series, _, err := SplitVersion(version)
if err != nil {
return false, err
}
key := ReservedKey(module, series)
for {
cur, rev, found, err := r.get(ctx, key)
if err != nil || !found || cur.Version != version {
return false, err
}
cur.Artifacts[artifact] = digest
if len(deps) > 0 {
if cur.Dependencies == nil {
cur.Dependencies = map[string][]meshbuilds.Dependency{}
}
cur.Dependencies[artifact] = deps
}
if cur.BuiltAt.IsZero() && allSet(cur.Artifacts) {
cur.BuiltAt = r.now()
}
raw, _ := json.Marshal(cur)
if _, err := r.Store.Update(ctx, key, raw, rev); errors.Is(err, ErrConflict) {
continue
} else if err != nil {
return false, fmt.Errorf("%s cannot be written: %w", key, err)
}
return true, nil
}
}
func allSet(m map[string]string) bool {
for _, d := range m {
if d == "" {
return false
}
}
return true
}
// Released takes the catalog's version-released in: the key of that version is deleted, and every key of module's other
// series (the manifest moved on). A key holding a newer reserved version of the same series stays. It answers the keys
// deleted.
func (r *Reserver) Released(ctx context.Context, module, version string) ([]string, error) {
series, _, err := SplitVersion(version)
if err != nil {
return nil, err
}
keys, err := r.Store.KeysOf(ctx, NameFilter(module))
if err != nil {
return nil, fmt.Errorf("the reservations of %s cannot be listed: %w", module, err)
}
var gone []string
for _, k := range keys {
cur, _, found, err := r.get(ctx, k)
if err != nil {
return gone, err
}
if !found || (cur.Series == series && cur.Version != version) {
continue
}
if err := r.Store.Delete(ctx, k); err != nil {
return gone, fmt.Errorf("%s cannot be deleted: %w", k, err)
}
gone = append(gone, k)
}
sort.Strings(gone)
return gone, nil
}
// Unreleased are the reservations whose build ended more than UnreleasedBound before now with an artifact unpublished:
// the finding version-unreleased, naming the module, the version and the artifacts missing (published says whether an
// artifact is). A reservation whose build has not ended is not one.
type Unreleased struct {
Module, Version string
Missing []string
}
// Overdue answers the reservations past the bound with an artifact not published.
func (r *Reserver) Overdue(ctx context.Context, published func(module, version, artifact string) bool) ([]Unreleased, error) {
keys, err := r.Store.KeysOf(ctx, AllFilter)
if err != nil {
return nil, fmt.Errorf("the reservations cannot be listed: %w", err)
}
sort.Strings(keys)
var out []Unreleased
for _, k := range keys {
cur, _, found, err := r.get(ctx, k)
if err != nil {
return nil, err
}
if !found || cur.BuiltAt.IsZero() || r.now().Sub(cur.BuiltAt) < UnreleasedBound {
continue
}
var missing []string
for _, a := range cur.Missing() {
if published == nil || !published(cur.Module, cur.Version, a) {
missing = append(missing, a)
}
}
if len(missing) > 0 {
out = append(out, Unreleased{Module: cur.Module, Version: cur.Version, Missing: missing})
}
}
return out, nil
}
// All are the reservations, sorted by key: builder_reserved.
func (r *Reserver) All(ctx context.Context) ([]ReservedVersion, error) {
keys, err := r.Store.KeysOf(ctx, AllFilter)
if err != nil {
return nil, fmt.Errorf("the reservations cannot be listed: %w", err)
}
sort.Strings(keys)
var out []ReservedVersion
for _, k := range keys {
cur, _, found, err := r.get(ctx, k)
if err != nil {
return nil, err
}
if found {
out = append(out, cur)
}
}
return out, nil
}
// Drop deletes module's reservation of series on a person's word (builder_reserved_drop); the caller writes the hand-act
// log. It answers whether there was one.
func (r *Reserver) Drop(ctx context.Context, module, series string) (bool, error) {
key := ReservedKey(module, series)
_, _, found, err := r.get(ctx, key)
if err != nil || !found {
return false, err
}
return true, r.Store.Delete(ctx, key)
}
// Marked is an artifact of a reservation a registry seat's published newly marked: the reservation as it stands then.
type Marked struct {
Reserved ReservedVersion
Artifact string
}
// PackagePublished is the seat mesh-builds' package-published of one artifact the holder said published at the reservation's
// version (design 47 G §52, §334, §673, A-57): the artifact reference, the version and the digest published, the watched branch and
// the source revision the build was made from, and the dependencies its step result named.
func (m Marked) PackagePublished(digest string, at time.Time) meshbuilds.PackagePublished {
at = at.UTC()
return meshbuilds.PackagePublished{V: 1, Artifact: m.Artifact, At: &at, Branch: m.Reserved.Branch, Source: m.Reserved.Source, Module: m.Reserved.Module,
Version: m.Reserved.Version, Digest: digest, Dependencies: m.Reserved.Dependencies[m.Artifact]}
}
// Published takes a registry seat's published in: the artifacts of a reservation at version (a leading v is the Go form)
// whose built digest it names are marked published. It answers each artifact it newly marked, with the reservation as marked.
func (r *Reserver) Published(ctx context.Context, version, digest string) ([]Marked, error) {
version = strings.TrimPrefix(version, "v")
if version == "" || digest == "" {
return nil, nil
}
keys, err := r.Store.KeysOf(ctx, AllFilter)
if err != nil {
return nil, fmt.Errorf("the reservations cannot be listed: %w", err)
}
sort.Strings(keys)
var marked []Marked
for _, k := range keys {
again:
cur, rev, found, err := r.get(ctx, k)
if err != nil {
return marked, err
}
if !found || cur.Version != version {
continue
}
changed := false
var arts []string
for a, d := range cur.Artifacts {
if d == digest && !cur.Published[a] {
if cur.Published == nil {
cur.Published = map[string]bool{}
}
cur.Published[a], changed = true, true
arts = append(arts, a)
}
}
sort.Strings(arts)
if !changed {
continue
}
raw, _ := json.Marshal(cur)
if _, err := r.Store.Update(ctx, k, raw, rev); errors.Is(err, ErrConflict) {
goto again
} else if err != nil {
return marked, fmt.Errorf("%s cannot be written: %w", k, err)
}
for _, a := range arts {
marked = append(marked, Marked{Reserved: cur, Artifact: a})
}
}
return marked, nil
}
// Of is module's reservation of series, and whether there is one; with an empty series the module's highest.
func (r *Reserver) Of(ctx context.Context, module, version string) (ReservedVersion, bool, error) {
all, err := r.All(ctx)
if err != nil {
return ReservedVersion{}, false, err
}
var out ReservedVersion
found := false
for _, v := range all {
if v.Module != module || (version != "" && v.Version != version) {
continue
}
if !found {
out, found = v, true
continue
}
_, p, _ := SplitVersion(v.Version)
_, q, _ := SplitVersion(out.Version)
if v.Series > out.Series || (v.Series == out.Series && p > q) {
out = v
}
}
return out, found, nil
}
const reservedBoundPrefix = "reserved."
// ReservedBoundKey is the bound of a reservation's publish in the state bounds (key-ttl): `reserved.<reserved key>`, armed
// when its build ended with every digest known. Its end while the reservation stands is the finding version-unreleased.
func ReservedBoundKey(module, series string) string {
return ReservedBoundFor(ReservedKey(module, series))
}
// ReservedBoundFor is the bound of the reservation under key.
func ReservedBoundFor(key string) string { return reservedBoundPrefix + key }
// ReservedBoundOf is the reserved key a reservation's bound names, and whether key is one.
func ReservedBoundOf(key string) (string, bool) { return strings.CutPrefix(key, reservedBoundPrefix) }
// readOnlyReserve is the reservations read by the tools bundle, which writes nothing.
type readOnlyReserve struct{ readStore }
func (readOnlyReserve) Delete(context.Context, string) error { return errReadOnly }
// AnswerReserved is builder_reserved: every reservation, or one module's.
func AnswerReserved(r Reader, module string) (map[string]any, error) {
rs := &Reserver{Store: readOnlyReserve{readStore{r}}}
all, err := rs.All(context.Background())
if err != nil {
return nil, err
}
out := []ReservedVersion{}
for _, v := range all {
if module == "" || v.Module == module {
out = append(out, v)
}
}
return map[string]any{"reserved": out}, nil
}
// AnswerReservedVerb is the seat mesh-builds' verb reserved: module's reserved version (the given one, else the highest)
// with each artifact's digest, an artifact not built yet with none.
func AnswerReservedVerb(r Reader, module, version string) (meshbuilds.ReservedAnswer, error) {
module = strings.TrimSpace(module)
if module == "" {
return meshbuilds.ReservedAnswer{}, errors.New("module is required")
}
rs := &Reserver{Store: readOnlyReserve{readStore{r}}}
v, found, err := rs.Of(context.Background(), module, strings.TrimSpace(version))
if err != nil {
return meshbuilds.ReservedAnswer{}, err
}
if !found {
return meshbuilds.ReservedAnswer{}, fmt.Errorf("%s has no reserved version%s", module, map[bool]string{true: " " + version, false: ""}[version != ""])
}
out := meshbuilds.ReservedAnswer{Module: v.Module, Version: v.Version, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{}}
names := make([]string, 0, len(v.Artifacts))
for a := range v.Artifacts {
names = append(names, a)
}
sort.Strings(names)
for _, a := range names {
out.Artifacts = append(out.Artifacts, meshbuilds.ReservedAnswerArtifactsItem{Artifact: a, Digest: v.Artifacts[a]})
}
return out, nil
}
// CatalogVersions answers a module's released versions, the seat mesh-catalog's verb versions (design 47 G §582).
type CatalogVersions func(ctx context.Context, module string) ([]meshcatalog.VersionEntry, error)
// AnswerReservedByName is the verb reserved given a package's name (design 47 G §572, §582, A-70): the module is found from the
// name in the catalog half's view (carriers); that module's reserved version answers as AnswerReservedVerb does. A version
// given with no reservation of it (a reservation goes once its version is released) is asked of the catalog, and answers
// released with the artifacts and digests the release recorded. A name no entry carries, more than one entry carries, or a
// version neither reserved nor released is answered error, naming which, set alone.
func AnswerReservedByName(ctx context.Context, r Reader, carriers func(context.Context, string) ([]Built, error), released CatalogVersions, name, version string) (meshbuilds.ReservedAnswer, error) {
name, version = strings.TrimSpace(name), strings.TrimSpace(version)
got, err := carriers(ctx, name)
if err != nil {
return meshbuilds.ReservedAnswer{}, fmt.Errorf("the entries of %s cannot be read: %w", name, err)
}
switch {
case len(got) == 0:
return meshbuilds.ReservedAnswer{Error: "no entry carries the name " + name}, nil
case len(got) > 1:
return meshbuilds.ReservedAnswer{Error: MoreThanOne(name, got)}, nil
}
module := got[0].Module
rs := &Reserver{Store: readOnlyReserve{readStore{r}}}
if _, found, err := rs.Of(ctx, module, version); err != nil {
return meshbuilds.ReservedAnswer{}, err
} else if found {
return AnswerReservedVerb(r, module, version)
}
if version != "" {
entries, err := released(ctx, module)
if err != nil {
return meshbuilds.ReservedAnswer{}, fmt.Errorf("the released versions of %s cannot be read: %w", module, err)
}
for _, e := range entries {
if e.Version != version {
continue
}
out := meshbuilds.ReservedAnswer{Module: module, Version: version, Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{}}
for _, a := range e.Artifacts {
out.Artifacts = append(out.Artifacts, meshbuilds.ReservedAnswerArtifactsItem{Artifact: a.Artifact, Digest: a.Digest})
}
return out, nil
}
return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no version %s reserved or released", name, module, version)}, nil
}
return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no reserved version", name, module)}, nil
}
// The descriptions of the reservation's tools.
const (
ReservedDescription = "The versions the builder reserved (its state reserved, design 47 G section 369): one per module and MAJOR.MINOR, " +
"the version its build reserved at its start, each artifact's digest once built, when the build ended and which artifacts a registry " +
"seat said published. With module, that module's only. A key goes when the catalog says version-released for it. Read-only."
ReservedDropDescription = "Drop a module's reservation of one MAJOR.MINOR by hand (design 47 G section 369), when its version will never be " +
"released; the next build takes the next after the highest released. A hand act: it is recorded in the hand-act log."
// ReservedVerb is the seat mesh-builds' verb served as a tool of the same name.
ReservedVerb = meshbuilds.VerbReserved
)
// Missing are the artifacts of v no registry seat said published, sorted.
func (v ReservedVersion) Missing() []string {
var out []string
for a := range v.Artifacts {
if !v.Published[a] {
out = append(out, a)
}
}
sort.Strings(out)
return out
}
// At is the reservation under key, and whether there is one.
func (r *Reserver) At(ctx context.Context, key string) (ReservedVersion, bool, error) {
v, _, found, err := r.get(ctx, key)
return v, found, err
}