-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathactor.go
More file actions
144 lines (131 loc) · 4.57 KB
/
Copy pathactor.go
File metadata and controls
144 lines (131 loc) · 4.57 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
package shutdown
import (
"context"
"errors"
"sync"
"time"
)
// actorRegistration is the internal record for one registered actor.
//
// Actors are long-running goroutines. The Run function is called by the
// caller (e.g. `go actor.Start()` in main); the manager holds the Interrupt
// function and calls it during the configured phase. Inspired by oklog/run.
type actorRegistration struct {
name string
interrupt InterruptFunc
phase Phase
timeout time.Duration
// completed is closed when the actor's Run returned naturally (the
// caller invoked actor.Done(err) after the actor goroutine exited).
// The interrupt handler waits on this so a phase doesn't proceed
// until the actor genuinely stopped.
completed chan struct{}
completedOnce sync.Once
completionErr error
}
// asHandler wraps the actor's interrupt+completion handshake into a
// HandlerFunc the manager can place in its phase buckets.
//
// The two-step handshake (call interrupt, then wait on `completed`) is
// what distinguishes an actor from a regular handler: regular handlers
// own their cleanup synchronously, but an actor's run loop is in another
// goroutine that the manager does not control. We must signal it AND
// wait for it — otherwise the next phase could start while the actor is
// still mid-cleanup.
func (a *actorRegistration) asHandler() HandlerFunc {
return func(ctx context.Context) error {
if a.interrupt != nil {
a.interrupt(errors.New("shutdown: requested"))
}
select {
case <-a.completed:
return a.completionErr
case <-ctx.Done():
// The actor is still running, but we've blown the per-actor
// timeout (or the manager budget). Returning ctx.Err() lets
// the runner aggregate this as a failure; the actor goroutine
// continues to run in the background and may still call
// handle.Done eventually — that call is now a no-op (the
// channel is closed only the first time).
return ctx.Err()
}
}
}
// RegisterActor registers a long-running actor (goroutine-style service)
// with the Manager. The actor is signalled to stop via interrupt during
// its phase, and the manager waits up to the per-actor timeout (or the
// remaining global budget, whichever is shorter) for the actor to confirm
// completion via the returned ActorHandle.
//
// Typical use:
//
// handle, err := mgr.RegisterActor("worker", workerStop,
// shutdown.WithActorPhase(shutdown.PhaseDrainTraffic))
// go func() {
// err := workerLoop() // blocks until workerStop is called
// handle.Done(err) // signals actor exited
// }()
//
// Note: the run loop itself is not held by the manager. The caller is
// responsible for spawning the goroutine that runs the work; the manager
// only owns the interrupt + completion handshake.
func (m *Manager) RegisterActor(name string, interrupt InterruptFunc, opts ...ActorOption) (*ActorHandle, error) {
if name == "" {
return nil, ErrEmptyName
}
if interrupt == nil {
return nil, errNilInterrupt
}
a := &actorRegistration{
name: name,
interrupt: interrupt,
phase: PhaseDrainTraffic,
timeout: 30 * time.Second,
completed: make(chan struct{}),
}
for _, o := range opts {
o(a)
}
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return nil, ErrClosed
}
for _, existing := range m.actors {
if existing.name == name {
return nil, ErrAlreadyRegistered
}
}
m.actors = append(m.actors, a)
return &ActorHandle{actor: a}, nil
}
// ActorHandle is returned from RegisterActor. Call Done(err) when the
// actor's run loop has exited so the manager can proceed to the next phase.
type ActorHandle struct {
actor *actorRegistration
}
// Done signals that the actor's run loop has returned. err is the run
// loop's exit error (nil if it returned cleanly). Idempotent — only the
// first call has effect.
func (h *ActorHandle) Done(err error) {
if h == nil || h.actor == nil {
return
}
h.actor.completedOnce.Do(func() {
h.actor.completionErr = err
close(h.actor.completed)
})
}
// ActorOption configures a RegisterActor call.
type ActorOption func(*actorRegistration)
// WithActorPhase places the actor's interrupt step in a specific phase.
// Default: PhaseDrainTraffic.
func WithActorPhase(p Phase) ActorOption {
return func(a *actorRegistration) { a.phase = p }
}
// WithActorTimeout caps how long the manager waits for the actor's run
// loop to confirm completion via Done after interrupt is called. Default: 30s.
func WithActorTimeout(d time.Duration) ActorOption {
return func(a *actorRegistration) { a.timeout = d }
}
var errNilInterrupt = errSentinel("shutdown: actor interrupt function must not be nil")