Files
mesh-catalog/modules/mesh-build-go/buildstep/narrate.go
T
jschoubben 37a1f2be53 CAT-NR1: every build narrates its asks of a seat in one place
asked, waiting on the seat every five minutes, stored or refused, as the ask's
phase on its step key, so a publish or a copy that stands still is readable on
the bus. PublishOne and PublishFiles narrate for every build module; the image
copy and the late publish use the same narration.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:05:27 +01:00

107 lines
3.3 KiB
Go

package buildstep
// **A build narrates its asks of a seat** (CAT-NR1; design 51 *Publishing*): asked, waiting on the seat (said again every
// NarrateEvery while it waits), stored, refused. Each line is the ask's phase (phase.go): logged with the ask's id and
// written to its running step key, so the builder and the delivery view read from the bus where a publish or a copy
// stands; outside an ask's run (a late publish, a tool called by hand) it is logged. One place for every build module:
// PublishOne and PublishFiles narrate through it, and a step that waits on a seat's event (an image's copy) uses
// NarrateSeat itself.
import (
"context"
"fmt"
"log"
"time"
)
// NarrateEvery is how often a build says again that it still waits on a seat.
var NarrateEvery = 5 * time.Minute
// narrateTicks is the clock of the waiting lines; a test hands its own.
var narrateTicks = func(every time.Duration) (<-chan time.Time, func()) {
t := time.NewTicker(every)
return t.C, t.Stop
}
// SeatNarration is one ask of a build on a seat, said as it moves.
type SeatNarration struct {
ctx context.Context
seat, what string
asked time.Time
tick <-chan time.Time
stop func()
}
// NarrateSeat says that the build asked seat for what (a verb and its object, such as "publish of nats/server") and
// starts the waiting lines; Stored or Refused ends it.
func NarrateSeat(ctx context.Context, seat, what string) *SeatNarration {
n := &SeatNarration{ctx: ctx, seat: seat, what: what, asked: now()}
n.tick, n.stop = narrateTicks(NarrateEvery)
n.say("asked %s: %s", seat, what)
return n
}
// Waiting is the clock a waiting step selects on beside its outcome; on each tick it calls StillWaiting.
func (n *SeatNarration) Waiting() <-chan time.Time { return n.tick }
// StillWaiting says that the build still waits on the seat, and since when.
func (n *SeatNarration) StillWaiting() {
n.say("waiting on %s: %s, %s since asked", n.seat, n.what, now().Sub(n.asked).Round(time.Second))
}
// Stored says that the seat holds what the build asked it to keep (how: where, or what travelled).
func (n *SeatNarration) Stored(how string) {
n.stop()
if how != "" {
how = " (" + how + ")"
}
n.say("stored on %s: %s%s", n.seat, n.what, how)
}
// Refused says that the seat did not keep it, and why.
func (n *SeatNarration) Refused(err error) {
n.stop()
n.say("refused by %s: %s: %v", n.seat, n.what, err)
}
func (n *SeatNarration) say(format string, a ...any) {
line := fmt.Sprintf(format, a...)
if r, ok := n.ctx.Value(phaseKey{}).(*phases); ok && r != nil {
r.enter(line)
return
}
log.Print(line)
}
// OnSeat runs call as one narrated ask of the build on seat: asked, waiting every NarrateEvery while call runs, then
// stored (how(answer)) or refused (its error).
func OnSeat[T any](ctx context.Context, seat, what string, call func() (T, error), how func(T) string) (T, error) {
n := NarrateSeat(ctx, seat, what)
type answer struct {
v T
err error
}
done := make(chan answer, 1)
go func() {
v, err := call()
done <- answer{v, err}
}()
for {
select {
case a := <-done:
if a.err != nil {
n.Refused(a.err)
} else {
h := ""
if how != nil {
h = how(a.v)
}
n.Stored(h)
}
return a.v, a.err
case <-n.Waiting():
n.StillWaiting()
}
}
}