go.temporal.io/server/common/deadlock/deadlock.go

209 LOC · 108 covered · 101 uncovered · 21 ranges · 66 concepts · 4 introducers · 13 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 deadlock
2
3 import (
4 "context"
5 "runtime/pprof"
6 "strings"
7 "sync/atomic"
8 "time"
9
10 "go.temporal.io/server/common/clock"
11 "go.temporal.io/server/common/dynamicconfig"
12 "go.temporal.io/server/common/goro"
13 "go.temporal.io/server/common/log"
14 "go.temporal.io/server/common/log/tag"
15 "go.temporal.io/server/common/metrics"
16 "go.temporal.io/server/common/pingable"
17 "go.uber.org/fx"
18 "google.golang.org/grpc/health"
19 )
20
21 type (
22 params struct {
23 fx.In
24
25 Logger log.SnTaggedLogger
26 Collection *dynamicconfig.Collection
27 HealthServer *health.Server
28 MetricsHandler metrics.Handler
29
30 Roots []pingable.Pingable `group:"deadlockDetectorRoots"`
31 }
32
33 config struct {
34 DumpGoroutines dynamicconfig.BoolPropertyFn
35 FailHealthCheck dynamicconfig.BoolPropertyFn
36 AbortProcess dynamicconfig.BoolPropertyFn
37 Interval dynamicconfig.DurationPropertyFn
38 MaxWorkersPerRoot dynamicconfig.IntPropertyFn
39 }
40
41 deadlockDetector struct {
42 logger log.Logger
43 healthServer *health.Server
44 metricsHandler metrics.Handler
45 config config
46 roots []pingable.Pingable
47 pools []*goro.AdaptivePool
48 loops goro.Group
49
50 // number of suspected deadlocks that have not resolved yet
51 current atomic.Int64
52 }
53
54 loopContext struct {
55 dd *deadlockDetector
56 root pingable.Pingable
57 p *goro.AdaptivePool
58 }
59 )
60
61 // CurrentSuspected returns the number of currently unresolved suspected deadlocks.
62 > func (dd *deadlockDetector) CurrentSuspected() int64 { deadlock.go ×10
63 > return dd.current.Load()
64 > }
65
66 > func NewDeadlockDetector(params params) *deadlockDetector { deadlock.go ×5
67 > return &deadlockDetector{
68 > logger: params.Logger,
69 > healthServer: params.HealthServer,
70 > metricsHandler: params.MetricsHandler.WithTags(metrics.OperationTag(metrics.DeadlockDetectorScope)),
71 > config: config{
72 > DumpGoroutines: dynamicconfig.DeadlockDumpGoroutines.Get(params.Collection),
73 > FailHealthCheck: dynamicconfig.DeadlockFailHealthCheck.Get(params.Collection),
74 > AbortProcess: dynamicconfig.DeadlockAbortProcess.Get(params.Collection),
75 > Interval: dynamicconfig.DeadlockInterval.Get(params.Collection),
76 > MaxWorkersPerRoot: dynamicconfig.DeadlockMaxWorkersPerRoot.Get(params.Collection),
77 > },
78 > roots: params.Roots,
79 > }
80 > }
81
82 > func (dd *deadlockDetector) Start() error { fx.go ×44
83 > for _, root := range dd.roots {
84 > pool := goro.NewAdaptivePool(
85 > clock.NewRealTimeSource(),
86 > 0,
87 > dd.config.MaxWorkersPerRoot(),
88 > 100*time.Millisecond,
89 > 10,
90 > )
91 > dd.pools = append(dd.pools, pool)
92 > loopCtx := &loopContext{
93 > dd: dd,
94 > root: root,
95 > p: pool,
96 > }
97 > dd.loops.Go(loopCtx.run)
98 > }
99 > return nil
100 }
101
102 > func (dd *deadlockDetector) Stop() error { service.go ×8
103 > for _, pool := range dd.pools {
104 > pool.Stop()
105 > }
106 > dd.loops.Cancel()
107 > // don't wait for workers to exit, they may be blocked
108 > return nil
109 }
110
111 > func (dd *deadlockDetector) detected(name string) { deadlock.go ×10
112 > dd.logger.Error("potential deadlock detected", tag.Name(name))
113 >
114 > metrics.DDSuspectedDeadlocks.With(dd.metricsHandler).Record(1)
115 >
116 > if dd.config.DumpGoroutines() {
117 > dd.dumpGoroutines()
118 > }
119
120 > if dd.config.FailHealthCheck() { deadlock.go ×10
121 dd.logger.Info("marking grpc services unhealthy")
122 dd.healthServer.Shutdown()
123 }
124
125 > if dd.config.AbortProcess() { deadlock.go ×10
126 dd.logger.Fatal("deadlock detected", tag.Name(name))
127 }
128 }
129
130 > func (dd *deadlockDetector) dumpGoroutines() { deadlock.go ×10
131 > profile := pprof.Lookup("goroutine")
132 > if profile == nil {
133 dd.logger.Error("could not find goroutine profile")
134 return
135 }
136 > var b strings.Builder deadlock.go ×10
137 > err := profile.WriteTo(&b, 1) // 1 is magic value that means "text format"
138 > if err != nil {
139 dd.logger.Error("failed to get goroutine profile", tag.Error(err))
140 return
141 }
142 // write it as a single log line with embedded newlines.
143 // the value starts with "goroutine profile: total ...\n" so it should be clear
144 > dd.logger.Info("dumping goroutine profile for suspected deadlock") deadlock.go ×10
145 > dd.logger.Info(b.String())
146 }
147
148 > func (dd *deadlockDetector) adjustCurrent(delta int64) { deadlock.go ×10
149 > dd.current.Add(delta)
150 > metrics.DDCurrentSuspectedDeadlocks.With(dd.metricsHandler).Record(float64(dd.current.Load()))
151 > }
152
153 > func (lc *loopContext) run(ctx context.Context) error { fx.go ×44
154 > for {
155 > // ping blocks until it has passed all checks to a worker goroutine (using an
156 > // unbuffered channel).
157 > lc.ping(ctx, []pingable.Pingable{lc.root})
158 >
159 > timer := time.NewTimer(lc.dd.config.Interval())
160 > select {
161 case <-timer.C:
162 > case <-ctx.Done(): service.go ×8
163 > timer.Stop()
164 > return ctx.Err()
165 }
166 }
167 }
168
169 > func (lc *loopContext) ping(ctx context.Context, pingables []pingable.Pingable) { deadlock.go ×5
170 > for _, pingable := range pingables {
171 > for _, check := range pingable.GetPingChecks() { fx.go ×44
172 > lc.p.Do(func() { lc.check(ctx, check) })
173 }
174 }
175 }
176
177 > func (lc *loopContext) check(ctx context.Context, check pingable.Check) { deadlock.go ×5
178 > lc.dd.logger.Debug("starting ping check", tag.Name(check.Name))
179 > startTime := time.Now().UTC()
180 > resolved := make(chan struct{})
181 >
182 > // Using AfterFunc is cheaper than creating another goroutine to be the waiter, since
183 > // we expect to always cancel it. If the go runtime is so messed up that it can't
184 > // create a goroutine, that's a bigger problem than we can handle.
185 > t := time.AfterFunc(check.Timeout, func() {
186 > if ctx.Err() != nil { deadlock.go ×10
187 // deadlock detector was stopped
188 return
189 }
190 > lc.dd.adjustCurrent(1) deadlock.go ×10
191 >
192 > lc.dd.detected(check.Name)
193 >
194 > // Wait and see if Ping() returns past the timeout. If it's a true deadlock, it'll
195 > // block here forever.
196 > <-resolved
197 > lc.dd.adjustCurrent(-1)
198 })
199 > newPingables := check.Ping() deadlock.go ×5
200 > t.Stop()
201 > if len(check.MetricsName) > 0 {
202 > lc.dd.metricsHandler.Timer(check.MetricsName).Record(time.Since(startTime)) fx.go ×44
203 > }
204 > close(resolved) deadlock.go ×5
205 >
206 > lc.dd.logger.Debug("ping check succeeded", tag.Name(check.Name))
207 >
208 > lc.ping(ctx, newPingables)
209 }