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() } } }