lib/model: Fix removal of paused folders, improve tests (fixes #4405)

This commit is contained in:
Audrius Butkevicius 2017-10-03 23:53:02 +01:00 committed by Jakob Borg
parent 74c8d34805
commit 9d3f3847ed
3 changed files with 73 additions and 43 deletions

View File

@ -128,15 +128,26 @@ func (w *Wrapper) RawCopy() Configuration {
return w.cfg.Copy() return w.cfg.Copy()
} }
// ReplaceBlocking swaps the current configuration object for the given one,
// and waits for subscribers to be notified.
func (w *Wrapper) ReplaceBlocking(cfg Configuration) error {
w.mut.Lock()
wg := sync.NewWaitGroup()
err := w.replaceLocked(cfg, wg)
w.mut.Unlock()
wg.Wait()
return err
}
// Replace swaps the current configuration object for the given one. // Replace swaps the current configuration object for the given one.
func (w *Wrapper) Replace(cfg Configuration) error { func (w *Wrapper) Replace(cfg Configuration) error {
w.mut.Lock() w.mut.Lock()
defer w.mut.Unlock() defer w.mut.Unlock()
return w.replaceLocked(cfg) return w.replaceLocked(cfg, nil)
} }
func (w *Wrapper) replaceLocked(to Configuration) error { func (w *Wrapper) replaceLocked(to Configuration, wg sync.WaitGroup) error {
from := w.cfg from := w.cfg
if err := to.clean(); err != nil { if err := to.clean(); err != nil {
@ -155,14 +166,22 @@ func (w *Wrapper) replaceLocked(to Configuration) error {
w.deviceMap = nil w.deviceMap = nil
w.folderMap = nil w.folderMap = nil
w.notifyListeners(from, to) w.notifyListeners(from, to, wg)
return nil return nil
} }
func (w *Wrapper) notifyListeners(from, to Configuration) { func (w *Wrapper) notifyListeners(from, to Configuration, wg sync.WaitGroup) {
if wg != nil {
wg.Add(len(w.subs))
}
for _, sub := range w.subs { for _, sub := range w.subs {
go w.notifyListener(sub, from.Copy(), to.Copy()) go func(commiter Committer) {
w.notifyListener(commiter, from.Copy(), to.Copy())
if wg != nil {
wg.Done()
}
}(sub)
} }
} }
@ -210,7 +229,7 @@ func (w *Wrapper) SetDevices(devs []DeviceConfiguration) error {
} }
} }
return w.replaceLocked(newCfg) return w.replaceLocked(newCfg, nil)
} }
// SetDevice adds a new device to the configuration, or overwrites an existing // SetDevice adds a new device to the configuration, or overwrites an existing
@ -237,7 +256,7 @@ func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
return nil return nil
} }
return w.replaceLocked(newCfg) return w.replaceLocked(newCfg, nil)
} }
// Folders returns a map of folders. Folder structures should not be changed, // Folders returns a map of folders. Folder structures should not be changed,
@ -273,7 +292,7 @@ func (w *Wrapper) SetFolder(fld FolderConfiguration) error {
newCfg.Folders = append(w.cfg.Folders, fld) newCfg.Folders = append(w.cfg.Folders, fld)
} }
return w.replaceLocked(newCfg) return w.replaceLocked(newCfg, nil)
} }
// Options returns the current options configuration object. // Options returns the current options configuration object.
@ -289,7 +308,7 @@ func (w *Wrapper) SetOptions(opts OptionsConfiguration) error {
defer w.mut.Unlock() defer w.mut.Unlock()
newCfg := w.cfg.Copy() newCfg := w.cfg.Copy()
newCfg.Options = opts newCfg.Options = opts
return w.replaceLocked(newCfg) return w.replaceLocked(newCfg, nil)
} }
// GUI returns the current GUI configuration object. // GUI returns the current GUI configuration object.
@ -305,7 +324,7 @@ func (w *Wrapper) SetGUI(gui GUIConfiguration) error {
defer w.mut.Unlock() defer w.mut.Unlock()
newCfg := w.cfg.Copy() newCfg := w.cfg.Copy()
newCfg.GUI = gui newCfg.GUI = gui
return w.replaceLocked(newCfg) return w.replaceLocked(newCfg, nil)
} }
// IgnoredDevice returns whether or not connection attempts from the given // IgnoredDevice returns whether or not connection attempts from the given

View File

