Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/retakeask.go
T
jschoubben 50cca4309f BL17c: the operator is asked once a key's event builds are spent; Build once more builds it as a person's build, Leave it nothing
The builder's asker on the seat operator-channel (level approve, about the key, state asked), the teller's three bus
adapters (publish, the router's record through the seat's holder on the seat list, decided heard through the hub's
events handler); the manifest uses the seat and consumes decided.

Builds: design 47 G §568, A-74
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:25:00 +01:00

163 lines
6.1 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-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 is the builder's asker on the seat operator-channel: its ask published under the builder's own name, its state
// `asked` (the asks it kept and the warrants it acted on), the router's record, the `decided` event.
Client *occ.Client
// 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: each warrant of this builder's asks is acted on once.
func (o *OperatorAsk) Listen() error {
return o.Client.Listen(o.act, func(h occ.Handled, w oc.Warrant, err error) {
if err != nil {
o.logf("the answer to the ask %s: %s: %v", w.Ask, h, err)
return
}
o.logf("the answer to the ask %s: %s", w.Ask, h)
})
}
// 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
}