go.temporal.io/server/common/finalizer/finalizer.go

164 LOC · 95 covered · 69 uncovered · 26 ranges · 132 concepts · 18 introducers · 40 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 package finalizer
2
3 import (
4 "context"
5 "errors"
6 "sync"
7 "time"
8
9 cclock "go.temporal.io/server/common/clock"
10 "go.temporal.io/server/common/goro"
11 "go.temporal.io/server/common/log"
12 "go.temporal.io/server/common/log/tag"
13 "go.temporal.io/server/common/metrics"
14 )
15
16 var (
17 FinalizerAlreadyDoneErr = errors.New("finalizer already finalized")
18 FinalizerUnknownIdErr = errors.New("finalizer callback not found")
19 FinalizerDuplicateIdErr = errors.New("finalizer callback already registered")
20 )
21
22 type Finalizer struct {
23 logger log.Logger
24 metricsHandler metrics.Handler
25 mu sync.Mutex
26 finalized bool
27 callbacks map[string]func(context.Context) error
28 }
29
30 func New(
31 logger log.Logger,
32 metricsHandler metrics.Handler,
33 > ) *Finalizer { finalizer.go ×1
34 > return &Finalizer{
35 > logger: logger,
36 > metricsHandler: metricsHandler,
37 > callbacks: make(map[string]func(context.Context) error),
38 > }
39 > }
40
41 // Register adds a callback to the finalizer.
42 // Returns an error if the ID is already registered, or when the finalizer is/was already running.
43 func (f *Finalizer) Register(
44 id string,
45 callback func(context.Context) error,
46 > ) error { finalizer.go ×1
47 > f.mu.Lock()
48 > defer f.mu.Unlock()
49 >
50 > if f.finalized {
51 > // aborting immediately once the finalizer is/was running finalizer.go ×1
52 > return FinalizerAlreadyDoneErr
53 > }
54
55 > if _, ok := f.callbacks[id]; ok { finalizer.go ×2
56 > return FinalizerDuplicateIdErr finalizer.go ×1
57 > }
58 > f.callbacks[id] = callback finalizer.go ×2
59 > return nil
60 }
61
62 // Deregister removes a callback from the finalizer.
63 // Returns an error if the ID is not found, or when the finalizer is/was already running.
64 func (f *Finalizer) Deregister(
65 id string,
66 > ) (err error) { finalizer.go ×1
67 > f.mu.Lock()
68 > defer f.mu.Unlock()
69 >
70 > if f.finalized {
71 > // aborting immediately once the finalizer is/was running finalizer.go ×1
72 > return FinalizerAlreadyDoneErr
73 > }
74
75 > if _, ok := f.callbacks[id]; !ok { finalizer.go ×1
76 > return FinalizerUnknownIdErr finalizer.go ×1
77 > }
78 > delete(f.callbacks, id) finalizer.go ×1
79 > return nil
80 }
81
82 // Run executes all registered callback functions within the given timeout (zero timeout skips execution).
83 // It can only be invoked once; calling it again has no effect.
84 // Returns the number of completed callbacks.
85 func (f *Finalizer) Run(
86 timeout time.Duration,
87 > ) int { finalizer.go ×1
88 > if timeout == 0 {
89 > f.logger.Debug("finalizer skipped: zero timeout") finalizer.go ×1
90 > return 0
91 > }
92
93 > f.mu.Lock() finalizer.go ×2
94 > if f.finalized {
95 > f.logger.Warn("finalizer skipped: called more than once") finalizer.go ×1
96 > f.mu.Unlock()
97 > return 0
98 > }
99 > f.finalized = true finalizer.go ×2
100 > f.mu.Unlock() // unlocking immediately to unblock any calls to Register/Deregister
101 >
102 > totalCount := len(f.callbacks)
103 > if totalCount == 0 {
104 > f.logger.Debug("finalizer skipped: no callbacks") finalizer.go ×1
105 > return 0
106 > }
107
108 > f.logger.Debug("finalizer starting", finalizer.go ×6
109 > tag.Int("items", totalCount),
110 > tag.Duration("timeout", timeout))
111 >
112 > startTime := time.Now()
113 > defer func() { metrics.FinalizerLatency.With(f.metricsHandler).Record(time.Since(startTime)) }()
114
115 > ctx, cancel := context.WithTimeout(context.Background(), timeout) finalizer.go ×6
116 > defer cancel()
117 >
118 > pool := goro.NewAdaptivePool(cclock.NewRealTimeSource(), 5, 15, 10*time.Millisecond, 10)
119 > defer pool.Stop()
120 >
121 > completionChannel := make(chan struct{})
122 > go func() {
123 > for _, callback := range f.callbacks {
124 > // NOTE: Once `pool.Stop` is called, any remaining calls to `pool.Do` will do nothing.
125 > pool.Do(func() {
126 > defer func() { completionChannel <- struct{}{} }()
127 > _ = callback(ctx)
128 })
129 }
130
131 // prevent holding on to the callbacks for longer than needed and allow garbage collection
132 // (safe since any calls to Register/Deregister will be aborted now that the finalizer ran)
133 > f.callbacks = nil finalizer.go ×6
134 }()
135
136 > var completedCallbacks int finalizer.go ×6
137 > defer func() {
138 > unfinishedItems := int64(totalCount - completedCallbacks)
139 > metrics.FinalizerRuns.With(f.metricsHandler).Record(1)
140 > if unfinishedItems > 0 {
141 > metrics.FinalizerRunTimeouts.With(f.metricsHandler).Record(1) finalizer.go ×2
142 > }
143 > metrics.FinalizerItemsCompleted.With(f.metricsHandler).Record(int64(completedCallbacks)) finalizer.go ×6
144 > metrics.FinalizerItemsUnfinished.With(f.metricsHandler).Record(unfinishedItems)
145 }()
146
147 > for { finalizer.go ×6
148 > select {
149 > case <-completionChannel:
150 > completedCallbacks += 1
151 > if completedCallbacks == totalCount {
152 > f.logger.Debug("finalizer completed", finalizer.go ×1
153 > tag.Int("completed", completedCallbacks))
154 > return completedCallbacks
155 > }
156
157 > case <-ctx.Done(): finalizer.go ×2
158 > f.logger.Error("finalizer timed out",
159 > tag.Int("completed", completedCallbacks),
160 > tag.Int("unfinished", totalCount-completedCallbacks))
161 > return completedCallbacks
162 }
163 }
164 }