Przeglądaj źródła

Refactor state tracking (...)

Move state tracking into the puller/scanner objects. This is a first
step towards resolving #1391.

Rename Puller and Scanner to roFolder and rwFolder as they have more
duties than just pulling and scanning, and don't need to be exported.
Jakob Borg 10 lat temu
rodzic
commit
bdbca75dfa

+ 89 - 0
internal/model/folderstate.go

@@ -0,0 +1,89 @@
+// Copyright (C) 2015 The Syncthing Authors.
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package model
+
+import (
+	"sync"
+	"time"
+
+	"github.com/syncthing/syncthing/internal/events"
+)
+
+type folderState int
+
+const (
+	FolderIdle folderState = iota
+	FolderScanning
+	FolderSyncing
+	FolderCleaning
+)
+
+func (s folderState) String() string {
+	switch s {
+	case FolderIdle:
+		return "idle"
+	case FolderScanning:
+		return "scanning"
+	case FolderCleaning:
+		return "cleaning"
+	case FolderSyncing:
+		return "syncing"
+	default:
+		return "unknown"
+	}
+}
+
+type stateTracker struct {
+	folder string
+
+	mut     sync.Mutex
+	current folderState
+	changed time.Time
+}
+
+func (s *stateTracker) setState(newState folderState) {
+	s.mut.Lock()
+	if newState != s.current {
+		/* This should hold later...
+		if s.current != FolderIdle && (newState == FolderScanning || newState == FolderSyncing) {
+			panic("illegal state transition " + s.current.String() + " -> " + newState.String())
+		}
+		*/
+
+		eventData := map[string]interface{}{
+			"folder": s.folder,
+			"to":     newState.String(),
+			"from":   s.current.String(),
+		}
+
+		if !s.changed.IsZero() {
+			eventData["duration"] = time.Since(s.changed).Seconds()
+		}
+
+		s.current = newState
+		s.changed = time.Now()
+
+		events.Default.Log(events.StateChanged, eventData)
+	}
+	s.mut.Unlock()
+}
+
+func (s *stateTracker) getState() (current folderState, changed time.Time) {
+	s.mut.Lock()
+	current, changed = s.current, s.changed
+	s.mut.Unlock()
+	return
+}

+ 40 - 95
internal/model/model.go

@@ -36,30 +36,6 @@ import (
 	"github.com/syndtr/goleveldb/leveldb"
 )
 
