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
107 lines
3.3 KiB
Go
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()
|
|
}
|
|
}
|
|
}
|