package alert import ( "errors" "fmt" "sync" "sync/atomic" ) const ( inactive = uint32(iota) active alerting dataToActuatorDefaultBuffer = 1 // PreAllocCommsDefaultBuffer is the default buffer size for comms PreAllocCommsDefaultBuffer = 5 ) var ( // pool is a silent shared pool between all notice instances for alerting // external routines waiting on a state change. pool = sync.Pool{New: func() interface{} { return make(chan bool) }} preAllocBufferSize = PreAllocCommsDefaultBuffer mu sync.RWMutex errInvalidBufferSize = errors.New("invalid buffer size cannot be equal or less than zero") ) // SetPreAllocationCommsBuffer sets buffer size of the pre-allocated comms. func SetPreAllocationCommsBuffer(size int) error { if size <= 0 { return fmt.Errorf("%w received %v", errInvalidBufferSize, size) } mu.Lock() preAllocBufferSize = size mu.Unlock() return nil } // SetDefaultPreAllocationCommsBuffer sets default buffer size of the // pre-allocated comms. func SetDefaultPreAllocationCommsBuffer() { mu.Lock() preAllocBufferSize = PreAllocCommsDefaultBuffer mu.Unlock() } // Notice defines fields required to alert sub-systems of a change of state so a // routine can re-check in memory data type Notice struct { // Channel to wait for an alert on. forAlert chan struct{} // Lets the updater functions know if there are any routines waiting for an // alert. sema uint32 // After closing the forAlert channel this will notify when all the routines // that have waited, have completed their checks. wg sync.WaitGroup // Segregated lock only for waiting routines, so as this does not interfere // with the main calling lock, this acts as a rolling gate. mu sync.Mutex // toActuatorRoutine is communication between the alert call and the // actuator routine toActuatorRoutine chan struct{} // alerters are a pre allocated channel of communications pipes alerters chan chan struct{} } // Alert establishes a state change on the required struct. func (n *Notice) Alert() { // CompareAndSwap is used to swap from 1 -> 2 so we don't keep actuating // the opposing compare and swap in method wait. This function can return // freely when an alert operation is in process. if !atomic.CompareAndSwapUint32(&n.sema, active, alerting) { // Return if no waiting routines or currently alerting. return } if n.toActuatorRoutine == nil { // Buffered communications channel in communication with actuate routine, // so as to not worry about slow receivers that will inhibit alert // returning. n.toActuatorRoutine = make(chan struct{}, dataToActuatorDefaultBuffer) // Spawn persistent routine that blocks only when required instead of // spawning a routine for every alert. go n.actuate() } // Buffered channel will alert actuate routine without waiting and return. n.toActuatorRoutine <- struct{}{} } // Actuate lock in a different routine, as alerting is a second order priority // compared to updating and releasing calling routine. func (n *Notice) actuate() { for range n.toActuatorRoutine { n.mu.Lock() // Closing; alerts many waiting routines. close(n.forAlert) // Wait for waiting routines to receive alert and return. n.wg.Wait() atomic.SwapUint32(&n.sema, inactive) // Swap back to neutral state. n.mu.Unlock() } } // generator routine pre-loads chan struct communicators that will be closed. func (n *Notice) generator() { for { // This will block once filled appropriately. n.alerters <- make(chan struct{}) } } // Wait pauses calling routine until change of state has been established via // notice method Alert. Kick allows for cancellation of waiting or when the // caller has been shut down, if this is not needed it can be set to nil. This // returns a channel so strategies can cleanly wait on a select statement case. // NOTE: Please see README.md for implementation example. func (n *Notice) Wait(kick <-chan struct{}) chan bool { reply, ok := pool.Get().(chan bool) if !ok { reply = make(chan bool) } n.mu.Lock() if atomic.CompareAndSwapUint32(&n.sema, inactive, active) { if n.alerters == nil { mu.RLock() n.alerters = make(chan chan struct{}, preAllocBufferSize) mu.RUnlock() go n.generator() } n.forAlert = <-n.alerters } n.wg.Add(1) go n.hold(reply, kick) n.mu.Unlock() return reply } // hold waits on either channel in the event that the routine has // finished/cancelled or an alert from an update has occurred. This routine // has the potential to leak if receivers never read but this ensures sanity // instead of closing and differentiation between alerting and kicking, also // ensures chan bool item is clean before being put back into pool. func (n *Notice) hold(ch chan bool, kick <-chan struct{}) { select { case <-n.forAlert: // Main waiting channel from alert n.wg.Done() ch <- false case <-kick: // This can be nil. n.wg.Done() ch <- true } pool.Put(ch) }