Skip to content

Commit db56c0f

Browse files
committed
Merge pull request #1037 from torarnv/harden-shutdown-logic
Harden shutdown logic
2 parents 1bac12f + bfd1211 commit db56c0f

File tree

5 files changed

+151
-83
lines changed

5 files changed

+151
-83
lines changed

cmd/ipfs/daemon.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
8080
// let the user know we're going.
8181
fmt.Printf("Initializing daemon...\n")
8282

83+
ctx := req.Context()
84+
85+
go func() {
86+
select {
87+
case <-ctx.Context.Done():
88+
fmt.Println("Received interrupt signal, shutting down...")
89+
}
90+
}()
91+
8392
// first, whether user has provided the initialization flag. we may be
8493
// running in an uninitialized state.
8594
initialize, _, err := req.Option(initOptionKwd).Bool()
@@ -111,7 +120,6 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
111120
return
112121
}
113122

114-
ctx := req.Context()
115123
cfg, err := ctx.GetConfig()
116124
if err != nil {
117125
res.SetError(err, cmds.ErrNormal)
@@ -149,7 +157,19 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
149157
res.SetError(err, cmds.ErrNormal)
150158
return
151159
}
152-
defer node.Close()
160+
161+
defer func() {
162+
// We wait for the node to close first, as the node has children
163+
// that it will wait for before closing, such as the API server.
164+
node.Close()
165+
166+
select {
167+
case <-ctx.Context.Done():
168+
log.Info("Gracefully shut down daemon")
169+
default:
170+
}
171+
}()
172+
153173
req.Context().ConstructNode = func() (*core.IpfsNode, error) {
154174
return node, nil
155175
}
@@ -262,9 +282,6 @@ func daemonFunc(req cmds.Request, res cmds.Response) {
262282
corehttp.VersionOption(),
263283
}
264284

265-
// our global interrupt handler can now try to stop the daemon
266-
close(req.Context().InitDone)
267-
268285
if rootRedirect != nil {
269286
opts = append(opts, rootRedirect)
270287
}

cmd/ipfs/main.go

Lines changed: 63 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"runtime"
1212
"runtime/pprof"
1313
"strings"
14+
"sync"
1415
"syscall"
1516
"time"
1617

@@ -39,7 +40,6 @@ const (
3940
cpuProfile = "ipfs.cpuprof"
4041
heapProfile = "ipfs.memprof"
4142
errorFormat = "ERROR: %v\n\n"
42-
shutdownMessage = "Received interrupt signal, shutting down..."
4343
)
4444

