package main import ( "sync" "time" ) // How this owner keeps time (novox/hq ADR 0406 §1, issue 975). It has no tick, no forge poll and no reconcile loop: // a look (React) is taken after each event that can change what a step needs, and a step that waits on the clock — a // grace, a bound, a retry after a failure — says when it must be looked at again (lookAt). The one timer below is // armed for the earliest of those and fires one more look; it is never a tick, since a look that finds nothing // waiting arms nothing. // askAgainAfter is how long after a failed ask of the controller, the forge or the operator it is asked again: the // failure is the reason, and it is said in the log when it happens. const askAgainAfter = 30 * time.Second // lookWanted is the earliest time a look must be taken again, and the bounded step that wants it. type lookWanted struct { mu sync.Mutex at time.Time step string working bool timer *time.Timer } // lookAt says a bounded step, waiting on the clock, must be looked at again at a time. Safe to call with or without // the holder's lock. func (h *Holder) lookAt(at time.Time, step string) { h.wanted.mu.Lock() defer h.wanted.mu.Unlock() if h.wanted.at.IsZero() || at.Before(h.wanted.at) { h.wanted.at, h.wanted.step = at, step } } // again says an ask that failed is asked again after askAgainAfter: a bounded step retried by the look's timer. func (h *Holder) again(step string) { h.lookAt(h.Now().Add(askAgainAfter), step) } // Poke asks for a look; any number of pokes before it starts are one look. func (h *Holder) Poke() { select { case h.poke <- struct{}{}: default: } } // Work takes a look each time it is poked, for ever: the one goroutine that acts on events. func (h *Holder) Work() { h.wanted.mu.Lock() h.wanted.working = true h.wanted.mu.Unlock() for range h.poke { h.React() } } // React takes every step the facts allow, after an event that can change what they are: the forge's open pull requests // read when a check may have answered, the steps that need another's word (look), what is owed (Flush), and then // the conditions of what stalled (conditions.go), and then the time the next look is wanted at, if any step waits on the clock. Never concurrent with itself. func (h *Holder) React() { h.reactMu.Lock() defer h.reactMu.Unlock() h.wanted.mu.Lock() h.wanted.at, h.wanted.step = time.Time{}, "" h.wanted.mu.Unlock() if h.uncheckedDue() { h.wantForge() } if h.takeForgeWanted() { if err := h.SeeOpenPulls(); err != nil { h.wantForge() h.again("the forge's open pull requests, read when a check may have changed") } } h.look() h.Flush() h.sayConditions() h.arm() } // arm sets the timer for the earliest time a step wants a look at, or stops it when none does. func (h *Holder) arm() { h.wanted.mu.Lock() defer h.wanted.mu.Unlock() if !h.wanted.working { return } if h.wanted.timer != nil { h.wanted.timer.Stop() h.wanted.timer = nil } if h.wanted.at.IsZero() { return } wait := max(h.wanted.at.Sub(h.Now()), 100*time.Millisecond) // retries: the time-bound step the last look left waiting (a grace, a bound or an ask that failed), at its time h.wanted.timer = time.AfterFunc(wait, h.Poke) }