github.com/haraldrudell/parl@v0.4.176/g0/reader.go (about)

     1  /*
     2  © 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  package g0
     7  
     8  import (
     9  	"context"
    10  	"errors"
    11  	"sync/atomic"
    12  
    13  	"github.com/haraldrudell/parl"
    14  	"github.com/haraldrudell/parl/perrors"
    15  )
    16  
    17  // for [Reader]
    18  var NoShouldTerm *atomic.Bool
    19  
    20  // for [Reader]
    21  var NoAddErr parl.AddError
    22  
    23  // for [Reader]
    24  var NoLog parl.PrintfFunc
    25  
    26  // Reader thread reads the error channel of a [GoGroup] or [SubGroup]
    27  //   - shouldTerminate is an optional pointer that an application’s most important goroutine sets to true
    28  //     prior to exit, causing graceful shutdown
    29  //   - addError receives fatal thread-exits.
    30  //     If not present, those are logged.
    31  //     typically [github.com/haraldrudell/parl/mains.Executable.AddErr]
    32  //   - log outputs warnings and more, default [parl.Log] standard error
    33  //   - g is from [parl.NewGoResult] or [parl.NewGoResult2] making Reader awaitable
    34  //   - a GoGroup’s or SubGroup’s error channel is unbound buffer so Reader is only required for:
    35  //   - — real-time warning output
    36  //   - — terminating the process while additional goroutines are still running:
    37  //   - — on fatal thread exit or
    38  //   - — on exit of a primary goroutine
    39  //   - —
    40  //   - because reading of the threadgroup’s error channel must not stop,
    41  //     it is done in this separate thread.
    42  //   - reading continues until:
    43  //   - — the threadGroup context is canceled by eg. [GoGroup.Cancel]
    44  //   - — the last thread exits
    45  //   - — a thread exits with error
    46  //   - — on thread exit, shouldTerminate is true
    47  //
    48  // Usage:
    49  //
    50  //	func main() {
    51  //	  var err error
    52  //	  defer mains.MinimalRecovery(&err)
    53  //	  var goGroup = g0.NewGoGroup(context.Background())
    54  //	  defer goGroup.Wait()
    55  //	  var goResult = parl.NewGoResult()
    56  //	  defer goResult.ReceiveError(&err)
    57  //	  go g0.Reader(g0.NoSholdTerm, g0.NoAddErr, g0.NoLog, goGroup, goResult)
    58  //	  defer goGroup.Cancel()
    59  //	  go someGoroutine(goGroup.Go())
    60  func Reader(shouldTerminate *atomic.Bool, addError parl.AddError, log parl.PrintfFunc, goGroup parl.GoGroup, g parl.GoResult) {
    61  	var err error
    62  	defer g.SendError(&err)
    63  	defer parl.RecoverErr(func() parl.DA { return parl.A() }, &err)
    64  
    65  	if log == nil {
    66  		log = parl.Log
    67  	}
    68  	for goError := range goGroup.Ch() {
    69  
    70  		// if not thread-exit, it is a warning
    71  		//	- if panic: full stack trace
    72  		//	- otherwise just error location
    73  		if !goError.IsThreadExit() {
    74  			log("Warning: " + goError.ErrString())
    75  			continue // warning processed
    76  		}
    77  
    78  		// error from exiting goroutine
    79  		var e = goError.Err()
    80  
    81  		// no thread should return [context.Canceled]
    82  		//	- on context cancel threads should exit silently
    83  		//	- here is printed any thread ID returning context cancel
    84  		if e != nil {
    85  			var gotContextCancel string
    86  			// error may be associated by using [perrors.AppendError]
    87  			for _, anError := range perrors.ErrorList(e) {
    88  				if errors.Is(anError, context.Canceled) {
    89  					gotContextCancel = "context.Canceled"
    90  					break
    91  				}
    92  			}
    93  			if gotContextCancel != "" {
    94  				var g = goError.Go()
    95  				log("BAD: %s emitted by goroutine#%d func: %s trace:\n%s",
    96  					gotContextCancel,
    97  					g.GoID(),
    98  					g.ThreadInfo().Func().Short(), // the function launching the goroutine
    99  					perrors.Long(e),               // stack trace for the main error having context.Canceled
   100  				)
   101  			}
   102  		}
   103  
   104  		// fatal thread-exit shuts down the app
   105  		if e != nil {
   106  			if addError != nil {
   107  				addError(e)
   108  			} else {
   109  				log("FATAL: " + goError.ErrString())
   110  			}
   111  			goGroup.Cancel()
   112  			continue // fatal exit processed
   113  		}
   114  
   115  		// shouldTerminate is for apps that has a primary sub-thread that on exit
   116  		// should shut down the app
   117  		if t := shouldTerminate; t != nil && t.Load() && goGroup.Context().Err() == nil {
   118  			goGroup.Cancel()
   119  			// shouldTerminate processed
   120  		}
   121  	}
   122  
   123  	// graceful threadGroup termination
   124  	log("threadGroup ended")
   125  }