Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retakeask.go
T
jschoubben b3f7054345 Builder: the operator's ask uses mesh-sdk's operatorask asker
The builder's own lookup of the seat holder and read of the router's record (routerBucket, recordVia in
cmd/mesh-delivery-builder/retake.go) are deleted: the ask is published and the router's decided heard
through operatorask.Asker, and each warrant is held to its ask and acted on once by the operator-channel
client's Take.

Builds: design 47 G A-101 (one operator-channel asker in mesh-sdk), §700, A-74
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 01:05:42 +01:00

172 lines
6.4 KiB
Go

package build
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/operatorask"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"mesh-delivery-builder/internal/versions"
)
// **The operator is asked once a key's event builds are spent** (design 47 G §568, ADR 0673 §3): the seat operator-channel's ask,
// level approve, with the options *Build once more* and *Leave it*, `about` the key so a newer ask replaces it (ADR 0666 §12),
// naming the module, the fingerprint and the last error whole. *Build once more* builds one revision as a person's build
// (BuildAgain, which starts the count again); *Leave it*, a decline or the ask's expiry leaves the key failed.
// The ids of the ask's options.
const (
OptionBuildOnceMore = "build-once-more"
OptionLeaveIt = "leave-it"
)
// AskLasts is how long the ask stays open: the longest an ask that approves may (the seat's own bound).
const AskLasts = occ.ApproveLasts
// askBuildBound is how long the builder waits for the claim and the asks of the build *Build once more* starts.
const askBuildBound = 15 * time.Second
// OperatorAsk asks the operator about a key and carries out the answer.
type OperatorAsk struct {
// Client keeps the builder's asks and the warrants it acted on in its state `asked`, checks each ask and holds each
// warrant to the ask it answers; its Publish is Asker's.
Client *occ.Client
// Asker is mesh-sdk's asker on the seat operator-channel (design 47 G A-101): the ask published under the builder's own
// name and the router's `decided` heard; no copy of it is the builder's.
Asker *operatorask.Asker[oc.Warrant]
// Acted keeps which key an ask was made for, beside the revision of the key it was made at (Retaker.Acted).
Acted versions.ReserveStore
Builder *Builder
// Ctx is the builder's own context: the build a person's answer starts lives on it.
Ctx context.Context
Now func() time.Time
Logf func(string, ...any)
}
func (o *OperatorAsk) logf(f string, a ...any) {
if o.Logf != nil {
o.Logf(f, a...)
}
}
func (o *OperatorAsk) now() time.Time {
if o.Now != nil {
return o.Now()
}
return time.Now()
}
// AskID is the id of the ask about one revision of a key: the same revision is the same ask, a newer one a new ask.
func AskID(key string, rev uint64) string {
sum := sha256.Sum256([]byte(fmt.Sprintf("%s@%d", key, rev)))
return "retake-" + hex.EncodeToString(sum[:16])
}
// askedOf is the acted key of the key an ask was made for.
func askedOf(id string) string { return versions.ActedKey("retake-ask:" + id) }
// BuildOnceMoreAct is what *Build once more* binds: the verb and the key's module and fingerprint.
func BuildOnceMoreAct(module, fingerprint string) occ.Act {
return occ.Act{"verb": "build-again", "module": module, "fingerprint": fingerprint}
}
// Compose is the ask about key as it stands, at revision rev.
func (o *OperatorAsk) Compose(key string, cur builds.BuilderBuild, rev uint64) (oc.Ask, error) {
module, fp, err := builds.ParseBuilderBuildKey(key)
if err != nil {
return oc.Ask{}, err
}
binds, err := occ.ActDigest(BuildOnceMoreAct(module, fp))
if err != nil {
return oc.Ask{}, err
}
headline := "Build " + module + " once more?"
if r := []rune(headline); len(r) > occ.HeadlineLength {
headline = string(r[:occ.HeadlineLength-1]) + "…"
}
last := []rune(cur.Error)
if len(last) > 120 {
last = append(last[:119], '…')
}
return oc.Ask{ID: AskID(key, rev), Headline: headline, Who: occ.Operator, About: key,
Explanation: fmt.Sprintf("The build of %s failed on a transport fault, and events built it again %d times without it passing.\nLast error: %s", module, cur.EventBuilds, string(last)),
Details: fmt.Sprintf("Key %s\nFingerprint %s\nLast error whole: %s", key, fp, cur.Error),
Expires: occ.FormatTime(o.now().Add(AskLasts)),
OnExpiry: "the key stays failed until builder_build_again or a new fingerprint",
Options: []oc.Option{
{ID: OptionBuildOnceMore, Label: "Build once more", Does: "the builder builds this fingerprint once more, and counts the build as a person's", Level: occ.Approve, Binds: binds},
{ID: OptionLeaveIt, Label: "Leave it", Does: "nothing: the key stays failed", Level: occ.Acknowledge},
}}, nil
}
// Ask asks the operator about key at revision rev. The same revision asked twice is one ask.
func (o *OperatorAsk) Ask(ctx context.Context, key string, cur builds.BuilderBuild, rev uint64) error {
a, err := o.Compose(key, cur, rev)
if err != nil {
return err
}
k := askedOf(a.ID)
if _, _, found, err := o.Acted.Get(ctx, k); err != nil {
return err
} else if !found {
if _, err := o.Acted.Create(ctx, k, []byte(key)); err != nil && !errors.Is(err, versions.ErrConflict) {
return err
}
}
// An ask published and not marked asked (the write after it failed) is read again: it is one ask.
if e, err := o.Client.State.Get("ask." + a.ID); err != nil {
return err
} else if e != nil {
return nil
}
return o.Client.Ask(a)
}
// Listen hears the operator's answers through the sdk asker: each warrant of this builder's asks is acted on once.
func (o *OperatorAsk) Listen() error {
return o.Asker.Hear(func(w oc.Warrant) error {
h, err := o.Client.Take(w, o.act)
if h == "" && err != nil {
return err // the state could not be reached: offered again
}
if err != nil {
o.logf("the answer to the ask %s: %s: %v", w.Ask, h, err)
return nil
}
o.logf("the answer to the ask %s: %s", w.Ask, h)
return nil
})
}
// act carries out the option the operator chose: Build once more builds the key as a person's build, Leave it nothing.
func (o *OperatorAsk) act(opt oc.Option, w oc.Warrant) error {
if opt.ID != OptionBuildOnceMore {
return nil
}
raw, _, found, err := o.Acted.Get(o.Ctx, askedOf(w.Ask))
if err != nil {
return err
}
if !found {
return fmt.Errorf("the builder holds no key the ask %s was made for", w.Ask)
}
module, fp, err := builds.ParseBuilderBuildKey(string(raw))
if err != nil {
return err
}
o.logf("%s: the operator chose %s; built again as a person's build", raw, opt.Label)
go func() {
if res := o.Builder.BuildAgain(o.Ctx, module, fp, "the operator chose "+opt.Label, "operator-channel", askBuildBound); res.Refused != "" {
o.logf("%s: not built again: %s", raw, res.Refused)
}
}()
return nil
}