Improve API/GUI shutdown handling (fixes #2694)

This fixes both a race condition where we could assign s.stop from one
goroutine and then read it from another without locking, and handles the
fact that listener may be nil at shutdown if we've had a bad
CommitConfiguration call in the meantime.
This commit is contained in:
Jakob Borg 2016-01-14 11:06:36 +01:00
parent 74a210f198
commit 97b1c66d4a
2 changed files with 91 additions and 10 deletions

View File

@ -57,10 +57,14 @@ type apiService struct {
eventSub *events.BufferedSubscription eventSub *events.BufferedSubscription
discoverer *discover.CachingMux discoverer *discover.CachingMux
relayService *relay.Service relayService *relay.Service
listener net.Listener
fss *folderSummaryService fss *folderSummaryService
stop chan struct{} systemConfigMut sync.Mutex // serializes posts to /rest/system/config
systemConfigMut sync.Mutex stop chan struct{} // signals intentional stop
configChanged chan struct{} // signals intentional listener close due to config change
started chan struct{} // signals startup complete, for testing only
listener net.Listener
listenerMut sync.Mutex
guiErrors *logger.Recorder guiErrors *logger.Recorder
systemLog *logger.Recorder systemLog *logger.Recorder
@ -76,6 +80,9 @@ func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m
discoverer: discoverer, discoverer: discoverer,
relayService: relayService, relayService: relayService,
systemConfigMut: sync.NewMutex(), systemConfigMut: sync.NewMutex(),
stop: make(chan struct{}),
configChanged: make(chan struct{}),
listenerMut: sync.NewMutex(),
guiErrors: errors, guiErrors: errors,
systemLog: systemLog, systemLog: systemLog,
} }
@ -146,7 +153,15 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
} }
func (s *apiService) Serve() { func (s *apiService) Serve() {
s.stop = make(chan struct{}) s.listenerMut.Lock()
listener := s.listener
s.listenerMut.Unlock()
if listener == nil {
// Not much we can do here other than exit quickly. The supervisor
// will log an error at some point.
return
}
// The GET handlers // The GET handlers
getRestMux := http.NewServeMux() getRestMux := http.NewServeMux()
@ -249,23 +264,37 @@ func (s *apiService) Serve() {
defer s.fss.Stop() defer s.fss.Stop()
s.fss.ServeBackground() s.fss.ServeBackground()
l.Infoln("API listening on", s.listener.Addr()) l.Infoln("API listening on", listener.Addr())
l.Infoln("GUI URL is", guiCfg.URL()) l.Infoln("GUI URL is", guiCfg.URL())
err := srv.Serve(s.listener) if s.started != nil {
// only set when run by the tests
close(s.started)
}
err := srv.Serve(listener)
// The return could be due to an intentional close. Wait for the stop // The return could be due to an intentional close. Wait for the stop
// signal before returning. IF there is no stop signal within a second, we // signal before returning. IF there is no stop signal within a second, we
// assume it was unintentional and log the error before retrying. // assume it was unintentional and log the error before retrying.
select { select {
case <-s.stop: case <-s.stop:
case <-s.configChanged:
case <-time.After(time.Second): case <-time.After(time.Second):
l.Warnln("API:", err) l.Warnln("API:", err)
} }
} }
func (s *apiService) Stop() { func (s *apiService) Stop() {
s.listenerMut.Lock()
listener := s.listener
s.listenerMut.Unlock()
close(s.stop) close(s.stop)
s.listener.Close()
// listener may be nil here if we've had a config change to a broken
// configuration, in which case we shouldn't try to close it.
if listener != nil {
listener.Close()
}
} }
func (s *apiService) String() string { func (s *apiService) String() string {
@ -285,7 +314,10 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
// must create a new listener before Serve() starts again. We can't create // must create a new listener before Serve() starts again. We can't create
// a new listener on the same port before the previous listener is closed. // a new listener on the same port before the previous listener is closed.
// To assist in this little dance the Serve() method will wait for a // To assist in this little dance the Serve() method will wait for a
// signal on the stop channel after the listener has closed. // signal on the configChanged channel after the listener has closed.
s.listenerMut.Lock()
defer s.listenerMut.Unlock()
s.listener.Close() s.listener.Close()
@ -299,7 +331,7 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
return false return false
} }
close(s.stop) s.configChanged <- struct{}{}
return true return true
} }

View File

@ -6,7 +6,13 @@
package main package main
import "testing" import (
"testing"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/thejerf/suture"
)
func TestCSRFToken(t *testing.T) { func TestCSRFToken(t *testing.T) {
t1 := newCsrfToken() t1 := newCsrfToken()
@ -40,3 +46,46 @@ func TestCSRFToken(t *testing.T) {
t.Fatal("t3 should have expired by now") t.Fatal("t3 should have expired by now")
} }
} }
func TestStopAfterBrokenConfig(t *testing.T) {
baseDirs["config"] = "../../test/h1" // to load HTTPS keys
expandLocations()
cfg := config.Configuration{
GUI: config.GUIConfiguration{
RawAddress: "127.0.0.1:0",
RawUseTLS: false,
},
}
w := config.Wrap("/dev/null", cfg)
srv, err := newAPIService(protocol.LocalDeviceID, w, "", nil, nil, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
srv.started = make(chan struct{})
sup := suture.NewSimple("test")
sup.Add(srv)
sup.ServeBackground()
<-srv.started
// Service is now running, listening on a random port on localhost. Now we
// request a config change to a completely invalid listen address. The
// commit will fail and the service will be in a broken state.
newCfg := config.Configuration{
GUI: config.GUIConfiguration{
RawAddress: "totally not a valid address",
RawUseTLS: false,
},
}
if srv.CommitConfiguration(cfg, newCfg) {
t.Fatal("Config commit should have failed")
}
// Nonetheless, it should be fine to Stop() it without panic.
sup.Stop()
}