github.com/haraldrudell/parl@v0.4.176/internal/cyclebreaker/recover.go (about) 1 /* 2 © 2023–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/) 3 ISC License 4 */ 5 6 package cyclebreaker 7 8 import ( 9 "fmt" 10 "strings" 11 12 "github.com/haraldrudell/parl/perrors" 13 "github.com/haraldrudell/parl/perrors/errorglue" 14 "github.com/haraldrudell/parl/pruntime" 15 ) 16 17 // Recover recovers panic using deferred annotation 18 // - Recover creates a single aggregate error of *errp and any panic 19 // - if onError non-nil, the function is invoked zero or one time with the aggregate error 20 // - if onError nil, the error is logged to standard error 21 // - if errp is non-nil, it is updated with any aggregate error 22 // 23 // Usage: 24 // 25 // func someFunc() (err error) { 26 // defer parl.Recover(func() parl.DA { return parl.A() }, &err, parl.NoOnError) 27 func Recover(deferredLocation func() DA, errp *error, onError OnError) { 28 doRecovery(noAnnotation, deferredLocation, errp, onError, recoverOnErrrorOnce, noIsPanic, recover()) 29 } 30 31 // Recover2 recovers panic using deferred annotation 32 // - if onError non-nil, the function is invoked zero, one or two times with any error in *errp and any panic 33 // - if onError nil, errors are logged to standard error 34 // - if errp is non-nil: 35 // - — if *errp was nil, it is updated with any panic 36 // - — if *errp was non-nil, it is updated with any panic as an aggregate error 37 // 38 // Usage: 39 // 40 // func someFunc() (err error) { 41 // defer parl.Recover2(func() parl.DA { return parl.A() }, &err, parl.NoOnError) 42 func Recover2(deferredLocation func() DA, errp *error, onError OnError) { 43 doRecovery(noAnnotation, deferredLocation, errp, onError, recoverOnErrrorMultiple, noIsPanic, recover()) 44 } 45 46 // RecoverAnnotation is like Recover but with fixed-string annotation 47 func RecoverAnnotation(annotation string, errp *error, onError OnError) { 48 doRecovery(annotation, noDeferredAnnotation, errp, onError, recoverOnErrrorOnce, noIsPanic, recover()) 49 } 50 51 // nil OnError function 52 // - public for RecoverAnnotation 53 var NoOnError OnError 54 55 // OnError is a function that receives error values from an errp error pointer or a panic 56 type OnError func(err error) 57 58 const ( 59 // counts the frames in [parl.A] 60 parlAFrames = 1 61 // counts the stack-frame in [parl.processRecover] 62 processRecoverFrames = 1 63 // counts the stack-frame of [parl.doRecovery] and [parl.Recover] or [parl.Recover2] 64 // - but for panic detector to work, there must be one frame after 65 // runtime.gopanic, so remove one frame 66 doRecoveryFrames = 2 - 1 67 // fixed-string annotation is not present 68 noAnnotation = "" 69 ) 70 71 const ( 72 // indicates onError to be invoked once for all errors 73 recoverOnErrrorOnce OnErrorStrategy = iota 74 // indicates onError to be invoked once per error 75 recoverOnErrrorMultiple 76 // do not invoke onError 77 recoverOnErrrorNone 78 ) 79 80 // how OnError is handled: recoverOnErrrorOnce recoverOnErrrorMultiple recoverOnErrrorNone 81 type OnErrorStrategy uint8 82 83 // indicates deferred annotation is not present 84 var noDeferredAnnotation func() DA 85 86 // DA is the value returned by a deferred code location function 87 type DA *pruntime.CodeLocation 88 89 // contains a deferred code location for annotation 90 type annotationLiteral func() DA 91 92 // A is a thunk returning a deferred code location 93 func A() DA { return pruntime.NewCodeLocation(parlAFrames) } 94 95 // noIsPanic is a stand-in nil value when noPanic is not present 96 var noIsPanic *bool 97 98 // doRecovery implements recovery for Recovery andd Recovery2 99 func doRecovery(annotation string, deferredAnnotation annotationLiteral, errp *error, onError OnError, onErrorStrategy OnErrorStrategy, isPanic *bool, recoverValue interface{}) { 100 if onErrorStrategy == recoverOnErrrorNone { 101 if errp == nil { 102 panic(NilError("errp")) 103 } 104 } else if errp == nil && onError == nil { 105 panic(NilError("both errp and onError")) 106 } 107 108 // build aggregate error in err 109 var err error 110 if errp != nil { 111 err = *errp 112 // if onError is to be invoked multiple times, 113 // and *errp contains an error, 114 // invoke onError or Log to standard error 115 if err != nil && onErrorStrategy == recoverOnErrrorMultiple { 116 invokeOnError(onError, err) // invoke onError or parl.Log 117 } 118 } 119 120 // consume recover() 121 if recoverValue != nil { 122 if isPanic != nil { 123 *isPanic = true 124 } 125 if annotation == noAnnotation { 126 annotation = getDeferredAnnotation(deferredAnnotation) 127 } 128 var panicError = processRecoverValue(annotation, recoverValue, doRecoveryFrames) 129 err = perrors.AppendError(err, panicError) 130 if onErrorStrategy == recoverOnErrrorMultiple { 131 invokeOnError(onError, panicError) 132 } 133 } 134 135 // if err now contains any error 136 if err != nil { 137 // if errp non-nil: 138 // - err was obtained from *errp 139 // - err may now be panicError or have had panicError appended 140 // - overwrite back to non-nil errp 141 if errp != nil && *errp != err { 142 *errp = err 143 } 144 // if OnError is once, invoke onError or Log with the aggregate error 145 if onErrorStrategy == recoverOnErrrorOnce { 146 invokeOnError(onError, err) 147 } 148 } 149 } 150 151 // getDeferredAnnotation obtains annotation from a deferred annotation function literal 152 func getDeferredAnnotation(deferredAnnotation annotationLiteral) (annotation string) { 153 if deferredAnnotation != nil { 154 if da := deferredAnnotation(); da != nil { 155 var cL = (*pruntime.CodeLocation)(da) 156 // single word package name 157 var packageName = cL.Package() 158 // recoverDaPanic.func1: hosting function name and a derived name for the function literal 159 var funcName = cL.FuncIdentifier() 160 // removed “.func1” suffix 161 if index := strings.LastIndex(funcName, "."); index != -1 { 162 funcName = funcName[:index] 163 } 164 annotation = fmt.Sprintf("panic detected in %s.%s:", 165 packageName, 166 funcName, 167 ) 168 } 169 } 170 if annotation == "" { 171 // default annotation cannot be obtained 172 // - the deferred Recover function is invoked directly from rutine, eg. runtime.gopanic 173 // - therefore, use fixed string 174 annotation = "recover from panic:" 175 } 176 177 return 178 } 179 180 // invokeOnError invokes an onError function or logs to standard error if onError is nil 181 func invokeOnError(onError OnError, err error) { 182 if onError != nil { 183 onError(err) 184 return 185 } 186 Log("Recover: %+v\n", err) 187 } 188 189 // processRecoverValue returns an error value with stack from annotation and panicValue 190 // - annotation is non-empty annotation indicating code loction or action 191 // - panicValue is non-nil value returned by built-in recover function 192 func processRecoverValue(annotation string, panicValue interface{}, frames int) (err error) { 193 if frames < 0 { 194 frames = 0 195 } 196 197 // if panicValue is an error with attached stack, 198 // the panic detector will fail because 199 // that innermost stack does not include panic recovery 200 var hadPreRecoverStack bool 201 if e, ok := panicValue.(error); ok { 202 hadPreRecoverStack = errorglue.GetInnerMostStack(e) != nil 203 } 204 // ensure an error value is derived from panicValue 205 err = perrors.Errorf("%s “%w”", 206 annotation, 207 ensureError(panicValue, frames+processRecoverFrames), 208 ) 209 // make sure err has a post-recover() stack 210 // - this will allow the panic detector to succeed 211 if hadPreRecoverStack { 212 err = perrors.Stackn(err, frames) 213 } 214 215 return 216 }