Files
mesh-delivery/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go
T
jschoubben 6b3a679151 DLV-B5: builder_publish_again, builder_floor_again, builder_tag, builder_tag_asks and builder_untag, with the state tag-asks
A holder that failed leaves a version to publish again by hand: said numbered again while the build is held, built
again under the same number when its file is gone, numbered anew after a published; refused for other bytes. The
floor is asked again by hand; a branch's tagged versions are removed through each seat's remove, per repository.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:30:20 +01:00

295 lines
9.7 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-builder/internal/build"
"mesh-delivery-builder/internal/versions"
)
// The floor's two module tools (novox/hq design 51 *The floor*); the seat's is `seat:<registry>.versions`.
const (
bridgeTagsTool = "mesh-delivery-forge-bridge.bridge_tags"
requirementsTool = "mesh-delivery-indexer.indexer_requirements"
)
// awaitedVersions is the state versions as versions.Store: bound once the bus holds it (the grants part makes it, issue
// 1790); until then every use is an error, so a trunk build is built unnumbered and said (build.Builder.number).
type awaitedVersions struct{ b *process.AwaitedBucket }
var errVersionsMissing = errors.New("the state " + versions.Bucket + " is not on the bus yet")
func (s awaitedVersions) kv() (jetstream.KeyValue, error) {
if kv := s.b.KV(); kv != nil {
return kv, nil
}
return nil, errVersionsMissing
}
func (s awaitedVersions) Get(ctx context.Context, key string) ([]byte, uint64, bool, error) {
kv, err := s.kv()
if err != nil {
return nil, 0, false, err
}
e, err := kv.Get(ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, 0, false, nil
}
if err != nil {
return nil, 0, false, err
}
return e.Value(), e.Revision(), true, nil
}
func versionsConflict(err error) error {
if errors.Is(err, jetstream.ErrKeyExists) || errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
return versions.ErrConflict
}
return err
}
func (s awaitedVersions) Create(ctx context.Context, key string, value []byte) (uint64, error) {
kv, err := s.kv()
if err != nil {
return 0, err
}
rev, err := kv.Create(ctx, key, value)
return rev, versionsConflict(err)
}
func (s awaitedVersions) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) {
kv, err := s.kv()
if err != nil {
return 0, err
}
rev, err := kv.Update(ctx, key, value, revision)
return rev, versionsConflict(err)
}
func (s awaitedVersions) KeysOf(ctx context.Context, filter string) ([]string, error) {
kv, err := s.kv()
if err != nil {
return nil, err
}
l, err := kv.ListKeysFiltered(ctx, filter)
if err != nil {
return nil, err
}
defer func() { _ = l.Stop() }()
var out []string
for k := range l.Keys() {
out = append(out, k)
}
return out, nil
}
// floorVia is the floor's three sources as calls of the builder's own account (design 51 *The floor*): the seat's verb
// versions, the forge bridge's bridge_tags and the indexer's indexer_requirements. A call that fails is that source
// silent.
func floorVia(call toolCaller) versions.FloorSources {
return versions.FloorSources{
Seat: func(_ context.Context, seat, name string) ([]string, error) {
raw, err := call("seat:"+seat+".versions", map[string]any{"name": name})
if err != nil {
return nil, err
}
var a struct {
Versions []struct {
Version string `json:"version"`
} `json:"versions"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("%s's versions answered what is no list of versions: %w", seat, err)
}
var out []string
for _, v := range a.Versions {
if v.Version != "" {
out = append(out, v.Version)
}
}
return out, nil
},
Tags: func(_ context.Context, repository, prefix string) ([]string, error) {
raw, err := call(bridgeTagsTool, map[string]any{"repository": repository, "prefix": prefix})
if err != nil {
return nil, err
}
var a struct {
Tags []string `json:"tags"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("bridge_tags answered what is no list of tags: %w", err)
}
return a.Tags, nil
},
Requirements: func(_ context.Context, name string) ([]string, error) {
raw, err := call(requirementsTool, map[string]any{"package": name})
if err != nil {
return nil, err
}
var a struct {
Requirements []string `json:"requirements"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return nil, fmt.Errorf("indexer_requirements answered what is no list of versions: %w", err)
}
return a.Requirements, nil
},
}
}
// emitNumbered says the builder's event version.numbered (design 51 *Events of the builder*), on its own family apart
// from its builds' ends; the name and version are the bus's dedup id.
func emitNumbered(ctx context.Context, js jetstream.JetStream, cred process.Account, b versions.NumberedBody) error {
id := "numbered-" + versions.Key(b.Name, b.Version)
if b.Again != "" {
id += "-again-" + b.Again
}
if err := emitEvent(ctx, js, cred, versions.NumberedEvent, id, b); err != nil {
return fmt.Errorf("saying %s for %s@%s: %w", versions.NumberedEvent, b.Name, b.Version, err)
}
return nil
}
// emitEvent says one of the builder's events, mesh.mod.<module>.event.<event>, with the SDK's envelope headers and id as
// the bus's dedup id.
func emitEvent(ctx context.Context, js jetstream.JetStream, cred process.Account, event, id string, body any) error {
data, err := json.Marshal(body)
if err != nil {
return err
}
msg := nats.NewMsg(cred.EventSubject(event))
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", cred.Module)
msg.Header.Set("x-node", cred.Node)
msg.Header.Set("x-time", time.Now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
msg.Data = data
pub, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
_, err = js.PublishMsg(pub, msg, jetstream.WithMsgID(id))
return err
}
// The events on the builder's consumer of EVENTS that wake a floor (design 51 *The floor*, DLV-B3b).
var (
manifestAcceptedSubject = "mesh.mod." + delivery.IndexerModule + ".event." + delivery.ManifestAcceptedEvent
conditionClearedSubject = "mesh.mod." + health.KeeperModule + ".event." + health.EventCleared
)
// floorWake asks the floor of the names an event wakes again, by looking at every module again: a trunk build is
// numbered before its claim (build.Builder.number).
type floorWake struct {
b *build.Builder
waits *versions.FloorWaits
logf func(string, ...any)
}
// heard takes a message of the consumer in when it is one that wakes a floor, acknowledged, and answers whether it was.
func (f *floorWake) heard(ctx context.Context, m jetstream.Msg) bool {
switch m.Subject() {
case manifestAcceptedSubject:
f.ack(m)
f.wake(ctx, versions.WakeAccepted, "", "")
case conditionClearedSubject:
f.ack(m)
var e health.Event
if err := json.Unmarshal(m.Data(), &e); err != nil {
f.logf("%s holds what is no condition, left: %v", m.Subject(), err)
return true
}
for _, module := range conditionModules(e.Subject.ID) {
f.wake(ctx, versions.WakeCleared, "", module)
}
default:
return false
}
return true
}
func (f *floorWake) ack(m jetstream.Msg) {
if err := m.Ack(); err != nil {
f.logf("acknowledging %s: %v", m.Subject(), err)
}
}
// wake looks at every module again when the event wakes a name, and says which.
func (f *floorWake) wake(ctx context.Context, kind, seat, module string) {
names := f.waits.Wake(kind, seat, module, "")
if len(names) == 0 {
return
}
f.logf("the floor of %s is asked again: %s", strings.Join(names, ", "), kind)
f.b.Reconsider(ctx)
}
// conditionModules are the tokens of a condition's subject id (`<node>/<module>`, `<module>.<check>`): a module named
// among them is the one whose condition cleared.
func conditionModules(id string) []string {
return strings.FieldsFunc(id, func(r rune) bool { return r == '/' || r == '.' || r == ':' })
}
// doneVia says whether a build key is done, read from the builder's own builds state: the build's file is held.
func doneVia(b *build.Builder) func(ctx context.Context, module, fp string) (bool, error) {
return func(ctx context.Context, module, fp string) (bool, error) {
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil {
return false, err
}
raw, _, found, err := b.Claimer.Store.Get(ctx, key)
if err != nil || !found {
return false, err
}
cur, err := delivery.DecodeBuilderBuild(raw)
if err != nil {
return false, err
}
return cur.State == delivery.BuildDone, nil
}
}
// removeVia calls a registry seat's remove for one tagged version, as the builder's own account (design 51 *Tagged
// versions*): the reason tag-gone; the seat says removed on its events.
func removeVia(call toolCaller) func(ctx context.Context, seat string, e versions.Entry) error {
return func(_ context.Context, seat string, e versions.Entry) error {
_, err := call("seat:"+seat+".remove", map[string]any{"name": e.Name, "version": e.Version, "tag": e.Tag, "reason": "tag-gone"})
return err
}
}
// tagAsked answers an unanswered value of tag-asks by compare-and-set at its revision, said in the log.
func tagAsked(ctx context.Context, acts versions.Acts, kv jetstream.KeyValue, e jetstream.KeyValueEntry, logf func(string, ...any)) {
if e.Operation() != jetstream.KeyValuePut {
return
}
t, err := versions.DecodeTagAsk(e.Value())
if err != nil {
logf("%s.%s holds what is not a tag ask, left: %v", versions.TagAsksState, e.Key(), err)
return
}
if !t.Answered.IsZero() {
return
}
t = acts.TagAsked(ctx, t, time.Now())
logf("%s: %s of %s %s by %s (%s): %s", e.Key(), map[bool]string{true: "untag", false: "tag ask"}[t.Untag], t.Repository, t.Branch, t.By, t.Why, t.Answer)
data, err := json.Marshal(t)
if err != nil {
return
}
if _, err := kv.Update(ctx, e.Key(), data, e.Revision()); err != nil && !errors.Is(err, jetstream.ErrKeyExists) && !errors.Is(err, jetstream.ErrKeyRevisionMismatch) {
logf("%s.%s: the answer could not be written: %v", versions.TagAsksState, e.Key(), err)
}
}