package schedule import ( "context" "sync" ) // HostGate is the one per-host gate shared by the three writers of a host's // timer rows (moves spec §9.4, round-2 finding 21): the runner holds a host's // read side for the whole of a delivery, the converger holds it across its // Frozen check and its writes, and a move's drain takes the write side once // the row is held. A drain therefore returns only when no delivery or // convergence that could have missed the hold is still running, and from // then on the hold check inside the gate refuses both. // // Entry is TryRLock, never RLock: Go's RWMutex blocks new readers once a // writer waits, and a drain can wait for a delivery of up to DeliveryTimeout. // A blocking entry would park the tick loop, and with it every other host's // timers, or the converger's pass, behind one held host. A refused entry is // a host being drained, which is a host about to be frozen: it is skipped // this tick or pass, with no bookkeeping. // // A nil *HostGate is inert (every entry succeeds, a drain returns at once): // the runner and converger with moves off are main's. type HostGate struct { mu sync.Mutex hosts map[string]*sync.RWMutex } func (g *HostGate) lock(host string) *sync.RWMutex { g.mu.Lock() defer g.mu.Unlock() if g.hosts == nil { g.hosts = map[string]*sync.RWMutex{} } l := g.hosts[host] if l == nil { l = &sync.RWMutex{} g.hosts[host] = l } return l } // tryEnter takes host's read side without waiting. false: a drain holds the // host or waits for it. func (g *HostGate) tryEnter(host string) bool { if g == nil { return true } return g.lock(host).TryRLock() } // leave ends an entry tryEnter granted. func (g *HostGate) leave(host string) { if g == nil { return } g.lock(host).RUnlock() } // Drain waits until every delivery and convergence in flight for host has // finished, then lets the host go: the cut's drain, called after SetHold // (moves spec §9.4 "drained"). Work that starts afterwards re-reads the row // or the freeze inside the gate and stops. ctx bounds the wait; an ended // context returns its error, and the pending drain still completes in the // background (until then the host's new work is refused, which is what a // held host wants anyway). func (g *HostGate) Drain(ctx context.Context, host string) error { if g == nil { return nil } l := g.lock(host) done := make(chan struct{}) go func() { // The barrier is the acquisition itself: nothing runs inside it. l.Lock() l.Unlock() close(done) }() select { case <-done: return nil case <-ctx.Done(): return ctx.Err() } }