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 }