4545
type cmdInvocation struct {
@@ -132,15 +132,10 @@ func main() {
132132
os.Exit(1)
133133
}
134134

135-
// our global interrupt handler may try to stop the daemon
136-
// before the daemon is ready to be stopped; this dirty
137-
// workaround is for the daemon only; other commands are always
138-
// ready to be stopped
139-
if invoc.cmd != daemonCmd {
140-
close(invoc.req.Context().InitDone)
141-
}
142-
143135
// ok, finally, run the command invocation.
136+
intrh, ctx := invoc.SetupInterruptHandler(ctx)
137+
defer intrh.Close()
138+
144139
output, err := invoc.Run(ctx)
145140
if err != nil {
146141
printErr(err)
@@ -157,8 +152,6 @@ func main() {
157152
}
158153

159154
func (i *cmdInvocation) Run(ctx context.Context) (output io.Reader, err error) {
160-
// setup our global interrupt handler.
161-
i.setupInterruptHandler()
162155

163156
// check if user wants to debug. option OR env var.
164157
debug, _, err := i.req.Option("debug").Bool()
@@ -226,7 +219,6 @@ func (i *cmdInvocation) Parse(ctx context.Context, args []string) error {
226219
if err != nil {
227220
return err
228221
}
229-
i.req.Context().Context = ctx
230222

231223
repoPath, err := getRepoPath(i.req)
232224
if err != nil {
@@ -279,6 +271,8 @@ func callCommand(ctx context.Context, req cmds.Request, root *cmds.Command, cmd
279271
log.Info(config.EnvDir, " ", req.Context().ConfigRoot)
280272
var res cmds.Response
281273

274+
req.Context().Context = ctx
275+
282276
details, err := commandDetails(req.Path(), root)
283277
if err != nil {
284278
return nil, err
@@ -474,59 +468,70 @@ func writeHeapProfileToFile() error {
474468
return pprof.WriteHeapProfile(mprof)
475469
}
476470

477-
// listen for and handle SIGTERM
478-
func (i *cmdInvocation) setupInterruptHandler() {
471+
// IntrHandler helps set up an interrupt handler that can
472+
// be cleanly shut down through the io.Closer interface.
473+
type IntrHandler struct {
474+
sig chan os.Signal
475+
wg sync.WaitGroup
476+
}
477+
478+
func NewIntrHandler() *IntrHandler {
479+
ih := &IntrHandler{}
480+
ih.sig = make(chan os.Signal, 1)
481+
return ih
482+
}
483+
484+
func (ih *IntrHandler) Close() error {
485+
close(ih.sig)
486+
ih.wg.Wait()
487+
return nil
488+
}
479489

480-
ctx := i.req.Context()
481-
sig := allInterruptSignals()
482490

491+
// Handle starts handling the given signals, and will call the handler
492+
// callback function each time a signal is catched. The function is passed
493+
// the number of times the handler has been triggered in total, as
494+
// well as the handler itself, so that the handling logic can use the
495+
// handler's wait group to ensure clean shutdown when Close() is called.
496+
func (ih *IntrHandler) Handle(handler func(count int, ih *IntrHandler), sigs ...os.Signal) {
497+
signal.Notify(ih.sig, sigs...)
498+
ih.wg.Add(1)
483499
go func() {
484-
// first time, try to shut down.
485-
486-
// loop because we may be
487-
for count := 0; ; count++ {
488-
<-sig
489-
490-
// if we're still initializing, cannot use `ctx.GetNode()`
491-
select {
492-
default: // initialization not done
493-
fmt.Println(shutdownMessage)
494-
os.Exit(-1)
495-
case <-ctx.InitDone:
496-
}
497-
498-
switch count {
499-
case 0:
500-
fmt.Println(shutdownMessage)
501-
if ctx.Online {
502-
go func() {
503-
// TODO cancel the command context instead
504-
n, err := ctx.GetNode()
505-
if err != nil {
506-
log.Error(err)
507-
fmt.Println(shutdownMessage)
508-
os.Exit(-1)
509-
}
510-
n.Close()
511-
log.Info("Gracefully shut down.")
512-
}()
513-
} else {
514-
os.Exit(0)
515-
}
516-
517-
default:
518-
fmt.Println("Received another interrupt before graceful shutdown, terminating...")
519-
os.Exit(-1)
520-
}
500+
defer ih.wg.Done()
501+
count := 0
502+
for _ = range ih.sig {
503+
count++
504+
handler(count, ih)
521505
}
506+
signal.Stop(ih.sig)
522507
}()
523508
}
524509

525-
func allInterruptSignals() chan os.Signal {
526-
sigc := make(chan os.Signal, 1)
527-
signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT,
528-
syscall.SIGTERM)
529-
return sigc
510+
func (i *cmdInvocation) SetupInterruptHandler(ctx context.Context) (io.Closer, context.Context) {
511+
512+
intrh := NewIntrHandler()
513+
ctx, cancelFunc := context.WithCancel(ctx)
514+
515+
handlerFunc := func(count int, ih *IntrHandler) {
516+
switch count {
517+
case 1:
518+
fmt.Println() // Prevent un-terminated ^C character in terminal
519+
520+
ih.wg.Add(1)
521+
go func() {
522+
defer ih.wg.Done()
523+
cancelFunc()
524+
}()
525+
526+
default:
527+
fmt.Println("Received another interrupt before graceful shutdown, terminating...")
528+
os.Exit(-1)
529+
}
530+
}
531+
532+
intrh.Handle(handlerFunc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
533+
534+
return intrh, ctx
530535
}
531536

532537
func profileIfEnabled() (func(), error) {

commands/http/client.go

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,25 +82,44 @@ func (c *client) Send(req cmds.Request) (cmds.Response, error) {
8282
version := config.CurrentVersionNumber
8383
httpReq.Header.Set("User-Agent", fmt.Sprintf("/go-ipfs/%s/", version))
8484

85-
httpRes, err := http.DefaultClient.Do(httpReq)
86-
if err != nil {
87-
return nil, err
88-
}
85+
ec := make(chan error, 1)
86+
rc := make(chan cmds.Response, 1)
87+
dc := req.Context().Context.Done()
8988

90-
// using the overridden JSON encoding in request
91-
res, err := getResponse(httpRes, req)
92-
if err != nil {
93-
return nil, err
94-
}
95-
96-
if found && len(previousUserProvidedEncoding) > 0 {
97-
// reset to user provided encoding after sending request
98-
// NB: if user has provided an encoding but it is the empty string,
99-
// still leave it as JSON.
100-
req.SetOption(cmds.EncShort, previousUserProvidedEncoding)
89+
go func() {
90+
httpRes, err := http.DefaultClient.Do(httpReq)
91+
if err != nil {
92+
ec <- err
93+
return
94+
}
95+
// using the overridden JSON encoding in request
96+
res, err := getResponse(httpRes, req)
97+
if err != nil {
98+
ec <- err
99+
return
100+
}
101+
rc <- res
102+
}()
103+
104+
for {
105+
select {
106+
case <-dc:
107+
log.Debug("Context cancelled, cancelling HTTP request...")
108+
tr := http.DefaultTransport.(*http.Transport)
109+
tr.CancelRequest(httpReq)
110+
dc = nil // Wait for ec or rc
111+
case err := <-ec:
112+
return nil, err
113+
case res := <-rc:
114+
if found && len(previousUserProvidedEncoding) > 0 {
115+
// reset to user provided encoding after sending request
116+
// NB: if user has provided an encoding but it is the empty string,
117+
// still leave it as JSON.
118+
req.SetOption(cmds.EncShort, previousUserProvidedEncoding)
119+
}
120+
return res, nil
121+
}
101122
}
102-
103-
return res, nil
104123
}
105124

106125
func getQuery(req cmds.Request) (string, error) {
@@ -162,6 +181,8 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
162181
dec := json.NewDecoder(httpRes.Body)
163182
outputType := reflect.TypeOf(req.Command().Type)
164183

184+
ctx := req.Context().Context
185+
165186
for {
166187
var v interface{}
167188
var err error
@@ -175,6 +196,14 @@ func getResponse(httpRes *http.Response, req cmds.Request) (cmds.Response, error
175196
fmt.Println(err.Error())
176197
return
177198
}
199+
200+
select {
201+
case <-ctx.Done():
202+
close(outChan)
203+
return
204+
default:
205+
}
206+
178207
if err == io.EOF {
179208
close(outChan)
180209
return

commands/request.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ type Context struct {
3030

3131
node *core.IpfsNode
3232
ConstructNode func() (*core.IpfsNode, error)
33-
InitDone chan bool
3433
}
3534

3635
// GetConfig returns the config of the current Command exection
@@ -288,7 +287,7 @@ func NewRequest(path []string, opts OptMap, args []string, file files.File, cmd
288287
optDefs = make(map[string]Option)
289288
}
290289

291-
ctx := Context{Context: context.TODO(), InitDone: make(chan bool)}
290+
ctx := Context{Context: context.TODO()}
292291
values := make(map[string]interface{})
293292
req := &request{path, opts, args, file, cmd, ctx, optDefs, values, os.Stdin}
294293
err := req.ConvertOptions()

core/corehttp/corehttp.go

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package corehttp
22

33
import (
44
"net/http"
5+
"time"
56

67
manners "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/braintree/manners"
78
ma "github.com/ipfs/go-ipfs/Godeps/_workspace/src/github.com/jbenet/go-multiaddr"
@@ -63,6 +64,9 @@ func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, handler http.Handler
6364
var serverError error
6465
serverExited := make(chan struct{})
6566

67+
node.Children().Add(1)
68+
defer node.Children().Done()
69+
6670
go func() {
6771
serverError = server.ListenAndServe(host, handler)
6872
close(serverExited)
@@ -75,8 +79,22 @@ func listenAndServe(node *core.IpfsNode, addr ma.Multiaddr, handler http.Handler
7579
// if node being closed before server exits, close server
7680
case <-node.Closing():
7781
log.Infof("server at %s terminating...", addr)
82+
83+
// make sure keep-alive connections do not keep the server running
84+
server.InnerServer.SetKeepAlivesEnabled(false)
85+
7886
server.Shutdown <- true
79-
<-serverExited // now, DO wait until server exit
87+
88+
outer:
89+
for {
90+
// wait until server exits
91+
select {
92+
case <-serverExited:
93+
break outer
94+
case <-time.After(5 * time.Second):
95+
log.Infof("waiting for server at %s to terminate...", addr)
96+
}
97+
}
8098
}
8199

82100
log.Infof("server at %s terminated", addr)

0 commit comments

Comments
 (0)