@ -335,25 +335,15 @@ func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
m.folderIgnores[cfg.ID] = ignores m.folderIgnores[cfg.ID] = ignores
} }
func (m *Model) RemoveFolder(folder string) { func (m *Model) RemoveFolder(cfg config.FolderConfiguration) {
m.fmut.Lock() m.fmut.Lock()
m.pmut.Lock() m.pmut.Lock()
// Delete syncthing specific files // Delete syncthing specific files
folderCfg, ok := m.folderCfgs[folder] cfg.Filesystem().RemoveAll(".stfolder")
if !ok {
// Folder might be paused
folderCfg, ok = m.cfg.Folder(folder)
}
if !ok {
panic("bug: remove non existing folder")
}
fs := folderCfg.Filesystem()
fs.RemoveAll(".stfolder")
m.tearDownFolderLocked(folder) m.tearDownFolderLocked(cfg.ID)
// Remove it from the database // Remove it from the database
db.DropFolder(m.db, folder) db.DropFolder(m.db, cfg.ID)
m.pmut.Unlock() m.pmut.Unlock()
m.fmut.Unlock() m.fmut.Unlock()
@ -2496,7 +2486,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
toCfg, ok := toFolders[folderID] toCfg, ok := toFolders[folderID]
if !ok { if !ok {
// The folder was removed. // The folder was removed.
m.RemoveFolder(folderID) m.RemoveFolder(fromCfg)
continue continue
} }

View File

@ -1907,48 +1907,69 @@ func TestIssue3164(t *testing.T) {
func TestIssue4357(t *testing.T) { func TestIssue4357(t *testing.T) {
db := db.OpenMemory() db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", db, nil) cfg := defaultConfig.RawCopy()
// Create a separate wrapper not to polute other tests.
wrapper := config.Wrap("/tmp/test", config.Configuration{})
m := NewModel(wrapper, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
m.ServeBackground() m.ServeBackground()
defer m.Stop() defer m.Stop()
cfg := m.cfg.RawCopy()
m.CommitConfiguration(config.Configuration{}, cfg) // Replace kicks off CommitConfiguration in multiple routines which we
// have no idea when they get called, so sleep a bit :(
// We could make replace blocking for all routines to come back without
// holding the lock, but that's a bit much just to make a test.
replaceCfg := func(cfg config.Configuration) {
t.Helper()
if err := wrapper.ReplaceBlocking(cfg); err != nil {
t.Error(err)
}
}
// Force the model to wire itself and add the folders
replaceCfg(cfg)
if _, ok := m.folderCfgs["default"]; !ok { if _, ok := m.folderCfgs["default"]; !ok {
t.Error("Folder should be running") t.Error("Folder should be running")
} }
newCfg := m.cfg.RawCopy() newCfg := wrapper.RawCopy()
newCfg.Folders[0].Paused = true newCfg.Folders[0].Paused = true
m.CommitConfiguration(cfg, newCfg)
replaceCfg(newCfg)
if _, ok := m.folderCfgs["default"]; ok { if _, ok := m.folderCfgs["default"]; ok {
t.Error("Folder should not be running") t.Error("Folder should not be running")
} }
// Should not panic when removing a paused folder. if _, ok := m.cfg.Folder("default"); !ok {
m.RemoveFolder("default") t.Error("should still have folder in config")
}
replaceCfg(config.Configuration{})
if _, ok := m.cfg.Folder("default"); ok {
t.Error("should not have folder in config")
}
// Add the folder back, should be running // Add the folder back, should be running
m.CommitConfiguration(config.Configuration{}, cfg) replaceCfg(cfg)
if _, ok := m.folderCfgs["default"]; !ok { if _, ok := m.folderCfgs["default"]; !ok {
t.Error("Folder should be running") t.Error("Folder should be running")
} }
if _, ok := m.cfg.Folder("default"); !ok {
t.Error("should still have folder in config")
}
// Should not panic when removing a running folder. // Should not panic when removing a running folder.
m.RemoveFolder("default") replaceCfg(config.Configuration{})
if _, ok := m.folderCfgs["default"]; ok { if _, ok := m.folderCfgs["default"]; ok {
t.Error("Folder should not be running") t.Error("Folder should not be running")
} }
if _, ok := m.cfg.Folder("default"); ok {
// Should panic when removing a non-existing folder t.Error("should not have folder in config")
defer func() { }
if recover() == nil {
t.Error("expected a panic")
}
}()
m.RemoveFolder("non-existing")
} }
func TestScanNoDatabaseWrite(t *testing.T) { func TestScanNoDatabaseWrite(t *testing.T) {