github.com/haraldrudell/parl@v0.4.176/g0/g0debug/thread-logger.go (about) 1 /* 2 © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 // Package g0debug provides troubleshooting types for Go threads and thread-groups 7 package g0debug 8 9 import ( 10 "strings" 11 "sync/atomic" 12 "time" 13 14 "github.com/haraldrudell/parl" 15 "github.com/haraldrudell/parl/g0" 16 "github.com/haraldrudell/parl/perrors" 17 "github.com/haraldrudell/parl/pmaps" 18 "github.com/haraldrudell/parl/pslices" 19 ) 20 21 const ( 22 threadLoggerLabel = "ThreadLogger" 23 ) 24 25 // ThreadLogger waits for a GoGroup, SubGo or SubGroup to terminate while printing 26 // information on threads that have yet to exit every second. 27 // - Because the GoGroup owner needs to continue consuming the GoGroup’s error channel, 28 // ThreadLogger has built-in threading 29 // - the returned sync.WaitGroup pointer should be used to ensure main does 30 // not exit prematurely. The WaitGroup ends when the GoGroup ends and ThreadLogger 31 // ceases output 32 type ThreadLogger struct { 33 log parl.PrintfFunc 34 endCh chan struct{} 35 isCancel atomic.Bool 36 37 goGroup *g0.GoGroup 38 isEnd func() bool 39 isAggregateThreads *atomic.Bool 40 setCancelListener func(f func()) 41 gEndCh <-chan struct{} 42 } 43 44 var _ = parl.AggregateThread 45 46 // NewThreadLogger wraps a GoGen thread-group in a debug listener 47 // - parl.AggregateThread is enabled for the thread-group 48 // - ThreadLogger listens for thread-group Cancel 49 // - Wait method ensures process does not exit prior to ThreadLogger complete 50 // - logFn is an optional logging function, default parl.Log to stderr 51 // 52 // Usage: 53 // 54 // main() { 55 // var threadGroup = g0.NewGoGroup(context.Background()) 56 // defer threadGroup.Wait() 57 // defer g0.NewThreadLogger(threadGroup).Log().Wait() 58 // defer threadGroup.Cancel() 59 // … 60 // threadGroup.Cancel() 61 func NewThreadLogger(goGen parl.GoGen, logFn ...func(format string, a ...interface{})) (threadLogger *ThreadLogger) { 62 t := ThreadLogger{endCh: make(chan struct{})} 63 64 // obtain logging function 65 if len(logFn) > 0 { 66 t.log = logFn[0] 67 } 68 if t.log == nil { 69 t.log = parl.Log 70 } 71 72 // obtain GoGroup 73 var ok bool 74 if t.goGroup, ok = goGen.(*g0.GoGroup); !ok { 75 panic(perrors.ErrorfPF("type assertion failed, need GoGroup SubGo or SubGroup, received: %T", goGen)) 76 } 77 t.isEnd, t.isAggregateThreads, t.setCancelListener, t.gEndCh = t.goGroup.Internals() 78 79 return &t 80 } 81 82 // Log preares the threadgroup for logging on Cancel 83 func (t *ThreadLogger) Log() (t2 *ThreadLogger) { 84 t2 = t 85 86 // if threadGroup has already ended, print that 87 var g = t.goGroup 88 var log = t.log 89 if t.isEnd() { 90 log(threadLoggerLabel + ": IsEnd true") 91 close(t.endCh) 92 return // thread-group already ended 93 } 94 t.isAggregateThreads.Store(true) 95 96 if g.Context().Err() == nil { 97 t.setCancelListener(t.cancelListener) 98 log(threadLoggerLabel + ": listening for Cancel") 99 return 100 } 101 102 t.launchThread() 103 return 104 } 105 106 func (t *ThreadLogger) Wait() { 107 <-t.endCh 108 } 109 110 // cancelListener is invoked on every threadGroup.Cancel() 111 func (t *ThreadLogger) cancelListener() { 112 if !t.isCancel.CompareAndSwap(false, true) { 113 return // subsequent cancel invocation 114 } 115 t.log(threadLoggerLabel + ": Cancel detected") 116 t.launchThread() 117 } 118 119 // launchThread prepares the waitgroup and lunches the logging thread 120 func (t *ThreadLogger) launchThread() { 121 go t.printThread() 122 } 123 124 // printThread prints goroutines that have yet to exit every second 125 func (t *ThreadLogger) printThread() { 126 var g = t.goGroup 127 var log = t.log 128 defer close(t.endCh) 129 defer parl.Recover(func() parl.DA { return parl.A() }, nil, parl.Infallible) 130 defer func() { log("%s %s: %s", parl.ShortSpace(), threadLoggerLabel, "thread-group ended") }() 131 132 // ticker for periodic printing 133 var ticker = time.NewTicker(time.Second) 134 defer ticker.Stop() 135 136 for { 137 138 // multi-line string of all threads 139 var threadLines string 140 141 // unsorted map: 142 // - key: internal parl.GoEntityID 143 // - value: *ThreadData, has no GoEntityID 144 // - keys must be retrieved for order 145 // - values must be retrieved for printing 146 var m = g.ThreadsInternal() // unordered list parl.ThreadData 147 // get implementation that has Range method 148 var rwm = m.(*pmaps.RWMap[parl.GoEntityID, *g0.ThreadData]) 149 // assemble sorted list of keys 150 var goEntityOrder = make([]parl.GoEntityID, m.Length())[:0] 151 rwm.Range(func(key parl.GoEntityID, value *g0.ThreadData) (keepGoing bool) { 152 goEntityOrder = pslices.InsertOrdered(goEntityOrder, key) 153 return true 154 }) 155 156 // printable string representation of all threads 157 var ts = make([]string, len(goEntityOrder)) 158 for i, goEntityId := range goEntityOrder { 159 var threadData, _ = m.Get(goEntityId) 160 ts[i] = threadData.LabeledString() + " G" + goEntityId.String() 161 } 162 threadLines = strings.Join(ts, "\n") 163 164 // header line 165 // - 230622 16:51:28-07 ThreadLogger: GoGen: goGroup#1_threads:316(325)_ 166 // New:main.main()-graffick.go:111 threads: 317 167 log("%s %s: GoGen: %s threads: %d\n%s", 168 parl.ShortSpace(), // 230622 16:51:26-07 169 threadLoggerLabel, // ThreadLogger 170 g, // GoGen: goGroup#1… 171 len(goEntityOrder), // threads: 317 172 threadLines, // one line for each thread 173 ) 174 175 // exit if thread-group done 176 if t.isEnd() { 177 return 178 } 179 180 // blocks here 181 select { 182 case <-t.gEndCh: // thread-group ended 183 case <-ticker.C: // timer trig 184 } 185 } 186 }