-
Notifications
You must be signed in to change notification settings - Fork 170
/
irrecoverable.go
98 lines (85 loc) · 3.38 KB
/
irrecoverable.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
package irrecoverable
import (
"context"
"fmt"
"log"
"os"
"runtime"
"go.uber.org/atomic"
)
// Signaler sends the error out.
type Signaler struct {
errChan chan error
errThrown *atomic.Bool
}
func NewSignaler() (*Signaler, <-chan error) {
errChan := make(chan error, 1)
return &Signaler{
errChan: errChan,
errThrown: atomic.NewBool(false),
}, errChan
}
// Throw is a narrow drop-in replacement for panic, log.Fatal, log.Panic, etc
// anywhere there's something connected to the error channel. It only sends
// the first error it is called with to the error channel, and logs subsequent
// errors as unhandled.
func (s *Signaler) Throw(err error) {
defer runtime.Goexit()
if s.errThrown.CompareAndSwap(false, true) {
s.errChan <- err
close(s.errChan)
} else {
// TODO: we simply log the unhandled irrecoverable to stderr for now, but we should probably
// allow the user to customize the logger / logging format used
log.New(os.Stderr, "", log.LstdFlags).Println(fmt.Errorf("unhandled irrecoverable: %w", err))
}
}
// SignalerContext is a constrained interface to provide a drop-in replacement for
// context.Context including in interfaces that compose it.
type SignalerContext interface {
context.Context
Throw(err error) // delegates to the signaler
sealed() // private, to constrain builder to using WithSignaler
}
// SignalerContextKey represents the key type for retrieving a SignalerContext from a value `context.Context`.
type SignalerContextKey struct{}
// private, to force context derivation / WithSignaler
type signalerCtx struct {
context.Context
*Signaler
}
func (sc signalerCtx) sealed() {}
// WithSignaler is the One True Way of getting a SignalerContext.
func WithSignaler(parent context.Context) (SignalerContext, <-chan error) {
sig, errChan := NewSignaler()
return &signalerCtx{parent, sig}, errChan
}
// WithSignalerContext wraps `SignalerContext` using `context.WithValue` so it can later be used with `Throw`.
func WithSignalerContext(parent context.Context, ctx SignalerContext) context.Context {
return context.WithValue(parent, SignalerContextKey{}, ctx)
}
// Throw enables throwing an irrecoverable error using any context.Context.
//
// If we have an SignalerContext, we can directly ctx.Throw.
// But a lot of library methods expect context.Context, & we want to pass the same w/o boilerplate.
// Moreover, we could have built with: context.WithCancel(irrecoverable.WithSignaler(ctx, sig)),
// "downcasting" to context.Context. Yet, we can still type-assert and recover.
//
// Throw can be a drop-in replacement anywhere we have a context.Context likely
// to support Irrecoverables. Note: this is not a method
func Throw(ctx context.Context, err error) {
signalerAbleContext, ok := ctx.Value(SignalerContextKey{}).(SignalerContext)
if ok {
signalerAbleContext.Throw(err)
} else {
// Be spectacular on how this does not -but should- handle irrecoverables:
log.Fatalf("irrecoverable error signaler not found for context, please implement! Unhandled irrecoverable error: %v", err)
}
}
// WithSignallerAndCancel returns an irrecoverable context, the cancel
// function for the context, and the error channel for the context.
func WithSignallerAndCancel(ctx context.Context) (SignalerContext, context.CancelFunc, <-chan error) {
parent, cancel := context.WithCancel(ctx)
irrecoverableCtx, errCh := WithSignaler(parent)
return irrecoverableCtx, cancel, errCh
}