package activator import "sync" // schedFence is one host's schedule write fence (zero-downtime spec ยง7.10, // final review F8). Every schedule write a control listener of the host // takes (a PUT's or DELETE's store checks and its mutation) runs inside it, // and an overlap's switch holds it exclusively: it waits for the writes in // flight, keeps new ones out, and latches the retiring generation in the // same step as the switch. A generation once retired stays refused, so a // handler that began before the switch, or that outlives the retiring // member's place in the operation set, can never write after it. // // Lock order: the switch takes the fence, then a.mu; a write holds no // lock while it runs, and never takes a.mu inside the fence, so the two // never invert. mu guards the fields; cond is on mu. type schedFence struct { mu sync.Mutex cond *sync.Cond writes int // schedule writes in flight switching bool // a switch holds the fence (or is waiting for its writes) retired map[uint64]bool // generations switched away from: latched } // schedFenceFor is host's fence, made on first use. func (a *Service) schedFenceFor(host string) *schedFence { a.schedMu.Lock() defer a.schedMu.Unlock() if a.schedFences == nil { a.schedFences = map[string]*schedFence{} } f := a.schedFences[host] if f == nil { f = &schedFence{retired: map[uint64]bool{}} f.cond = sync.NewCond(&f.mu) a.schedFences[host] = f } return f } // write runs w for generation gen unless gen is retired, and reports // whether it ran. It waits while a switch holds the fence. func (f *schedFence) write(gen uint64, w func()) bool { f.mu.Lock() for f.switching { f.cond.Wait() } if f.retired[gen] { f.mu.Unlock() return false } f.writes++ f.mu.Unlock() defer func() { f.mu.Lock() f.writes-- f.cond.Broadcast() f.mu.Unlock() }() w() return true } // lock takes the fence for a switch: one switch at a time, and none while // a write runs. Never call it with a.mu held. func (f *schedFence) lock() { f.mu.Lock() for f.switching { f.cond.Wait() } f.switching = true for f.writes > 0 { f.cond.Wait() } f.mu.Unlock() } // retire latches gen as switched away from. The caller holds the fence // (lock). func (f *schedFence) retire(gen uint64) { f.mu.Lock() f.retired[gen] = true f.mu.Unlock() } func (f *schedFence) unlock() { f.mu.Lock() f.switching = false f.cond.Broadcast() f.mu.Unlock() } // ScheduleWrite is the control listeners' write fence (schedule.WriteFence, // bound per listener through ExecBackend.ScheduleFence): it runs write for // a schedule write made on the control socket of host's generation gen, // unless an overlap's switch has retired that generation, and reports // whether it ran. The switch waits for a write in flight, so the retirement // decision and the mutation are one step against it. // // With swaps off nothing is ever retired and nothing is serialized: write // runs at once, as before. func (a *Service) ScheduleWrite(host string, gen uint64, write func()) bool { if !a.gens { write() return true } return a.schedFenceFor(host).write(gen, write) } // RetiredGen reports whether host's generation gen has been switched // away from (its schedule writes are refused for good). False with swaps // off. func (a *Service) RetiredGen(host string, gen uint64) bool { if !a.gens { return false } f := a.schedFenceFor(host) f.mu.Lock() defer f.mu.Unlock() return f.retired[gen] }