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  }