-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun.go
More file actions
157 lines (138 loc) · 5.25 KB
/
Copy pathrun.go
File metadata and controls
157 lines (138 loc) · 5.25 KB
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package di
// Run and Shutdown: the main-function loop over Start and Stop, and the
// signal handling around it.
import (
"context"
"errors"
"os"
"os/signal"
"syscall"
"time"
)
// Shutdown asks a running Run to stop and records the cause it should return.
// It never blocks, may be called from any goroutine, and the first call wins.
// It propagates to ancestor scopes, so a service in a child scope can stop the
// application.
func (s *Scope) Shutdown(cause error) {
first := false
for st := s.st; st != nil; st = st.parent {
sd := st.shutdownState()
sd.once.Do(func() {
sd.err = cause
close(sd.ch)
first = first || st == s.st
})
}
if first {
s.st.emit(Event{Kind: EventShutdown, Scope: s.st.name, Err: cause})
}
}
// RunOption configures Run.
type RunOption func(*runConfig)
type runConfig struct{ startTimeout, stopTimeout time.Duration }
// defaultTimeout bounds both of Run's phases until an option says otherwise.
const defaultTimeout = 15 * time.Second
// exitSignals are what make Run exit: an interrupt or a termination request.
var exitSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
// StartTimeout bounds how long Run's start may take: the context the OnStart
// hooks Start runs receive expires after d, and once it has, the start ends at
// the next step and rolls back what it had started. The default is 15 seconds;
// a d of zero or less takes the bound off, leaving the start bounded only by
// the context Run was called with.
//
// It bounds that phase and no more. A constructor reads Scope.Context, which
// stays the context Run was called with, and a worker runs for as long as its
// service; a service resolved from inside a start hook starts on the scope's
// context too, so a hook that waits on one of those is not bounded either.
// Nothing here cuts short a constructor or hook that ignores its context: the
// phase ends between steps.
func StartTimeout(d time.Duration) RunOption { return func(c *runConfig) { c.startTimeout = d } }
// StopTimeout bounds how long Stop may take once Run decides to exit.
// The default is 15 seconds.
func StopTimeout(d time.Duration) RunOption { return func(c *runConfig) { c.stopTimeout = d } }
// startContext bounds the start phase. It is not the context the scope keeps:
// that one outlives the phase, and start is given both.
func (c runConfig) startContext(ctx context.Context) (context.Context, func()) {
if c.startTimeout <= 0 {
return ctx, func() {}
}
return context.WithTimeout(ctx, c.startTimeout)
}
// stopContext builds the context Run stops with: detached from the caller's,
// bounded by StopTimeout, and cancelled by a second signal. A rollback from a
// failed Start gets the same context.
func (c runConfig) stopContext(ctx context.Context) (context.Context, func()) {
stopCtx, cancelStop := context.WithTimeout(context.WithoutCancel(ctx), c.stopTimeout)
forceCtx, cancelForce := signal.NotifyContext(stopCtx, exitSignals...)
return forceCtx, func() { cancelForce(); cancelStop() }
}
// Run starts the scope within StartTimeout and blocks until ctx is cancelled,
// a termination signal arrives, or Shutdown is called. It then stops the scope
// within StopTimeout; a second signal during the stop cancels that context so
// a hung hook cannot keep the process alive. Run returns the Start error, the
// error passed to Shutdown, and any Stop errors, joined; a worker that died on
// its own is reported once.
func (s *Scope) Run(ctx context.Context, opts ...RunOption) error {
cfg := runConfig{startTimeout: defaultTimeout, stopTimeout: defaultTimeout}
for _, o := range opts {
o(&cfg)
}
// Register before Start so a signal during a slow start is not lost.
sigCtx, cancelSig := signal.NotifyContext(ctx, exitSignals...)
defer cancelSig()
// A failed start's rollback and the exit stop the scope the same way.
stop := func() error {
stopCtx, cancel := cfg.stopContext(ctx)
defer cancel()
return s.Stop(stopCtx)
}
startCtx, cancelStart := cfg.startContext(ctx)
defer cancelStart() // a configuration rejection unwinds past the call below
err := s.start(ctx, startCtx, stop)
cancelStart() // the phase is over; the scope keeps ctx, not startCtx
if err != nil {
// The rollback runs the hooks, so a worker can die and publish its
// failure here as it can during an ordinary shutdown.
return joinCause(err, s.publishedCause())
}
var cause error
sd := s.st.shutdownState()
select {
case <-sigCtx.Done():
case <-sd.ch:
cause = sd.err
}
stopErr := stop()
if cause == nil {
// A worker that died during the stop published its failure after the
// select above had woken for a signal.
cause = s.publishedCause()
}
return joinCause(stopErr, cause)
}
// publishedCause reports the failure Shutdown recorded, without waiting for
// one.
func (s *Scope) publishedCause() error {
sd := s.st.shutdown.Load()
if sd == nil {
return nil
}
select {
case <-sd.ch:
return sd.err
default:
return nil
}
}
// joinCause adds a published cause to what Run is already returning, unless
// it is in there already: a worker's error reaches Run both as the cause and
// through the Stop that cancelled it.
func joinCause(err, cause error) error {
if cause == nil {
return err
}
if errors.Is(err, cause) {
return err // one failure, reached by both routes
}
return errors.Join(err, cause)
}