forked from gin-contrib/graceful
-
Notifications
You must be signed in to change notification settings - Fork 0
/
graceful.go
307 lines (249 loc) · 7.51 KB
/
graceful.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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
package graceful
import (
"context"
"errors"
"net"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"golang.org/x/sync/errgroup"
)
// Graceful is a wrapper around a [gin.Engine] that provides graceful shutdown
type Graceful struct {
*gin.Engine
started context.Context
stop context.CancelFunc
err chan error
lock sync.Mutex
servers []*http.Server
listenAndServe []listenAndServe
cleanup []cleanup
}
// ErrAlreadyStarted is returned when trying to start a router that has already been started
var ErrAlreadyStarted = errors.New("already started router")
// ErrNotStarted is returned when trying to stop a router that has not been started
var ErrNotStarted = errors.New("router not started")
type (
listenAndServe func() error
cleanup func()
)
// Default returns a Graceful gin instance with the Logger and Recovery middleware already attached.
func Default(opts ...Option) (*Graceful, error) {
return New(gin.Default(), opts...)
}
// New returns a Graceful gin instance from the given gin.Engine.
func New(router *gin.Engine, opts ...Option) (*Graceful, error) {
g := &Graceful{
Engine: router,
}
for _, o := range opts {
if err := g.apply(o); err != nil {
g.Close()
return nil, err
}
}
return g, nil
}
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
func (g *Graceful) Run(addr ...string) error {
for _, a := range addr {
if err := g.apply(WithAddr(a)); err != nil {
return err
}
}
return g.RunWithContext(context.Background())
}
// RunTLS attaches the router to a http.Server and starts listening and serving HTTPS (secure) requests.
func (g *Graceful) RunTLS(addr, certFile, keyFile string) error {
if err := g.apply(WithTLS(addr, certFile, keyFile)); err != nil {
return err
}
return g.RunWithContext(context.Background())
}
// RunUnix attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified unix socket (i.e. a file).
func (g *Graceful) RunUnix(file string) error {
if err := g.apply(WithUnix(file)); err != nil {
return err
}
return g.RunWithContext(context.Background())
}
// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified file descriptor.
func (g *Graceful) RunFd(fd uintptr) error {
if err := g.apply(WithFd(fd)); err != nil {
return err
}
return g.RunWithContext(context.Background())
}
// RunListener attaches the router to a http.Server and starts listening and serving HTTP requests
// through the specified net.Listener
func (g *Graceful) RunListener(listener net.Listener) error {
if err := g.apply(WithListener(listener)); err != nil {
return err
}
return g.RunWithContext(context.Background())
}
// RunWithContext attaches the router to the configured http.Server (fallback to configuring one on
// :8080 if none are configured) and starts listening and serving HTTP requests. If the passed
// context is canceled, the server is gracefully shut down
func (g *Graceful) RunWithContext(ctx context.Context) error {
if err := g.ensureAtLeastDefaultServer(); err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
go func() {
<-ctx.Done()
_ = g.Shutdown(ctx)
}()
defer cancel()
eg := errgroup.Group{}
g.lock.Lock()
for _, srv := range g.listenAndServe {
safeCopy := srv
eg.Go(func() error {
if err := safeCopy(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
})
}
g.lock.Unlock()
if err := waitWithContext(ctx, &eg); err != nil {
return err
}
return g.Shutdown(ctx)
}
// Shutdown gracefully shuts down the server without interrupting any active connections.
func (g *Graceful) Shutdown(ctx context.Context) error {
var err error
g.lock.Lock()
defer g.lock.Unlock()
for _, srv := range g.servers {
if e := srv.Shutdown(ctx); e != nil {
err = e
}
}
g.servers = nil
return err
}
// Start will start the Graceful instance and all underlying http.Servers in a separate
// goroutine and return right away. You must call Stop and not Shutdown if you use Start.
func (g *Graceful) Start() error {
g.lock.Lock()
defer g.lock.Unlock()
if g.started != nil {
return ErrAlreadyStarted
}
g.err = make(chan error)
ctxStarted, cancel := context.WithCancel(context.Background())
ctx, cancelStop := context.WithCancel(context.Background())
go func() {
err := g.RunWithContext(ctx)
cancel()
g.err <- err
}()
g.stop = cancelStop
g.started = ctxStarted
return nil
}
// Stop will stop the Graceful instance previously started with Start. It
// will return once the instance has been stopped.
func (g *Graceful) Stop() error {
resetStartedState := func() (context.Context, context.CancelFunc, chan error, error) {
g.lock.Lock()
defer g.lock.Unlock()
if g.started == nil {
return nil, nil, nil, ErrNotStarted
}
stop := g.stop
started := g.started
chErr := g.err
g.stop = nil
g.started = nil
return started, stop, chErr, nil
}
started, stop, chErr, err := resetStartedState()
if err != nil {
return err
}
stop()
err = <-chErr
<-started.Done()
if !errors.Is(err, context.Canceled) {
return err
}
err = started.Err()
if errors.Is(err, context.Canceled) {
err = nil
}
return err
}
// Close gracefully shuts down the server.
// It first shuts down the server using the Shutdown method,
// then it performs any cleanup operations registered with the server.
// Finally, it resets the server's internal state.
func (g *Graceful) Close() {
_ = g.Shutdown(context.Background())
g.lock.Lock()
defer g.lock.Unlock()
for _, c := range g.cleanup {
c()
}
g.cleanup = nil
g.listenAndServe = nil
g.servers = nil
}
// apply applies the given option to the Graceful instance.
// It creates a new server, applies the option to it, and adds the server and cleanup function to the Graceful instance.
// If an error occurs during the application of the option, it returns the error.
func (g *Graceful) apply(o Option) error {
srv, cleanup, err := o.apply(g)
if err != nil {
return err
}
g.listenAndServe = append(g.listenAndServe, srv)
g.cleanup = append(g.cleanup, cleanup)
return nil
}
// appendHTTPServer appends a new HTTP server to the list of servers managed by the Graceful instance.
// It returns the newly created http.Server.
func (g *Graceful) appendHTTPServer() *http.Server {
srv := &http.Server{
Handler: g.Engine,
ReadHeaderTimeout: time.Second * 5, // Set a reasonable ReadHeaderTimeout value
}
g.lock.Lock()
defer g.lock.Unlock()
g.servers = append(g.servers, srv)
return srv
}
// ensureAtLeastDefaultServer ensures that there is at least one server running with the default address ":8080".
// If no server is running, it creates a new server with the default address and starts it.
// It returns an error if there was a problem creating or starting the server.
func (g *Graceful) ensureAtLeastDefaultServer() error {
g.lock.Lock()
defer g.lock.Unlock()
if len(g.listenAndServe) == 0 {
if err := g.apply(WithAddr(":8080")); err != nil {
return err
}
}
return nil
}
// waitWithContext waits for the completion of the errgroup.Group and returns any error encountered.
// If the context is canceled before the errgroup.Group completes, it returns the context error.
// If the errgroup.Group completes successfully or the context is not canceled, it returns nil.
func waitWithContext(ctx context.Context, eg *errgroup.Group) error {
if err := eg.Wait(); err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}
func donothing() {}