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  }