github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/module/irrecoverable/irrecoverable.go (about) 1 package irrecoverable 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "runtime" 9 10 "go.uber.org/atomic" 11 ) 12 13 // Signaler sends the error out. 14 type Signaler struct { 15 errChan chan error 16 errThrown *atomic.Bool 17 } 18 19 func NewSignaler() (*Signaler, <-chan error) { 20 errChan := make(chan error, 1) 21 return &Signaler{ 22 errChan: errChan, 23 errThrown: atomic.NewBool(false), 24 }, errChan 25 } 26 27 // Throw is a narrow drop-in replacement for panic, log.Fatal, log.Panic, etc 28 // anywhere there's something connected to the error channel. It only sends 29 // the first error it is called with to the error channel, and logs subsequent 30 // errors as unhandled. 31 func (s *Signaler) Throw(err error) { 32 defer runtime.Goexit() 33 if s.errThrown.CompareAndSwap(false, true) { 34 s.errChan <- err 35 close(s.errChan) 36 } else { 37 // TODO: we simply log the unhandled irrecoverable to stderr for now, but we should probably 38 // allow the user to customize the logger / logging format used 39 log.New(os.Stderr, "", log.LstdFlags).Println(fmt.Errorf("unhandled irrecoverable: %w", err)) 40 } 41 } 42 43 // SignalerContext is a constrained interface to provide a drop-in replacement for 44 // context.Context including in interfaces that compose it. 45 type SignalerContext interface { 46 context.Context 47 Throw(err error) // delegates to the signaler 48 sealed() // private, to constrain builder to using WithSignaler 49 } 50 51 // SignalerContextKey represents the key type for retrieving a SignalerContext from a value `context.Context`. 52 type SignalerContextKey struct{} 53 54 // private, to force context derivation / WithSignaler 55 type signalerCtx struct { 56 context.Context 57 *Signaler 58 } 59 60 func (sc signalerCtx) sealed() {} 61 62 // WithSignaler is the One True Way of getting a SignalerContext. 63 func WithSignaler(parent context.Context) (SignalerContext, <-chan error) { 64 sig, errChan := NewSignaler() 65 return &signalerCtx{parent, sig}, errChan 66 } 67 68 // WithSignalerContext wraps `SignalerContext` using `context.WithValue` so it can later be used with `Throw`. 69 func WithSignalerContext(parent context.Context, ctx SignalerContext) context.Context { 70 return context.WithValue(parent, SignalerContextKey{}, ctx) 71 } 72 73 // Throw enables throwing an irrecoverable error using any context.Context. 74 // 75 // If we have an SignalerContext, we can directly ctx.Throw. 76 // But a lot of library methods expect context.Context, & we want to pass the same w/o boilerplate. 77 // Moreover, we could have built with: context.WithCancel(irrecoverable.WithSignaler(ctx, sig)), 78 // "downcasting" to context.Context. Yet, we can still type-assert and recover. 79 // 80 // Throw can be a drop-in replacement anywhere we have a context.Context likely 81 // to support Irrecoverables. Note: this is not a method 82 func Throw(ctx context.Context, err error) { 83 signalerAbleContext, ok := ctx.Value(SignalerContextKey{}).(SignalerContext) 84 if ok { 85 signalerAbleContext.Throw(err) 86 } else { 87 // Be spectacular on how this does not -but should- handle irrecoverables: 88 log.Fatalf("irrecoverable error signaler not found for context, please implement! Unhandled irrecoverable error: %v", err) 89 } 90 } 91 92 // WithSignallerAndCancel returns an irrecoverable context, the cancel 93 // function for the context, and the error channel for the context. 94 func WithSignallerAndCancel(ctx context.Context) (SignalerContext, context.CancelFunc, <-chan error) { 95 parent, cancel := context.WithCancel(ctx) 96 irrecoverableCtx, errCh := WithSignaler(parent) 97 return irrecoverableCtx, cancel, errCh 98 }