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  }