-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathadd-notifier.go
120 lines (103 loc) · 3.98 KB
/
add-notifier.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
/*
© 2022–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
ISC License
*/
package parl
import (
"context"
"sync/atomic"
)
const (
// counts [parl.handleContextNotify] and [parl.invokeCancel]
cancelNotifierFrames = 2
)
// a NotifierFunc receives a stack trace of function causing cancel
// - typically stack trace begins with [parl.InvokeCancel]
type NotifierFunc func(slice Stack)
// notifier1Key is the context value key for child-context notifiers
var notifier1Key cancelContextKey = "notifyChild"
// notifierAllKey is the context value key for all-context notifiers
var notifierAllKey cancelContextKey = "notifyAll"
// threadSafeList is a thread-safe slice for all-cancel notifers
type threadSafeList struct {
// - atomic.Pointer.Load for thread-safe read
// - CompareAndSwap of cloned list for thread-safe write
notifiers atomic.Pointer[[]NotifierFunc]
}
// AddNotifier1 adds a function that is invoked when a child context is canceled
// - child contexts with their own AddNotifier1 are not detected
// - invocation is immediately after context cancel completes
// - implemented by inserting a value into the context chain
// - notifier receives a stack trace of the cancel invocation,
// typically beginning with [parl.InvokeCancel]
// - notifier should be thread-safe and not long running
// - typical usage is debug of unexpected context cancel
func AddNotifier1(ctx context.Context, notifier NotifierFunc) (ctx2 context.Context) {
if ctx == nil {
panic(NilError("ctx"))
} else if notifier == nil {
panic(NilError("notifier"))
}
return context.WithValue(ctx, notifier1Key, notifier)
}
// AddNotifier adds a function that is invoked when any context is canceled
// - AddNotifier is typically invoked on the root context
// - any InvokeCancel in the context tree below the top AddNotifier
// invocation causes notification
// - invocation is immediately after context cancel completes
// - implemented by inserting a thread-safe slice value into the context chain
// - notifier receives a stack trace of the cancel invocation,
// typically beginning with [parl.InvokeCancel]
// - notifier should be thread-safe and not long running
// - typical usage is debug of unexpected context cancel
func AddNotifier(ctx context.Context, notifier NotifierFunc) (ctx2 context.Context) {
if ctx == nil {
panic(NilError("ctx"))
} else if notifier == nil {
panic(NilError("notifier"))
}
// if this context-chain has static notifier, append to it
if list, ok := ctx.Value(notifierAllKey).(*threadSafeList); ok { // ok only if non-nil
for {
// currentSlicep is read-only to be thread-safe
var currentSlicep = list.notifiers.Load()
// clone and append
var newSlice = append(append([]NotifierFunc{}, *currentSlicep...), notifier)
if list.notifiers.CompareAndSwap(currentSlicep, &newSlice) {
ctx2 = ctx // appended: ctx does not change
return // append return
}
}
}
// create a new list
var newList threadSafeList
var newSlice = []NotifierFunc{notifier}
newList.notifiers.Store(&newSlice)
// insert list pointer into context chain
ctx2 = context.WithValue(ctx, notifierAllKey, &newList)
return // insert context value return, ctx2 new value
}
// handleContextNotify is invoked for all CancelContext cancel invocations
func handleContextNotify(ctx context.Context) {
// fetch the nearest notify1 function
// - notify1 are created by [parl.AddNotifier1] and are notified of
// cancellation of a child context
var notifier, _ = ctx.Value(notifier1Key).(NotifierFunc)
// fetch any notifyall list
var notifiers []NotifierFunc
if list, ok := ctx.Value(notifierAllKey).(*threadSafeList); ok {
notifiers = *list.notifiers.Load()
}
if notifier == nil && len(notifiers) == 0 {
return // no notifiers return
}
// stack trace for notifiers: expensive
var cl = newStack(cancelNotifierFrames)
// invoke all notifier functions
if notifier != nil {
notifier(cl)
}
for _, n := range notifiers {
n(cl)
}
}