// Package qu is a library for making handling signal (chan struct{}) channels // simpler, as well as monitoring the state of the signal channels in an // application. package qu import ( "fmt" "strings" "sync" "time" "go.uber.org/atomic" "lol.mleku.dev" "lol.mleku.dev/log" ) // C is your basic empty struct signal channel type C chan struct{} var ( createdList []string createdChannels []C createdChannelBufferCounts []int mx sync.Mutex logEnabled = atomic.NewBool(false) ) // SetLogging switches on and off the channel logging func SetLogging(on bool) { logEnabled.Store(on) } func l(a ...interface{}) { if logEnabled.Load() { log.D.Ln(a...) } } func lc(cl func() string) { if logEnabled.Load() { log.D.Ln(cl()) } } // T creates an unbuffered chan struct{} for trigger and quit signalling // (momentary and breaker switches) func T() C { mx.Lock() defer mx.Unlock() msg := fmt.Sprintf("chan from %s", lol.GetLoc(1)) l("created", msg) createdList = append(createdList, msg) o := make(C) createdChannels = append(createdChannels, o) createdChannelBufferCounts = append(createdChannelBufferCounts, 0) return o } // Ts creates a buffered chan struct{} which is specifically intended for // signalling without blocking, generally one is the size of buffer to be used, // though there might be conceivable cases where the channel should accept more // signals without blocking the caller func Ts(n int) C { mx.Lock() defer mx.Unlock() msg := fmt.Sprintf("buffered chan (%d) from %s", n, lol.GetLoc(1)) l("created", msg) createdList = append(createdList, msg) o := make(C, n) createdChannels = append(createdChannels, o) createdChannelBufferCounts = append(createdChannelBufferCounts, n) return o } // Q closes the channel, which makes it emit a nil every time it is selected. func (c C) Q() { open := !testChanIsClosed(c) lc( func() (o string) { lo := getLocForChan(c) mx.Lock() defer mx.Unlock() if open { return "closing chan from " + lo + "\n" + strings.Repeat( " ", 48, ) + "from" + lol.GetLoc(1) } else { return "from" + lol.GetLoc(1) + "\n" + strings.Repeat(" ", 48) + "channel " + lo + " was already closed" } }, ) if open { close(c) } } // Signal sends struct{}{} on the channel which functions as a momentary switch, // useful in pairs for stop/start func (c C) Signal() { lc(func() (o string) { return "signalling " + getLocForChan(c) }) if !testChanIsClosed(c) { c <- struct{}{} } } // Wait should be placed with a `<-` in a select case in addition to the channel // variable name func (c C) Wait() <-chan struct{} { lc( func() (o string) { return fmt.Sprint( "waiting on "+getLocForChan(c)+"at", lol.GetLoc(1), ) }, ) return c } // IsClosed exposes a test to see if the channel is closed func (c C) IsClosed() bool { return testChanIsClosed(c) } // testChanIsClosed allows you to see whether the channel has been closed so you // can avoid a panic by trying to close or signal on it func testChanIsClosed(ch C) (o bool) { if ch == nil { return true } select { case <-ch: o = true default: } return } // getLocForChan finds which record connects to the channel in question func getLocForChan(c C) (s string) { s = "not found" mx.Lock() for i := range createdList { if i >= len(createdChannels) { break } if createdChannels[i] == c { s = createdList[i] } } mx.Unlock() return } // once a minute clean up the channel cache to remove closed channels no longer // in use func init() { go func() { for { <-time.After(time.Minute) l("cleaning up closed channels") var c []C var ll []string mx.Lock() for i := range createdChannels { if i >= len(createdList) { break } if testChanIsClosed(createdChannels[i]) { } else { c = append(c, createdChannels[i]) ll = append(ll, createdList[i]) } } createdChannels = c createdList = ll mx.Unlock() } }() } // PrintChanState creates an output showing the current state of the channels // being monitored This is a function for use by the programmer while debugging func PrintChanState() { mx.Lock() for i := range createdChannels { if i >= len(createdList) { break } if testChanIsClosed(createdChannels[i]) { log.T.Ln(">>> closed", createdList[i]) } else { log.T.Ln("<<< open", createdList[i]) } } mx.Unlock() } // GetOpenUnbufferedChanCount returns the number of qu channels that are still // open func GetOpenUnbufferedChanCount() (o int) { mx.Lock() var c int for i := range createdChannels { if i >= len(createdChannels) { break } // skip buffered channels if createdChannelBufferCounts[i] > 0 { continue } if testChanIsClosed(createdChannels[i]) { c++ } else { o++ } } mx.Unlock() return } // GetOpenBufferedChanCount returns the number of qu channels that are still // open func GetOpenBufferedChanCount() (o int) { mx.Lock() var c int for i := range createdChannels { if i >= len(createdChannels) { break } // skip unbuffered channels if createdChannelBufferCounts[i] < 1 { continue } if testChanIsClosed(createdChannels[i]) { c++ } else { o++ } } mx.Unlock() return }