Skip to content

Commit 044a11c

Browse files
authored
btcd: Simplify shutdown signal handling logic. (btcsuite#733)
This rewrites the shutdown logic to simplify the shutdown signalling. All cleanup is now run from deferred functions in the main function and channels are used to signal shutdown either from OS signals or from other subsystems such as the RPC server and windows service controller. The RPC server has been modified to use a new channel for signalling shutdown that is exposed via the RequestedProcessShutdown function instead of directly calling Stop on the server as it previously did. Finally, it adds a few checks for early termination during the main start sequence so the process can be stopped without starting all the subsystems if desired. This is a backport of the equivalent logic from Decred with a few slight modifications. Credits go to @jrick.
1 parent a7b35d9 commit 044a11c

File tree

6 files changed

+119
-125
lines changed

6 files changed

+119
-125
lines changed

btcd.go

+27-25
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ import (
1919
)
2020

2121
var (
22-
cfg *config
23-
shutdownChannel = make(chan struct{})
22+
cfg *config
2423
)
2524

2625
// winServiceMain is only invoked on Windows. It detects when btcd is running
@@ -42,6 +41,12 @@ func btcdMain(serverChan chan<- *server) error {
4241
cfg = tcfg
4342
defer backendLog.Flush()
4443

44+
// Get a channel that will be closed when a shutdown signal has been
45+
// triggered either from an OS signal such as SIGINT (Ctrl+C) or from
46+
// another subsystem such as the RPC server.
47+
interruptedChan := interruptListener()
48+
defer btcdLog.Info("Shutdown complete")
49+
4550
// Show version at startup.
4651
btcdLog.Infof("Version %s", version())
4752

@@ -75,19 +80,27 @@ func btcdMain(serverChan chan<- *server) error {
7580
return err
7681
}
7782

83+
// Return now if an interrupt signal was triggered.
84+
if interruptRequested(interruptedChan) {
85+
return nil
86+
}
87+
7888
// Load the block database.
7989
db, err := loadBlockDB()
8090
if err != nil {
8191
btcdLog.Errorf("%v", err)
8292
return err
8393
}
84-
defer db.Close()
85-
86-
// Ensure the database is sync'd and closed on Ctrl+C.
87-
addInterruptHandler(func() {
94+
defer func() {
95+
// Ensure the database is sync'd and closed on shutdown.
8896
btcdLog.Infof("Gracefully shutting down the database...")
8997
db.Close()
90-
})
98+
}()
99+
100+
// Return now if an interrupt signal was triggered.
101+
if interruptRequested(interruptedChan) {
102+
return nil
103+
}
91104

92105
// Drop indexes and exit if requested.
93106
//
@@ -118,32 +131,21 @@ func btcdMain(serverChan chan<- *server) error {
118131
cfg.Listeners, err)
119132
return err
120133
}
121-
addInterruptHandler(func() {
134+
defer func() {
122135
btcdLog.Infof("Gracefully shutting down the server...")
123136
server.Stop()
124137
server.WaitForShutdown()
125-
})
138+
srvrLog.Infof("Server shutdown complete")
139+
}()
126140
server.Start()
127141
if serverChan != nil {
128142
serverChan <- server
129143
}
130144

131-
// Monitor for graceful server shutdown and signal the main goroutine
132-
// when done. This is done in a separate goroutine rather than waiting
133-
// directly so the main goroutine can be signaled for shutdown by either
134-
// a graceful shutdown or from the main interrupt handler. This is
135-
// necessary since the main goroutine must be kept running long enough
136-
// for the interrupt handler goroutine to finish.
137-
go func() {
138-
server.WaitForShutdown()
139-
srvrLog.Infof("Server shutdown complete")
140-
shutdownChannel <- struct{}{}
141-
}()
142-
143-
// Wait for shutdown signal from either a graceful server stop or from
144-
// the interrupt handler.
145-
<-shutdownChannel
146-
btcdLog.Info("Shutdown complete")
145+
// Wait until the interrupt signal is received from an OS signal or
146+
// shutdown is requested through one of the subsystems such as the RPC
147+
// server.
148+
<-interruptedChan
147149
return nil
148150
}
149151

rpcserver.go

+38-26
Original file line numberDiff line numberDiff line change
@@ -3513,7 +3513,10 @@ func handleSetGenerate(s *rpcServer, cmd interface{}, closeChan <-chan struct{})
35133513

35143514
// handleStop implements the stop command.
35153515
func handleStop(s *rpcServer, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) {
3516-
s.server.Stop()
3516+
select {
3517+
case s.requestProcessShutdown <- struct{}{}:
3518+
default:
3519+
}
35173520
return "btcd stopping.", nil
35183521
}
35193522