-type folderState int
-
-const (
-	FolderIdle folderState = iota
-	FolderScanning
-	FolderSyncing
-	FolderCleaning
-)
-
-func (s folderState) String() string {
-	switch s {
-	case FolderIdle:
-		return "idle"
-	case FolderScanning:
-		return "scanning"
-	case FolderCleaning:
-		return "cleaning"
-	case FolderSyncing:
-		return "syncing"
-	default:
-		return "unknown"
-	}
-}
-
 // How many files to send in each Index/IndexUpdate message.
 const (
 	indexTargetSize   = 250 * 1024 // Aim for making index messages no larger than 250 KiB (uncompressed)
@@ -73,6 +49,9 @@ type service interface {
 	Stop()
 	Jobs() ([]string, []string) // In progress, Queued
 	BringToFront(string)
+
+	setState(folderState)
+	getState() (folderState, time.Time)
 }
 
 type Model struct {
@@ -95,10 +74,6 @@ type Model struct {
 	folderStatRefs map[string]*stats.FolderStatisticsReference            // folder -> statsRef
 	fmut           sync.RWMutex                                           // protects the above
 
-	folderState        map[string]folderState // folder -> state
-	folderStateChanged map[string]time.Time   // folder -> time when state changed
-	smut               sync.RWMutex
-
 	protoConn map[protocol.DeviceID]protocol.Connection
 	rawConn   map[protocol.DeviceID]io.Closer
 	deviceVer map[protocol.DeviceID]string
@@ -120,26 +95,24 @@ var (
 // for file data without altering the local folder in any way.
 func NewModel(cfg *config.Wrapper, deviceName, clientName, clientVersion string, ldb *leveldb.DB) *Model {
 	m := &Model{
-		cfg:                cfg,
-		db:                 ldb,
-		deviceName:         deviceName,
-		clientName:         clientName,
-		clientVersion:      clientVersion,
-		folderCfgs:         make(map[string]config.FolderConfiguration),
-		folderFiles:        make(map[string]*db.FileSet),
-		folderDevices:      make(map[string][]protocol.DeviceID),
-		deviceFolders:      make(map[protocol.DeviceID][]string),
-		deviceStatRefs:     make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
-		folderIgnores:      make(map[string]*ignore.Matcher),
-		folderRunners:      make(map[string]service),
-		folderStatRefs:     make(map[string]*stats.FolderStatisticsReference),
-		folderState:        make(map[string]folderState),
-		folderStateChanged: make(map[string]time.Time),
-		protoConn:          make(map[protocol.DeviceID]protocol.Connection),
-		rawConn:            make(map[protocol.DeviceID]io.Closer),
-		deviceVer:          make(map[protocol.DeviceID]string),
-		finder:             db.NewBlockFinder(ldb, cfg),
-		progressEmitter:    NewProgressEmitter(cfg),
+		cfg:             cfg,
+		db:              ldb,
+		deviceName:      deviceName,
+		clientName:      clientName,
+		clientVersion:   clientVersion,
+		folderCfgs:      make(map[string]config.FolderConfiguration),
+		folderFiles:     make(map[string]*db.FileSet),
+		folderDevices:   make(map[string][]protocol.DeviceID),
+		deviceFolders:   make(map[protocol.DeviceID][]string),
+		deviceStatRefs:  make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
+		folderIgnores:   make(map[string]*ignore.Matcher),
+		folderRunners:   make(map[string]service),
+		folderStatRefs:  make(map[string]*stats.FolderStatisticsReference),
+		protoConn:       make(map[protocol.DeviceID]protocol.Connection),
+		rawConn:         make(map[protocol.DeviceID]io.Closer),
+		deviceVer:       make(map[protocol.DeviceID]string),
+		finder:          db.NewBlockFinder(ldb, cfg),
+		progressEmitter: NewProgressEmitter(cfg),
 	}
 	if cfg.Options().ProgressUpdateIntervalS > -1 {
 		go m.progressEmitter.Serve()
@@ -153,7 +126,6 @@ func NewModel(cfg *config.Wrapper, deviceName, clientName, clientVersion string,
 		}
 	}
 	deadlockDetect(&m.fmut, time.Duration(timeout)*time.Second)
-	deadlockDetect(&m.smut, time.Duration(timeout)*time.Second)
 	deadlockDetect(&m.pmut, time.Duration(timeout)*time.Second)
 	return m
 }
@@ -172,18 +144,7 @@ func (m *Model) StartFolderRW(folder string) {
 	if ok {
 		panic("cannot start already running folder " + folder)
 	}
-	p := &Puller{
-		folder:          folder,
-		dir:             cfg.Path,
-		scanIntv:        time.Duration(cfg.RescanIntervalS) * time.Second,
-		model:           m,
-		ignorePerms:     cfg.IgnorePerms,
-		lenientMtimes:   cfg.LenientMtimes,
-		progressEmitter: m.progressEmitter,
-		copiers:         cfg.Copiers,
-		pullers:         cfg.Pullers,
-		queue:           newJobQueue(),
-	}
+	p := newRWFolder(m, cfg)
 	m.folderRunners[folder] = p
 	m.fmut.Unlock()
 
@@ -216,11 +177,7 @@ func (m *Model) StartFolderRO(folder string) {
 	if ok {
 		panic("cannot start already running folder " + folder)
 	}
-	s := &Scanner{
-		folder: folder,
-		intv:   time.Duration(cfg.RescanIntervalS) * time.Second,
-		model:  m,
-	}
+	s := newROFolder(m, folder, time.Duration(cfg.RescanIntervalS)*time.Second)
 	m.folderRunners[folder] = s
 	m.fmut.Unlock()
 
@@ -1154,11 +1111,15 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
 	}
 
 	m.fmut.Lock()
-	fs, ok := m.folderFiles[folder]
+	fs := m.folderFiles[folder]
 	folderCfg := m.folderCfgs[folder]
 	ignores := m.folderIgnores[folder]
+	runner, ok := m.folderRunners[folder]
 	m.fmut.Unlock()
 
+	// Folders are added to folderRunners only when they are started. We can't
+	// scan them before they have started, so that's what we need to check for
+	// here.
 	if !ok {
 		return errors.New("no such folder")
 	}
@@ -1189,7 +1150,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
 		Hashers:      folderCfg.Hashers,
 	}
 
-	m.setState(folder, FolderScanning)
+	runner.setState(FolderScanning)
 	fchan, err := w.Walk()
 
 	if err != nil {
@@ -1289,7 +1250,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
 		fs.Update(protocol.LocalDeviceID, batch)
 	}
 
-	m.setState(folder, FolderIdle)
+	runner.setState(FolderIdle)
 	return nil
 }
 
@@ -1332,40 +1293,24 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe
 	return cm
 }
 
-func (m *Model) setState(folder string, state folderState) {
-	m.smut.Lock()
-	oldState := m.folderState[folder]
-	changed, ok := m.folderStateChanged[folder]
-	if state != oldState {
-		m.folderState[folder] = state
-		m.folderStateChanged[folder] = time.Now()
-		eventData := map[string]interface{}{
-			"folder": folder,
-			"to":     state.String(),
-		}
-		if ok {
-			eventData["duration"] = time.Since(changed).Seconds()
-			eventData["from"] = oldState.String()
-		}
-		events.Default.Log(events.StateChanged, eventData)
-	}
-	m.smut.Unlock()
-}
-
 func (m *Model) State(folder string) (string, time.Time) {
-	m.smut.RLock()
-	state := m.folderState[folder]
-	changed := m.folderStateChanged[folder]
-	m.smut.RUnlock()
+	m.fmut.RLock()
+	runner, ok := m.folderRunners[folder]
+	m.fmut.RUnlock()
+	if !ok {
+		return "", time.Time{}
+	}
+	state, changed := runner.getState()
 	return state.String(), changed
 }
 
 func (m *Model) Override(folder string) {
 	m.fmut.RLock()
 	fs := m.folderFiles[folder]
+	runner := m.folderRunners[folder]
 	m.fmut.RUnlock()
 
-	m.setState(folder, FolderScanning)
+	runner.setState(FolderScanning)
 	batch := make([]protocol.FileInfo, 0, indexBatchSize)
 	fs.WithNeed(protocol.LocalDeviceID, func(fi db.FileIntf) bool {
 		need := fi.(protocol.FileInfo)
@@ -1391,7 +1336,7 @@ func (m *Model) Override(folder string) {
 	if len(batch) > 0 {
 		fs.Update(protocol.LocalDeviceID, batch)
 	}
-	m.setState(folder, FolderIdle)
+	runner.setState(FolderIdle)
 }
 
 // CurrentLocalVersion returns the change version for the given folder.

+ 2 - 0
internal/model/model_test.go

@@ -90,6 +90,7 @@ func TestRequest(t *testing.T) {
 
 	// device1 shares default, but device2 doesn't
 	m.AddFolder(defaultFolderConfig)
+	m.StartFolderRO("default")
 	m.ScanFolder("default")
 
 	// Existing, shared file
@@ -470,6 +471,7 @@ func TestIgnores(t *testing.T) {
 	db, _ := leveldb.Open(storage.NewMemStorage(), nil)
 	m := NewModel(defaultConfig, "device", "syncthing", "dev", db)
 	m.AddFolder(defaultFolderConfig)
+	m.StartFolderRO("default")
 
 	expected := []string{
 		".*",

+ 65 - 43
internal/model/puller.go

@@ -54,31 +54,53 @@ var (
 	errNoDevice = errors.New("no available source device")
 )
 
-type Puller struct {
-	folder          string
-	dir             string
-	scanIntv        time.Duration
+type rwFolder struct {
+	stateTracker
+
 	model           *Model
-	stop            chan struct{}
-	versioner       versioner.Versioner
-	ignorePerms     bool
-	lenientMtimes   bool
 	progressEmitter *ProgressEmitter
-	copiers         int
-	pullers         int
-	queue           *jobQueue
+
+	folder        string
+	dir           string
+	scanIntv      time.Duration
+	versioner     versioner.Versioner
+	ignorePerms   bool
+	lenientMtimes bool
+	copiers       int
+	pullers       int
+
+	stop  chan struct{}
+	queue *jobQueue
+}
+
+func newRWFolder(m *Model, cfg config.FolderConfiguration) *rwFolder {
+	return &rwFolder{
+		stateTracker: stateTracker{folder: cfg.ID},
+
+		model:           m,
+		progressEmitter: m.progressEmitter,
+
+		folder:        cfg.ID,
+		dir:           cfg.Path,
+		scanIntv:      time.Duration(cfg.RescanIntervalS) * time.Second,
+		ignorePerms:   cfg.IgnorePerms,
+		lenientMtimes: cfg.LenientMtimes,
+		copiers:       cfg.Copiers,
+		pullers:       cfg.Pullers,
+
+		stop:  make(chan struct{}),
+		queue: newJobQueue(),
+	}
 }
 
 // Serve will run scans and pulls. It will return when Stop()ed or on a
 // critical error.
-func (p *Puller) Serve() {
+func (p *rwFolder) Serve() {
 	if debug {
 		l.Debugln(p, "starting")
 		defer l.Debugln(p, "exiting")
 	}
 
-	p.stop = make(chan struct{})
-
 	pullTimer := time.NewTimer(checkPullIntv)
 	scanTimer := time.NewTimer(time.Millisecond) // The first scan should be done immediately.
 
@@ -86,7 +108,7 @@ func (p *Puller) Serve() {
 		pullTimer.Stop()
 		scanTimer.Stop()
 		// TODO: Should there be an actual FolderStopped state?
-		p.model.setState(p.folder, FolderIdle)
+		p.setState(FolderIdle)
 	}()
 
 	var prevVer int64
@@ -143,7 +165,7 @@ loop:
 			if debug {
 				l.Debugln(p, "pulling", prevVer, curVer)
 			}
-			p.model.setState(p.folder, FolderSyncing)
+			p.setState(FolderSyncing)
 			tries := 0
 			for {
 				tries++
@@ -191,7 +213,7 @@ loop:
 					break
 				}
 			}
-			p.model.setState(p.folder, FolderIdle)
+			p.setState(FolderIdle)
 
 		// The reason for running the scanner from within the puller is that
 		// this is the easiest way to make sure we are not doing both at the
@@ -200,12 +222,12 @@ loop:
 			if debug {
 				l.Debugln(p, "rescan")
 			}
-			p.model.setState(p.folder, FolderScanning)
+			p.setState(FolderScanning)
 			if err := p.model.ScanFolder(p.folder); err != nil {
 				p.model.cfg.InvalidateFolder(p.folder, err.Error())
 				break loop
 			}
-			p.model.setState(p.folder, FolderIdle)
+			p.setState(FolderIdle)
 			if p.scanIntv > 0 {
 				// Sleep a random time between 3/4 and 5/4 of the configured interval.
 				sleepNanos := (p.scanIntv.Nanoseconds()*3 + rand.Int63n(2*p.scanIntv.Nanoseconds())) / 4
@@ -224,19 +246,19 @@ loop:
 	}
 }
 
-func (p *Puller) Stop() {
+func (p *rwFolder) Stop() {
 	close(p.stop)
 }
 
-func (p *Puller) String() string {
-	return fmt.Sprintf("puller/%s@%p", p.folder, p)
+func (p *rwFolder) String() string {
+	return fmt.Sprintf("rwFolder/%s@%p", p.folder, p)
 }
 
 // pullerIteration runs a single puller iteration for the given folder and
 // returns the number items that should have been synced (even those that
 // might have failed). One puller iteration handles all files currently
 // flagged as needed in the folder.
-func (p *Puller) pullerIteration(ignores *ignore.Matcher) int {
+func (p *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
 	pullChan := make(chan pullBlockState)
 	copyChan := make(chan copyBlocksState)
 	finisherChan := make(chan *sharedPullerState)
@@ -422,7 +444,7 @@ nextFile:
 }
 
 // handleDir creates or updates the given directory
-func (p *Puller) handleDir(file protocol.FileInfo) {
+func (p *rwFolder) handleDir(file protocol.FileInfo) {
 	var err error
 	events.Default.Log(events.ItemStarted, map[string]interface{}{
 		"folder":  p.folder,
@@ -497,7 +519,7 @@ func (p *Puller) handleDir(file protocol.FileInfo) {
 }
 
 // deleteDir attempts to delete the given directory
-func (p *Puller) deleteDir(file protocol.FileInfo) {
+func (p *rwFolder) deleteDir(file protocol.FileInfo) {
 	var err error
 	events.Default.Log(events.ItemStarted, map[string]interface{}{
 		"folder":  p.folder,
@@ -532,7 +554,7 @@ func (p *Puller) deleteDir(file protocol.FileInfo) {
 }
 
 // deleteFile attempts to delete the given file
-func (p *Puller) deleteFile(file protocol.FileInfo) {
+func (p *rwFolder) deleteFile(file protocol.FileInfo) {
 	var err error
 	events.Default.Log(events.ItemStarted, map[string]interface{}{
 		"folder":  p.folder,
@@ -564,7 +586,7 @@ func (p *Puller) deleteFile(file protocol.FileInfo) {
 
 // renameFile attempts to rename an existing file to a destination
 // and set the right attributes on it.
-func (p *Puller) renameFile(source, target protocol.FileInfo) {
+func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
 	var err error
 	events.Default.Log(events.ItemStarted, map[string]interface{}{
 		"folder":  p.folder,
@@ -634,7 +656,7 @@ func (p *Puller) renameFile(source, target protocol.FileInfo) {
 
 // handleFile queues the copies and pulls as necessary for a single new or
 // changed file.
-func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
+func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
 	events.Default.Log(events.ItemStarted, map[string]interface{}{
 		"folder":  p.folder,
 		"item":    file.Name,
@@ -732,7 +754,7 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt
 
 // shortcutFile sets file mode and modification time, when that's the only
 // thing that has changed.
-func (p *Puller) shortcutFile(file protocol.FileInfo) (err error) {
+func (p *rwFolder) shortcutFile(file protocol.FileInfo) (err error) {
 	realName := filepath.Join(p.dir, file.Name)
 	if !p.ignorePerms {
 		err = os.Chmod(realName, os.FileMode(file.Flags&0777))
@@ -763,7 +785,7 @@ func (p *Puller) shortcutFile(file protocol.FileInfo) (err error) {
 }
 
 // shortcutSymlink changes the symlinks type if necessery.
-func (p *Puller) shortcutSymlink(file protocol.FileInfo) (err error) {
+func (p *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
 	err = symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
 	if err == nil {
 		p.model.updateLocal(p.folder, file)
@@ -775,7 +797,7 @@ func (p *Puller) shortcutSymlink(file protocol.FileInfo) (err error) {
 
 // copierRoutine reads copierStates until the in channel closes and performs
 // the relevant copies when possible, or passes it to the puller routine.
-func (p *Puller) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
+func (p *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
 	buf := make([]byte, protocol.BlockSize)
 
 	for state := range in {
@@ -857,7 +879,7 @@ func (p *Puller) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBl
 	}
 }
 
-func (p *Puller) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
+func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
 	for state := range in {
 		if state.failed() != nil {
 			continue
@@ -918,7 +940,7 @@ func (p *Puller) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPulle
 	}
 }
 
-func (p *Puller) performFinish(state *sharedPullerState) {
+func (p *rwFolder) performFinish(state *sharedPullerState) {
 	var err error
 	defer func() {
 		events.Default.Log(events.ItemFinished, map[string]interface{}{
@@ -931,7 +953,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 	if !p.ignorePerms {
 		err = os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777))
 		if err != nil {
-			l.Warnln("puller: final:", err)
+			l.Warnln("Puller: final:", err)
 			return
 		}
 	}
@@ -947,7 +969,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 			// sync.
 			l.Infof("Puller (folder %q, file %q): final: %v (continuing anyway as requested)", p.folder, state.file.Name, err)
 		} else {
-			l.Warnln("puller: final:", err)
+			l.Warnln("Puller: final:", err)
 			return
 		}
 	}
@@ -958,7 +980,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 	if p.versioner != nil {
 		err = p.versioner.Archive(state.realName)
 		if err != nil {
-			l.Warnln("puller: final:", err)
+			l.Warnln("Puller: final:", err)
 			return
 		}
 	}
@@ -972,7 +994,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 	// Replace the original content with the new one
 	err = osutil.Rename(state.tempName, state.realName)
 	if err != nil {
-		l.Warnln("puller: final:", err)
+		l.Warnln("Puller: final:", err)
 		return
 	}
 
@@ -980,7 +1002,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 	if state.file.IsSymlink() {
 		content, err := ioutil.ReadFile(state.realName)
 		if err != nil {
-			l.Warnln("puller: final: reading symlink:", err)
+			l.Warnln("Puller: final: reading symlink:", err)
 			return
 		}
 
@@ -990,7 +1012,7 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 			return symlinks.Create(path, string(content), state.file.Flags)
 		}, state.realName)
 		if err != nil {
-			l.Warnln("puller: final: creating symlink:", err)
+			l.Warnln("Puller: final: creating symlink:", err)
 			return
 		}
 	}
@@ -999,14 +1021,14 @@ func (p *Puller) performFinish(state *sharedPullerState) {
 	p.model.updateLocal(p.folder, state.file)
 }
 
-func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
+func (p *rwFolder) finisherRoutine(in <-chan *sharedPullerState) {
 	for state := range in {
 		if closed, err := state.finalClose(); closed {
 			if debug {
 				l.Debugln(p, "closing", state.file.Name)
 			}
 			if err != nil {
-				l.Warnln("puller: final:", err)
+				l.Warnln("Puller: final:", err)
 				continue
 			}
 
@@ -1029,11 +1051,11 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
 }
 
 // Moves the given filename to the front of the job queue
-func (p *Puller) BringToFront(filename string) {
+func (p *rwFolder) BringToFront(filename string) {
 	p.queue.BringToFront(filename)
 }
 
-func (p *Puller) Jobs() ([]string, []string) {
+func (p *rwFolder) Jobs() ([]string, []string) {
 	return p.queue.Jobs()
 }
 

+ 6 - 6
internal/model/puller_test.go

@@ -72,7 +72,7 @@ func TestHandleFile(t *testing.T) {
 	// Update index
 	m.updateLocal("default", existingFile)
 
-	p := Puller{
+	p := rwFolder{
 		folder: "default",
 		dir:    "testdata",
 		model:  m,
@@ -126,7 +126,7 @@ func TestHandleFileWithTemp(t *testing.T) {
 	// Update index
 	m.updateLocal("default", existingFile)
 
-	p := Puller{
+	p := rwFolder{
 		folder: "default",
 		dir:    "testdata",
 		model:  m,
@@ -197,7 +197,7 @@ func TestCopierFinder(t *testing.T) {
 		}
 	}
 
-	p := Puller{
+	p := rwFolder{
 		folder: "default",
 		dir:    "testdata",
 		model:  m,
@@ -331,7 +331,7 @@ func TestLastResortPulling(t *testing.T) {
 		t.Error("Expected block not found")
 	}
 
-	p := Puller{
+	p := rwFolder{
 		folder: "default",
 		dir:    "testdata",
 		model:  m,
@@ -384,7 +384,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
 	emitter := NewProgressEmitter(defaultConfig)
 	go emitter.Serve()
 
-	p := Puller{
+	p := rwFolder{
 		folder:          "default",
 		dir:             "testdata",
 		model:           m,
@@ -471,7 +471,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 	emitter := NewProgressEmitter(defaultConfig)
 	go emitter.Serve()
 
-	p := Puller{
+	p := rwFolder{
 		folder:          "default",
 		dir:             "testdata",
 		model:           m,

+ 21 - 9
internal/model/scanner.go

@@ -12,14 +12,26 @@ import (
 	"time"
 )
 
-type Scanner struct {
+type roFolder struct {
+	stateTracker
+
 	folder string
 	intv   time.Duration
 	model  *Model
 	stop   chan struct{}
 }
 
-func (s *Scanner) Serve() {
+func newROFolder(model *Model, folder string, interval time.Duration) *roFolder {
+	return &roFolder{
+		stateTracker: stateTracker{folder: folder},
+		folder:       folder,
+		intv:         interval,
+		model:        model,
+		stop:         make(chan struct{}),
+	}
+}
+
+func (s *roFolder) Serve() {
 	if debug {
 		l.Debugln(s, "starting")
 		defer l.Debugln(s, "exiting")
@@ -39,12 +51,12 @@ func (s *Scanner) Serve() {
 				l.Debugln(s, "rescan")
 			}
 
-			s.model.setState(s.folder, FolderScanning)
+			s.setState(FolderScanning)
 			if err := s.model.ScanFolder(s.folder); err != nil {
 				s.model.cfg.InvalidateFolder(s.folder, err.Error())
 				return
 			}
-			s.model.setState(s.folder, FolderIdle)
+			s.setState(FolderIdle)
 
 			if !initialScanCompleted {
 				l.Infoln("Completed initial scan (ro) of folder", s.folder)
@@ -62,16 +74,16 @@ func (s *Scanner) Serve() {
 	}
 }
 
-func (s *Scanner) Stop() {
+func (s *roFolder) Stop() {
 	close(s.stop)
 }
 
-func (s *Scanner) String() string {
-	return fmt.Sprintf("scanner/%s@%p", s.folder, s)
+func (s *roFolder) String() string {
+	return fmt.Sprintf("roFolder/%s@%p", s.folder, s)
 }
 
-func (s *Scanner) BringToFront(string) {}
+func (s *roFolder) BringToFront(string) {}
 
-func (s *Scanner) Jobs() ([]string, []string) {
+func (s *roFolder) Jobs() ([]string, []string) {
 	return nil, nil
 }

+ 17 - 1
test/ignore_test.go

@@ -13,6 +13,7 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 
 	"github.com/syncthing/syncthing/internal/symlinks"
 )
@@ -71,7 +72,22 @@ func TestIgnores(t *testing.T) {
 
 	// Rescan and verify that we see them all
 
-	p.post("/rest/scan?folder=default", nil)
+	// Wait for one scan to succeed, or up to 20 seconds...
+	// This is to let startup, UPnP etc complete.
+	for i := 0; i < 20; i++ {
+		resp, err := p.post("/rest/scan?folder=default", nil)
+		if err != nil {
+			time.Sleep(time.Second)
+			continue
+		}
+		if resp.StatusCode != 200 {
+			resp.Body.Close()
+			time.Sleep(time.Second)
+			continue
+		}
+		break
+	}
+
 	m, err := p.model("default")
 	if err != nil {
 		t.Fatal(err)

+ 20 - 2
test/parallell_scan_test.go

@@ -29,7 +29,7 @@ func TestParallellScan(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	log.Println("Generaing .stignore...")
+	log.Println("Generating .stignore...")
 	err = ioutil.WriteFile("s1/.stignore", []byte("some ignore data\n"), 0644)
 	if err != nil {
 		t.Fatal(err)
@@ -46,7 +46,25 @@ func TestParallellScan(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	time.Sleep(5 * time.Second)
+
+	// Wait for one scan to succeed, or up to 20 seconds...
+	// This is to let startup, UPnP etc complete.
+	for i := 0; i < 20; i++ {
+		resp, err := st.post("/rest/scan?folder=default", nil)
+		if err != nil {
+			time.Sleep(time.Second)
+			continue
+		}
+		if resp.StatusCode != 200 {
+			resp.Body.Close()
+			time.Sleep(time.Second)
+			continue
+		}
+		break
+	}
+
+	// Wait for UPnP and stuff
+	time.Sleep(10 * time.Second)
 
 	var wg sync.WaitGroup
 	log.Println("Starting scans...")

+ 16 - 2
test/transfer-bench_test.go

@@ -43,8 +43,22 @@ func TestBenchmarkTransfer(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	// Make sure the sender has the full index before they connect
-	sender.post("/rest/scan?folder=default", nil)
+	// Wait for one scan to succeed, or up to 20 seconds... This is to let
+	// startup, UPnP etc complete and make sure the sender has the full index
+	// before they connect.
+	for i := 0; i < 20; i++ {
+		resp, err := sender.post("/rest/scan?folder=default", nil)
+		if err != nil {
+			time.Sleep(time.Second)
+			continue
+		}
+		if resp.StatusCode != 200 {
+			resp.Body.Close()
+			time.Sleep(time.Second)
+			continue
+		}
+		break
+	}
 
 	log.Println("Starting receiver...")
 	receiver := syncthingProcess{ // id2