-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathengine.go
164 lines (130 loc) · 3.17 KB
/
engine.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
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
158
159
160
161
162
163
164
package lemon
import (
"context"
"os"
"sync"
"time"
)
// Engine is a components that handle your application lifecycle.
//
// Start
//
// It will start every registered hook (or daemon, service, etc...) and block until it
// receives a SIGINT, SIGTERM or SIGQUIT signal.
//
// For example:
//
// e, err := New(ctx)
// if err == nil {
// e.Register(&MyService{})
// e.Register(&MyWorker{})
// e.Start()
// // Wait until Ctrl-C
// }
//
// Stop
//
// When your application has to stop, the engine will notify every hook to shutdown gracefully.
// However, if a hook fails to stop before timeout, the underlying goroutine will be destroyed...
//
type Engine struct {
interrupt chan os.Signal
timeout time.Duration
hooks []Hook
wait sync.WaitGroup
parent context.Context
ctx context.Context
cancel context.CancelFunc
noSignal bool
signals []os.Signal
beforeShutdown func()
afterShutdown func()
logger func(error)
mutex sync.Mutex
cause error
}
// New creates a new engine with given options.
//
// Options can change the timeout, register a signal, execute a pre-hook callback and many other behaviours.
func New(ctx context.Context, options ...Option) (*Engine, error) {
e := &Engine{}
e.parent = ctx
e.init()
for _, o := range options {
if err := o.apply(e); err != nil {
return nil, err
}
}
return e, nil
}
// launch will start given hook.
func (e *Engine) launch(h Hook) {
e.wait.Add(1)
go func() {
defer e.wait.Done()
runtime := &HookRuntime{}
// Wait for an event to notify this goroutine that a shutdown is required.
// It could either be from engine's context or during Hook startup if an error has occurred.
// NOTE: If HookRuntime returns an error, we have to shutdown every Hook...
err := runtime.WaitForEvent(e.ctx, h)
if err != nil {
e.mutex.Lock()
e.log(err)
e.cancel()
e.cause = err
e.mutex.Unlock()
}
// Wait for hook to gracefully shutdown, or kill it after timeout.
// This is handled by HookRuntime.
for _, err := range runtime.Shutdown(e.timeout) {
e.mutex.Lock()
e.log(err)
e.mutex.Unlock()
}
}()
}
// init configures default parameters for engine.
func (e *Engine) init() {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.ctx == nil || e.cancel == nil {
e.ctx, e.cancel = context.WithCancel(e.parent)
}
if e.timeout == 0 {
e.timeout = DefaultTimeout
}
if len(e.signals) == 0 && !e.noSignal {
e.signals = Signals
}
if e.interrupt == nil {
e.interrupt = make(chan os.Signal, 1)
}
}
// Start will launch the engine and start registered hooks.
// It will block until every hooks has shutdown, gracefully or with force...
func (e *Engine) Start() error {
e.init()
go e.waitShutdownNotification()
for _, h := range e.hooks {
e.launch(h)
}
e.wait.Wait()
if e.afterShutdown != nil {
e.afterShutdown()
}
e.mutex.Lock()
defer e.mutex.Unlock()
return e.cause
}
// Stop will shutdown engine.
func (e *Engine) Stop() error {
e.mutex.Lock()
defer e.mutex.Unlock()
if e.interrupt != nil {
select {
case e.interrupt <- os.Interrupt:
default:
}
}
return nil
}