@@ -3683,23 +3686,24 @@ func handleVerifyMessage(s *rpcServer, cmd interface{}, closeChan <-chan struct{
36833686
// rpcServer holds the items the rpc server may need to access (config,
36843687
// shutdown, main server, etc.)
36853688
type rpcServer struct {
3686-
started int32
3687-
shutdown int32
3688-
policy *mining.Policy
3689-
server *server
3690-
chain *blockchain.BlockChain
3691-
authsha [fastsha256.Size]byte
3692-
limitauthsha [fastsha256.Size]byte
3693-
ntfnMgr *wsNotificationManager
3694-
numClients int32
3695-
statusLines map[int]string
3696-
statusLock sync.RWMutex
3697-
wg sync.WaitGroup
3698-
listeners []net.Listener
3699-
workState *workState
3700-
gbtWorkState *gbtWorkState
3701-
helpCacher *helpCacher
3702-
quit chan int
3689+
started int32
3690+
shutdown int32
3691+
policy *mining.Policy
3692+
server *server
3693+
chain *blockchain.BlockChain
3694+
authsha [fastsha256.Size]byte
3695+
limitauthsha [fastsha256.Size]byte
3696+
ntfnMgr *wsNotificationManager
3697+
numClients int32
3698+
statusLines map[int]string
3699+
statusLock sync.RWMutex
3700+
wg sync.WaitGroup
3701+
listeners []net.Listener
3702+
workState *workState
3703+
gbtWorkState *gbtWorkState
3704+
helpCacher *helpCacher
3705+
requestProcessShutdown chan struct{}
3706+
quit chan int
37033707
}
37043708

37053709
// httpStatusLine returns a response Status-Line (RFC 2616 Section 6.1)
@@ -3783,6 +3787,13 @@ func (s *rpcServer) Stop() error {
37833787
return nil
37843788
}
37853789

3790+
// RequestedProcessShutdown returns a channel that is sent to when an authorized
3791+
// RPC client requests the process to shutdown. If the request can not be read
3792+
// immediately, it is dropped.
3793+
func (s *rpcServer) RequestedProcessShutdown() <-chan struct{} {
3794+
return s.requestProcessShutdown
3795+
}
3796+
37863797
// limitConnections responds with a 503 service unavailable and returns true if
37873798
// adding another client would exceed the maximum allow RPC clients.
37883799
//
@@ -4164,14 +4175,15 @@ func genCertPair(certFile, keyFile string) error {
41644175
// newRPCServer returns a new instance of the rpcServer struct.
41654176
func newRPCServer(listenAddrs []string, policy *mining.Policy, s *server) (*rpcServer, error) {
41664177
rpc := rpcServer{
4167-
policy: policy,
4168-
server: s,
4169-
chain: s.blockManager.chain,
4170-
statusLines: make(map[int]string),
4171-
workState: newWorkState(),
4172-
gbtWorkState: newGbtWorkState(s.timeSource),
4173-
helpCacher: newHelpCacher(),
4174-
quit: make(chan int),
4178+
policy: policy,
4179+
server: s,
4180+
chain: s.blockManager.chain,
4181+
statusLines: make(map[int]string),
4182+
workState: newWorkState(),
4183+
gbtWorkState: newGbtWorkState(s.timeSource),
4184+
helpCacher: newHelpCacher(),
4185+
requestProcessShutdown: make(chan struct{}),
4186+
quit: make(chan int),
41754187
}
41764188
if cfg.RPCUser != "" && cfg.RPCPass != "" {
41774189
login := cfg.RPCUser + ":" + cfg.RPCPass

server.go

+6
Original file line numberDiff line numberDiff line change
@@ -2555,6 +2555,12 @@ func newServer(listenAddrs []string, db database.DB, chainParams *chaincfg.Param
25552555
if err != nil {
25562556
return nil, err
25572557
}
2558+
2559+
// Signal process shutdown when the RPC server requests it.
2560+
go func() {
2561+
<-s.rpcServer.RequestedProcessShutdown()
2562+
shutdownRequestChannel <- struct{}{}
2563+
}()
25582564
}
25592565

25602566
return &s, nil

service_windows.go

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2013-2014 The btcsuite developers
1+
// Copyright (c) 2013-2016 The btcsuite developers
22
// Use of this source code is governed by an ISC
33
// license that can be found in the LICENSE file.
44

@@ -85,18 +85,8 @@ loop:
8585
// more commands while pending.
8686
changes <- svc.Status{State: svc.StopPending}
8787

88-
// Stop the main server gracefully when it is
89-
// already setup or just break out and allow
90-
// the service to exit immediately if it's not
91-
// setup yet. Note that calling Stop will cause
92-
// btcdMain to exit in the goroutine above which
93-
// will in turn send a signal (and a potential
94-
// error) to doneChan.
95-
if mainServer != nil {
96-
mainServer.Stop()
97-
} else {
98-
break loop
99-
}
88+
// Signal the main function to exit.
89+
shutdownRequestChannel <- struct{}{}
10090

10191
default:
10292
elog.Error(1, fmt.Sprintf("Unexpected control "+

signal.go

+44-60
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2013-2014 The btcsuite developers
1+
// Copyright (c) 2013-2016 The btcsuite developers
22
// Use of this source code is governed by an ISC
33
// license that can be found in the LICENSE file.
44

@@ -9,79 +9,63 @@ import (
99
"os/signal"
1010
)
1111

12-
// interruptChannel is used to receive SIGINT (Ctrl+C) signals.
13-
var interruptChannel chan os.Signal
12+
// shutdownRequestChannel is used to initiate shutdown from one of the
13+
// subsystems using the same code paths as when an interrupt signal is received.
14+
var shutdownRequestChannel = make(chan struct{})
1415

15-
// addHandlerChannel is used to add an interrupt handler to the list of handlers
16-
// to be invoked on SIGINT (Ctrl+C) signals.
17-
var addHandlerChannel = make(chan func())
16+
// interruptSignals defines the default signals to catch in order to do a proper
17+
// shutdown. This may be modified during init depending on the platform.
18+
var interruptSignals = []os.Signal{os.Interrupt}
1819

19-
// signals defines the default signals to catch in order to do a proper
20-
// shutdown.
21-
var signals = []os.Signal{os.Interrupt}
20+
// interruptListener listens for OS Signals such as SIGINT (Ctrl+C) and shutdown
21+
// requests from shutdownRequestChannel. It returns a channel that is closed
22+
// when either signal is received.
23+
func interruptListener() <-chan struct{} {
24+
c := make(chan struct{})
25+
go func() {
26+
interruptChannel := make(chan os.Signal, 1)
27+
signal.Notify(interruptChannel, interruptSignals...)
2228

23-
// mainInterruptHandler listens for SIGINT (Ctrl+C) signals on the
24-
// interruptChannel and invokes the registered interruptCallbacks accordingly.
25-
// It also listens for callback registration. It must be run as a goroutine.
26-
func mainInterruptHandler() {
27-
// interruptCallbacks is a list of callbacks to invoke when a
28-
// SIGINT (Ctrl+C) is received.
29-
var interruptCallbacks []func()
30-
31-
// isShutdown is a flag which is used to indicate whether or not
32-
// the shutdown signal has already been received and hence any future
33-
// attempts to add a new interrupt handler should invoke them
34-
// immediately.
35-
var isShutdown bool
36-
37-
for {
29+
// Listen for initial shutdown signal and close the returned
30+
// channel to notify the caller.
3831
select {
3932
case sig := <-interruptChannel:
40-
// Ignore more than one shutdown signal.
41-
if isShutdown {
42-
btcdLog.Infof("Received signal (%s). "+
43-
"Already shutting down...", sig)
44-
continue
45-
}
46-
47-
isShutdown = true
4833
btcdLog.Infof("Received signal (%s). Shutting down...",
4934
sig)
5035

51-
// Run handlers in LIFO order.
52-
for i := range interruptCallbacks {
53-
idx := len(interruptCallbacks) - 1 - i
54-
callback := interruptCallbacks[idx]
55-
callback()
56-
}
36+
case <-shutdownRequestChannel:
37+
btcdLog.Info("Shutdown requested. Shutting down...")
38+
}
39+
close(c)
5740

58-
// Signal the main goroutine to shutdown.
59-
go func() {
60-
shutdownChannel <- struct{}{}
61-
}()
41+
// Listen for repeated signals and display a message so the user
42+
// knows the shutdown is in progress and the process is not
43+
// hung.
44+
for {
45+
select {
46+
case sig := <-interruptChannel:
47+
btcdLog.Infof("Received signal (%s). Already "+
48+
"shutting down...", sig)
6249

63-
case handler := <-addHandlerChannel:
64-
// The shutdown signal has already been received, so
65-
// just invoke and new handlers immediately.
66-
if isShutdown {
67-
handler()
50+
case <-shutdownRequestChannel:
51+
btcdLog.Info("Shutdown requested. Already " +
52+
"shutting down...")
6853
}
69-
70-
interruptCallbacks = append(interruptCallbacks, handler)
7154
}
72-
}
55+
}()
56+
57+
return c
7358
}
7459

75-
// addInterruptHandler adds a handler to call when a SIGINT (Ctrl+C) is
76-
// received.
77-
func addInterruptHandler(handler func()) {
78-
// Create the channel and start the main interrupt handler which invokes
79-
// all other callbacks and exits if not already done.
80-
if interruptChannel == nil {
81-
interruptChannel = make(chan os.Signal, 1)
82-
signal.Notify(interruptChannel, signals...)
83-
go mainInterruptHandler()
60+
// interruptRequested returns true when the channel returned by
61+
// interruptListener was closed. This simplifies early shutdown slightly since
62+
// the caller can just use an if statement instead of a select.
63+
func interruptRequested(interrupted <-chan struct{}) bool {
64+
select {
65+
case <-interrupted:
66+
return true
67+
default:
8468
}
8569

86-
addHandlerChannel <- handler
70+
return false
8771
}

signalsigterm.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ import (
1212
)
1313

1414
func init() {
15-
signals = []os.Signal{os.Interrupt, syscall.SIGTERM}
15+
interruptSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
1616
}

0 commit comments

Comments
 (0)