Files
mesh-delivery/modules/mesh-delivery-builder/internal/versions/store.go
T
jschoubben 5fbd84091e DLV-B6: a check's build of a head a kept tag ask names numbers its packages tagged at its claim
The builder reads tag-asks for the candidate's repository and head (versions.TagFor), numbers only its packages under
the tag below the next version, keeps each with its repository for the branch's end, and says numbered with the head;
the same fingerprint claimed again keeps its number (design 51 *Tagged versions*).

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

231 lines
7.6 KiB
Go

package versions
import (
"context"
"errors"
"fmt"
"time"
)
// AllFilter is the key filter of every version of every name: two tokens.
const AllFilter = "*.*"
// ErrConflict is what a Store answers when a Create finds the key or an Update finds another revision.
var ErrConflict = errors.New("the key exists or changed")
// Store is the bucket: create and compare-and-set, as the builder's builds bucket (internal/build.Store).
type Store interface {
Get(ctx context.Context, key string) (value []byte, revision uint64, found bool, err error)
Create(ctx context.Context, key string, value []byte) (uint64, error)
Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)
// KeysOf are the keys under a filter (NameFilter, or AllFilter for every key).
KeysOf(ctx context.Context, filter string) ([]string, error)
}
// Versions is the builder's state.
type Versions struct {
Store Store
Now func() time.Time
}
// Of is every entry of name.
func (v Versions) Of(ctx context.Context, name string) ([]Entry, error) {
keys, err := v.Store.KeysOf(ctx, NameFilter(name))
if err != nil {
return nil, fmt.Errorf("the versions of %s cannot be listed: %w", name, err)
}
var out []Entry
for _, k := range keys {
raw, _, found, err := v.Store.Get(ctx, k)
if err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}
if !found {
continue
}
e, err := Decode(raw)
if err != nil {
return nil, fmt.Errorf("%s holds what is not a version: %w", k, err)
}
out = append(out, e)
}
return InOrder(out), nil
}
// Claim is one numbering asked of the builder.
type Claim struct {
Name, Kind, Series, Module, Fingerprint, Commit, Revision, Digest, Seat string
// Repository is the module's repository, kept on a tagged version for its branch's end.
Repository string
// Tag asks a tagged version for this cleaned tag; empty for a trunk version.
Tag string
Merge uint64
Floor Floor
// Fresh numbers anew a fingerprint or revision already numbered: builder_publish_again after a published (design 51
// *Tools of the builder*).
Fresh bool
}
// Number numbers a claim under compare-and-set: two claims of one name never get one number, the second reading the
// first's key and taking the next (ADR 0606 §2). It answers the decision and, when it numbered or kept a version, its
// entry. Attempts are bounded by the claims the name can meet at once; a store that keeps conflicting is an error.
func (v Versions) Number(ctx context.Context, c Claim) (Decision, Entry, error) {
if c.Series == "" {
c.Series = DefaultSeries
}
for attempt := 0; attempt < 16; attempt++ {
entries, err := v.Of(ctx, c.Name)
if err != nil {
return Decision{}, Entry{}, err
}
var d Decision
if c.Tag != "" {
if !c.Floor.Known() && len(entries) == 0 {
return Decision{Wait: true, Why: "the tagged version waits for its floor"}, Entry{}, nil
}
ver := Tagged(entries, c.Series, c.Tag, c.Floor)
d = Decision{Version: ver, Publish: true, Why: "tagged " + ver}
for _, e := range entries {
// The same fingerprint claimed again for its tag (a check's build looked at again) keeps its number.
if e.Tag == CleanTag(c.Tag) && e.Fingerprint == c.Fingerprint && e.State != Removed {
d = Decision{Version: e.Version, Keep: true, Publish: true, Why: "tagged " + e.Version + ", kept for this fingerprint"}
break
}
}
} else {
d = Decide(fresh(entries, c), c.Kind, c.Series, c.Fingerprint, c.Revision, c.Floor)
}
if d.Version == "" {
return d, Entry{}, nil
}
if d.Keep {
for _, e := range entries {
if e.Version == d.Version {
return d, e, nil
}
}
}
e := Entry{Name: c.Name, Kind: c.Kind, Series: c.Series, Version: d.Version, Tag: c.Tag, Module: c.Module,
Fingerprint: c.Fingerprint, Commit: c.Commit, Revision: c.Revision, Digest: c.Digest, Seat: c.Seat,
State: Numbered, At: v.now(), Merge: c.Merge}
if c.Tag != "" {
e.Repository = c.Repository
}
raw, err := Encode(e)
if err != nil {
return Decision{}, Entry{}, err
}
_, err = v.Store.Create(ctx, Key(c.Name, d.Version), raw)
if errors.Is(err, ErrConflict) {
continue // another claim took this number: read again and take the next
}
if err != nil {
return Decision{}, Entry{}, fmt.Errorf("%s@%s: %w", c.Name, d.Version, err)
}
return d, e, nil
}
return Decision{}, Entry{}, fmt.Errorf("%s: the versions kept changing under 16 claims", c.Name)
}
// Mark moves one version to a new state by compare-and-set, as the holders' events say (published, refused, removed):
// change edits the entry read; a version the state does not hold is an error naming it.
func (v Versions) Mark(ctx context.Context, name, version string, change func(*Entry)) (Entry, error) {
key := Key(name, version)
for attempt := 0; attempt < 16; attempt++ {
raw, rev, found, err := v.Store.Get(ctx, key)
if err != nil {
return Entry{}, fmt.Errorf("%s: %w", key, err)
}
if !found {
return Entry{}, fmt.Errorf("%s@%s is not numbered by the builder", name, version)
}
e, err := Decode(raw)
if err != nil {
return Entry{}, fmt.Errorf("%s holds what is not a version: %w", key, err)
}
change(&e)
e.At = v.now()
out, err := Encode(e)
if err != nil {
return Entry{}, err
}
if _, err := v.Store.Update(ctx, key, out, rev); errors.Is(err, ErrConflict) {
continue
} else if err != nil {
return Entry{}, fmt.Errorf("%s: %w", key, err)
}
return e, nil
}
return Entry{}, fmt.Errorf("%s@%s kept changing under 16 marks", name, version)
}
// Recorded is the holder's question through builder_versions (ADR 0605 §4): whether name, version and digest are a
// version the builder numbered. It answers the entry when they are, and why not otherwise.
func (v Versions) Recorded(ctx context.Context, name, version, digest string) (Entry, bool, string, error) {
raw, _, found, err := v.Store.Get(ctx, Key(name, version))
if err != nil {
return Entry{}, false, "", err
}
if !found {
return Entry{}, false, fmt.Sprintf("%s@%s is not a version the builder numbered", name, version), nil
}
e, err := Decode(raw)
if err != nil {
return Entry{}, false, "", err
}
switch {
case e.State == Removed:
return e, false, fmt.Sprintf("%s@%s was removed (%s)", name, version, e.Reason), nil
case e.Digest != "" && digest != "" && e.Digest != digest:
return e, false, fmt.Sprintf("%s@%s is recorded with digest %s, not %s", name, version, e.Digest, digest), nil
}
return e, true, "", nil
}
func (v Versions) now() time.Time {
if v.Now != nil {
return v.Now().UTC()
}
return time.Now().UTC()
}
// fresh is entries as Decide reads them for c: with Fresh, the entries of c's fingerprint and revision still count for the
// next number but keep none.
func fresh(entries []Entry, c Claim) []Entry {
if !c.Fresh {
return entries
}
out := make([]Entry, len(entries))
for i, e := range entries {
if e.Tag == "" && (e.Fingerprint == c.Fingerprint || (c.Revision != "" && e.Revision == c.Revision)) {
e.Fingerprint, e.Revision = "", ""
}
out[i] = e
}
return out
}
// All is every entry of every name, by name in order: what builder_untag and the tag asks read.
func (v Versions) All(ctx context.Context) ([]Entry, error) {
keys, err := v.Store.KeysOf(ctx, AllFilter)
if err != nil {
return nil, fmt.Errorf("the versions cannot be listed: %w", err)
}
var out []Entry
for _, k := range keys {
raw, _, found, err := v.Store.Get(ctx, k)
if err != nil {
return nil, fmt.Errorf("%s: %w", k, err)
}
if !found {
continue
}
e, err := Decode(raw)
if err != nil {
return nil, fmt.Errorf("%s holds what is not a version: %w", k, err)
}
out = append(out, e)
}
return out, nil
}