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