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
172 lines
6.4 KiB
Go
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
|
|
}
|