Pārlūkot izejas kodu

all: Handle errors opening db/creating file-set (ref #5907) (#7150)

Simon Frei 4 gadi atpakaļ
vecāks
revīzija
78bd0341a8

+ 5 - 1
cmd/syncthing/main.go

@@ -681,7 +681,11 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 		appOpts.DBIndirectGCInterval = dur
 	}
 
-	app := syncthing.New(cfg, ldb, evLogger, cert, appOpts)
+	app, err := syncthing.New(cfg, ldb, evLogger, cert, appOpts)
+	if err != nil {
+		l.Warnln("Failed to start Syncthing:", err)
+		os.Exit(util.ExitError.AsInt())
+	}
 
 	if autoUpgradePossible {
 		go autoUpgrade(cfg, app, evLogger)

+ 20 - 21
lib/db/benchmark_test.go

@@ -11,7 +11,6 @@ import (
 	"testing"
 
 	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -44,11 +43,11 @@ func lazyInitBenchFiles() {
 	}
 }
 
-func getBenchFileSet() (*db.Lowlevel, *db.FileSet) {
+func getBenchFileSet(b testing.TB) (*db.Lowlevel, *db.FileSet) {
 	lazyInitBenchFiles()
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
-	benchS := db.NewFileSet("test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	ldb := newLowlevelMemory(b)
+	benchS := newFileSet(b, "test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	replace(benchS, remoteDevice0, files)
 	replace(benchS, protocol.LocalDeviceID, firstHalf)
 
@@ -56,12 +55,12 @@ func getBenchFileSet() (*db.Lowlevel, *db.FileSet) {
 }
 
 func BenchmarkReplaceAll(b *testing.B) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		m := db.NewFileSet("test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+		m := newFileSet(b, "test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 		replace(m, protocol.LocalDeviceID, files)
 	}
 
@@ -69,7 +68,7 @@ func BenchmarkReplaceAll(b *testing.B) {
 }
 
 func BenchmarkUpdateOneChanged(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	changed := make([]protocol.FileInfo, 1)
@@ -89,7 +88,7 @@ func BenchmarkUpdateOneChanged(b *testing.B) {
 }
 
 func BenchmarkUpdate100Changed(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -118,7 +117,7 @@ func setup10Remotes(benchS *db.FileSet) {
 }
 
 func BenchmarkUpdate100Changed10Remotes(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	setup10Remotes(benchS)
@@ -136,7 +135,7 @@ func BenchmarkUpdate100Changed10Remotes(b *testing.B) {
 }
 
 func BenchmarkUpdate100ChangedRemote(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -152,7 +151,7 @@ func BenchmarkUpdate100ChangedRemote(b *testing.B) {
 }
 
 func BenchmarkUpdate100ChangedRemote10Remotes(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -168,7 +167,7 @@ func BenchmarkUpdate100ChangedRemote10Remotes(b *testing.B) {
 }
 
 func BenchmarkUpdateOneUnchanged(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -180,7 +179,7 @@ func BenchmarkUpdateOneUnchanged(b *testing.B) {
 }
 
 func BenchmarkNeedHalf(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -201,9 +200,9 @@ func BenchmarkNeedHalf(b *testing.B) {
 }
 
 func BenchmarkNeedHalfRemote(b *testing.B) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(b)
 	defer ldb.Close()
-	fset := db.NewFileSet("test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	fset := newFileSet(b, "test)", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	replace(fset, remoteDevice0, firstHalf)
 	replace(fset, protocol.LocalDeviceID, files)
 
@@ -225,7 +224,7 @@ func BenchmarkNeedHalfRemote(b *testing.B) {
 }
 
 func BenchmarkHave(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -246,7 +245,7 @@ func BenchmarkHave(b *testing.B) {
 }
 
 func BenchmarkGlobal(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -267,7 +266,7 @@ func BenchmarkGlobal(b *testing.B) {
 }
 
 func BenchmarkNeedHalfTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -288,7 +287,7 @@ func BenchmarkNeedHalfTruncated(b *testing.B) {
 }
 
 func BenchmarkHaveTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -309,7 +308,7 @@ func BenchmarkHaveTruncated(b *testing.B) {
 }
 
 func BenchmarkGlobalTruncated(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	b.ResetTimer()
@@ -330,7 +329,7 @@ func BenchmarkGlobalTruncated(b *testing.B) {
 }
 
 func BenchmarkNeedCount(b *testing.B) {
-	ldb, benchS := getBenchFileSet()
+	ldb, benchS := getBenchFileSet(b)
 	defer ldb.Close()
 
 	benchS.Update(protocol.LocalDeviceID, changed100)

+ 5 - 7
lib/db/blockmap_test.go

@@ -10,7 +10,6 @@ import (
 	"encoding/binary"
 	"testing"
 
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
 
@@ -36,10 +35,9 @@ func init() {
 	}
 }
 
-func setup() (*Lowlevel, *BlockFinder) {
-	// Setup
-
-	db := NewLowlevel(backend.OpenMemory())
+func setup(t testing.TB) (*Lowlevel, *BlockFinder) {
+	t.Helper()
+	db := newLowlevelMemory(t)
 	return db, NewBlockFinder(db)
 }
 
@@ -105,7 +103,7 @@ func discardFromBlockMap(db *Lowlevel, folder []byte, fs []protocol.FileInfo) er
 }
 
 func TestBlockMapAddUpdateWipe(t *testing.T) {
-	db, f := setup()
+	db, f := setup(t)
 	defer db.Close()
 
 	if !dbEmpty(db) {
@@ -193,7 +191,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
 }
 
 func TestBlockFinderLookup(t *testing.T) {
-	db, f := setup()
+	db, f := setup(t)
 	defer db.Close()
 
 	folder1 := []byte("folder1")

+ 28 - 21
lib/db/db_test.go

@@ -13,6 +13,7 @@ import (
 	"testing"
 
 	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -35,17 +36,17 @@ func TestIgnoredFiles(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	db := NewLowlevel(ldb)
+	db := newLowlevel(t, ldb)
 	defer db.Close()
 	if err := UpdateSchema(db); err != nil {
 		t.Fatal(err)
 	}
 
-	fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
+	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
 
 	// The contents of the database are like this:
 	//
-	// 	fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
+	// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
 	// 	fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
 	// 		{ // invalid (ignored) file
 	// 			Name:    "foo",
@@ -164,7 +165,7 @@ func TestUpdate0to3(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	db := NewLowlevel(ldb)
+	db := newLowlevel(t, ldb)
 	defer db.Close()
 	updater := schemaUpdater{db}
 
@@ -293,7 +294,7 @@ func TestUpdate0to3(t *testing.T) {
 
 // TestRepairSequence checks that a few hand-crafted messed-up sequence entries get fixed.
 func TestRepairSequence(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	folderStr := "test"
@@ -397,7 +398,7 @@ func TestRepairSequence(t *testing.T) {
 	// Loading the metadata for the first time means a "re"calculation happens,
 	// along which the sequences get repaired too.
 	db.gcMut.RLock()
-	_ = db.loadMetadataTracker(folderStr)
+	_, err = db.loadMetadataTracker(folderStr)
 	db.gcMut.RUnlock()
 	if err != nil {
 		t.Fatal(err)
@@ -466,7 +467,7 @@ func TestRepairSequence(t *testing.T) {
 }
 
 func TestDowngrade(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 	// sets the min version etc
 	if err := UpdateSchema(db); err != nil {
@@ -491,10 +492,10 @@ func TestDowngrade(t *testing.T) {
 }
 
 func TestCheckGlobals(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
-	fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
+	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
 
 	// Add any file
 	name := "foo"
@@ -532,14 +533,17 @@ func TestUpdateTo10(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	db := NewLowlevel(ldb)
+	db := newLowlevel(t, ldb)
 	defer db.Close()
 
 	UpdateSchema(db)
 
 	folder := "test"
 
-	meta := db.getMetaAndCheck(folder)
+	meta, err := db.getMetaAndCheck(folder)
+	if err != nil {
+		t.Fatal(err)
+	}
 
 	empty := Counts{}
 
@@ -643,9 +647,9 @@ func TestDropDuplicates(t *testing.T) {
 func TestGCIndirect(t *testing.T) {
 	// Verify that the gcIndirect run actually removes block lists.
 
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
-	meta := newMetadataTracker(db.keyer)
+	meta := newMetadataTracker(db.keyer, events.NoopLogger)
 
 	// Add three files with different block lists
 
@@ -731,7 +735,7 @@ func TestGCIndirect(t *testing.T) {
 }
 
 func TestUpdateTo14(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	folderStr := "default"
@@ -741,7 +745,10 @@ func TestUpdateTo14(t *testing.T) {
 	file.BlocksHash = protocol.BlocksHash(file.Blocks)
 	fileWOBlocks := file
 	fileWOBlocks.Blocks = nil
-	meta := db.loadMetadataTracker(folderStr)
+	meta, err := db.loadMetadataTracker(folderStr)
+	if err != nil {
+		t.Fatal(err)
+	}
 
 	// Initally add the correct file the usual way, all good here.
 	if err := db.updateLocalFiles(folder, []protocol.FileInfo{file}, meta); err != nil {
@@ -800,7 +807,7 @@ func TestFlushRecursion(t *testing.T) {
 	// Verify that a commit hook can write to the transaction without
 	// causing another flush and thus recursion.
 
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	// A commit hook that writes a small piece of data to the transaction.
@@ -838,11 +845,11 @@ func TestFlushRecursion(t *testing.T) {
 }
 
 func TestCheckLocalNeed(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	folderStr := "test"
-	fs := NewFileSet(folderStr, fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
+	fs := newFileSet(t, folderStr, fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
 
 	// Add files such that we are in sync for a and b, and need c and d.
 	files := []protocol.FileInfo{
@@ -913,13 +920,13 @@ func TestCheckLocalNeed(t *testing.T) {
 }
 
 func TestDuplicateNeedCount(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	folder := "test"
 	testFs := fs.NewFilesystem(fs.FilesystemTypeFake, "")
 
-	fs := NewFileSet(folder, testFs, db)
+	fs := newFileSet(t, folder, testFs, db)
 	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID), Sequence: 1}}
 	fs.Update(protocol.LocalDeviceID, files)
 	files[0].Version = files[0].Version.Update(remoteDevice0.Short())
@@ -927,7 +934,7 @@ func TestDuplicateNeedCount(t *testing.T) {
 
 	db.checkRepair()
 
-	fs = NewFileSet(folder, testFs, db)
+	fs = newFileSet(t, folder, testFs, db)
 	found := false
 	for _, c := range fs.meta.counts.Counts {
 		if bytes.Equal(protocol.LocalDeviceID[:], c.DeviceID) && c.LocalFlags == needFlag {

+ 3 - 5
lib/db/keyer_test.go

@@ -9,8 +9,6 @@ package db
 import (
 	"bytes"
 	"testing"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
 )
 
 func TestDeviceKey(t *testing.T) {
@@ -18,7 +16,7 @@ func TestDeviceKey(t *testing.T) {
 	dev := []byte("device67890123456789012345678901")
 	name := []byte("name")
 
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	key, err := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
@@ -50,7 +48,7 @@ func TestGlobalKey(t *testing.T) {
 	fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
 	name := []byte("name")
 
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	key, err := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
@@ -67,7 +65,7 @@ func TestGlobalKey(t *testing.T) {
 func TestSequenceKey(t *testing.T) {
 	fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
 
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	defer db.Close()
 
 	const seq = 1234567890

+ 53 - 37
lib/db/lowlevel.go

@@ -10,6 +10,7 @@ import (
 	"bytes"
 	"context"
 	"encoding/binary"
+	"errors"
 	"fmt"
 	"io"
 	"os"
@@ -19,6 +20,7 @@ import (
 	"github.com/dchest/siphash"
 	"github.com/greatroar/blobloom"
 	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/rand"
@@ -66,9 +68,10 @@ type Lowlevel struct {
 	indirectGCInterval time.Duration
 	recheckInterval    time.Duration
 	oneFileSetCreated  chan struct{}
+	evLogger           events.Logger
 }
 
-func NewLowlevel(backend backend.Backend, opts ...Option) *Lowlevel {
+func NewLowlevel(backend backend.Backend, evLogger events.Logger, opts ...Option) (*Lowlevel, error) {
 	// Only log restarts in debug mode.
 	spec := util.SpecWithDebugLogger(l)
 	db := &Lowlevel{
@@ -80,6 +83,7 @@ func NewLowlevel(backend backend.Backend, opts ...Option) *Lowlevel {
 		indirectGCInterval: indirectGCDefaultInterval,
 		recheckInterval:    recheckDefaultInterval,
 		oneFileSetCreated:  make(chan struct{}),
+		evLogger:           evLogger,
 	}
 	for _, opt := range opts {
 		opt(db)
@@ -89,11 +93,14 @@ func NewLowlevel(backend backend.Backend, opts ...Option) *Lowlevel {
 	if path := db.needsRepairPath(); path != "" {
 		if _, err := os.Lstat(path); err == nil {
 			l.Infoln("Database was marked for repair - this may take a while")
-			db.checkRepair()
+			if err := db.checkRepair(); err != nil {
+				db.handleFailure(err)
+				return nil, err
+			}
 			os.Remove(path)
 		}
 	}
-	return db
+	return db, nil
 }
 
 type Option func(*Lowlevel)
@@ -822,29 +829,22 @@ func (b *bloomFilter) hash(id []byte) uint64 {
 }
 
 // checkRepair checks folder metadata and sequences for miscellaneous errors.
-func (db *Lowlevel) checkRepair() {
+func (db *Lowlevel) checkRepair() error {
 	for _, folder := range db.ListFolders() {
-		_ = db.getMetaAndCheck(folder)
+		if _, err := db.getMetaAndCheck(folder); err != nil {
+			return err
+		}
 	}
+	return nil
 }
 
-func (db *Lowlevel) getMetaAndCheck(folder string) *metadataTracker {
+func (db *Lowlevel) getMetaAndCheck(folder string) (*metadataTracker, error) {
 	db.gcMut.RLock()
 	defer db.gcMut.RUnlock()
 
-	var err error
-	defer func() {
-		if err != nil && !backend.IsClosed(err) {
-			l.Warnf("Fatal error: %v", err)
-			obfuscateAndPanic(err)
-		}
-	}()
-
-	var fixed int
-	fixed, err = db.checkLocalNeed([]byte(folder))
+	fixed, err := db.checkLocalNeed([]byte(folder))
 	if err != nil {
-		err = fmt.Errorf("checking local need: %w", err)
-		return nil
+		return nil, fmt.Errorf("checking local need: %w", err)
 	}
 	if fixed != 0 {
 		l.Infof("Repaired %d local need entries for folder %v in database", fixed, folder)
@@ -852,24 +852,22 @@ func (db *Lowlevel) getMetaAndCheck(folder string) *metadataTracker {
 
 	meta, err := db.recalcMeta(folder)
 	if err != nil {
-		err = fmt.Errorf("recalculating metadata: %w", err)
-		return nil
+		return nil, fmt.Errorf("recalculating metadata: %w", err)
 	}
 
 	fixed, err = db.repairSequenceGCLocked(folder, meta)
 	if err != nil {
-		err = fmt.Errorf("repairing sequences: %w", err)
-		return nil
+		return nil, fmt.Errorf("repairing sequences: %w", err)
 	}
 	if fixed != 0 {
 		l.Infof("Repaired %d sequence entries for folder %v in database", fixed, folder)
 	}
 
-	return meta
+	return meta, nil
 }
 
-func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
-	meta := newMetadataTracker(db.keyer)
+func (db *Lowlevel) loadMetadataTracker(folder string) (*metadataTracker, error) {
+	meta := newMetadataTracker(db.keyer, db.evLogger)
 	if err := meta.fromDB(db, []byte(folder)); err != nil {
 		if err == errMetaInconsistent {
 			l.Infof("Stored folder metadata for %q is inconsistent; recalculating", folder)
@@ -881,7 +879,9 @@ func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
 	}
 
 	curSeq := meta.Sequence(protocol.LocalDeviceID)
-	if metaOK := db.verifyLocalSequence(curSeq, folder); !metaOK {
+	if metaOK, err := db.verifyLocalSequence(curSeq, folder); err != nil {
+		return nil, fmt.Errorf("verifying sequences: %w", err)
+	} else if !metaOK {
 		l.Infof("Stored folder metadata for %q is out of date after crash; recalculating", folder)
 		return db.getMetaAndCheck(folder)
 	}
@@ -891,13 +891,13 @@ func (db *Lowlevel) loadMetadataTracker(folder string) *metadataTracker {
 		return db.getMetaAndCheck(folder)
 	}
 
-	return meta
+	return meta, nil
 }
 
 func (db *Lowlevel) recalcMeta(folderStr string) (*metadataTracker, error) {
 	folder := []byte(folderStr)
 
-	meta := newMetadataTracker(db.keyer)
+	meta := newMetadataTracker(db.keyer, db.evLogger)
 	if err := db.checkGlobals(folder); err != nil {
 		return nil, fmt.Errorf("checking globals: %w", err)
 	}
@@ -951,7 +951,7 @@ func (db *Lowlevel) recalcMeta(folderStr string) (*metadataTracker, error) {
 
 // Verify the local sequence number from actual sequence entries. Returns
 // true if it was all good, or false if a fixup was necessary.
-func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) bool {
+func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) (bool, error) {
 	// Walk the sequence index from the current (supposedly) highest
 	// sequence number and raise the alarm if we get anything. This recovers
 	// from the occasion where we have written sequence entries to disk but
@@ -964,20 +964,18 @@ func (db *Lowlevel) verifyLocalSequence(curSeq int64, folder string) bool {
 
 	t, err := db.newReadOnlyTransaction()
 	if err != nil {
-		l.Warnf("Fatal error: %v", err)
-		obfuscateAndPanic(err)
+		return false, err
 	}
 	ok := true
 	if err := t.withHaveSequence([]byte(folder), curSeq+1, func(fi protocol.FileIntf) bool {
 		ok = false // we got something, which we should not have
 		return false
-	}); err != nil && !backend.IsClosed(err) {
-		l.Warnf("Fatal error: %v", err)
-		obfuscateAndPanic(err)
+	}); err != nil {
+		return false, err
 	}
 	t.close()
 
-	return ok
+	return ok, nil
 }
 
 // repairSequenceGCLocked makes sure the sequence numbers in the sequence keys
@@ -1177,6 +1175,17 @@ func (db *Lowlevel) needsRepairPath() string {
 	return path + needsRepairSuffix
 }
 
+func (db *Lowlevel) checkErrorForRepair(err error) {
+	if errors.Is(err, errEntryFromGlobalMissing) || errors.Is(err, errEmptyGlobal) {
+		// Inconsistency error, mark db for repair on next start.
+		if path := db.needsRepairPath(); path != "" {
+			if fd, err := os.Create(path); err == nil {
+				fd.Close()
+			}
+		}
+	}
+}
+
 // unchanged checks if two files are the same and thus don't need to be updated.
 // Local flags or the invalid bit might change without the version
 // being bumped.
@@ -1184,8 +1193,15 @@ func unchanged(nf, ef protocol.FileIntf) bool {
 	return ef.FileVersion().Equal(nf.FileVersion()) && ef.IsInvalid() == nf.IsInvalid() && ef.FileLocalFlags() == nf.FileLocalFlags()
 }
 
+func (db *Lowlevel) handleFailure(err error) {
+	db.checkErrorForRepair(err)
+	if shouldReportFailure(err) {
+		db.evLogger.Log(events.Failure, err)
+	}
+}
+
 var ldbPathRe = regexp.MustCompile(`(open|write|read) .+[\\/].+[\\/]index[^\\/]+[\\/][^\\/]+: `)
 
-func obfuscateAndPanic(err error) {
-	panic(ldbPathRe.ReplaceAllString(err.Error(), "$1 x: "))
+func shouldReportFailure(err error) bool {
+	return !ldbPathRe.MatchString(err.Error())
 }

+ 11 - 3
lib/db/meta.go

@@ -9,10 +9,12 @@ package db
 import (
 	"bytes"
 	"errors"
+	"fmt"
 	"math/bits"
 	"time"
 
 	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/sync"
 )
@@ -28,8 +30,9 @@ type countsMap struct {
 type metadataTracker struct {
 	keyer keyer
 	countsMap
-	mut   sync.RWMutex
-	dirty bool
+	mut      sync.RWMutex
+	dirty    bool
+	evLogger events.Logger
 }
 
 type metaKey struct {
@@ -39,13 +42,14 @@ type metaKey struct {
 
 const needFlag uint32 = 1 << 31 // Last bit, as early ones are local flags
 
-func newMetadataTracker(keyer keyer) *metadataTracker {
+func newMetadataTracker(keyer keyer, evLogger events.Logger) *metadataTracker {
 	return &metadataTracker{
 		keyer: keyer,
 		mut:   sync.NewRWMutex(),
 		countsMap: countsMap{
 			indexes: make(map[metaKey]int),
 		},
+		evLogger: evLogger,
 	}
 }
 
@@ -296,18 +300,22 @@ func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flag uint32, f
 	// the created timestamp to zero. Next time we start up the metadata
 	// will be seen as infinitely old and recalculated from scratch.
 	if cp.Deleted < 0 {
+		m.evLogger.Log(events.Failure, fmt.Sprintf("meta deleted count for flag 0x%x dropped below zero", flag))
 		cp.Deleted = 0
 		m.counts.Created = 0
 	}
 	if cp.Files < 0 {
+		m.evLogger.Log(events.Failure, fmt.Sprintf("meta files count for flag 0x%x dropped below zero", flag))
 		cp.Files = 0
 		m.counts.Created = 0
 	}
 	if cp.Directories < 0 {
+		m.evLogger.Log(events.Failure, fmt.Sprintf("meta directories count for flag 0x%x dropped below zero", flag))
 		cp.Directories = 0
 		m.counts.Created = 0
 	}
 	if cp.Symlinks < 0 {
+		m.evLogger.Log(events.Failure, fmt.Sprintf("meta deleted count for flag 0x%x dropped below zero", flag))
 		cp.Symlinks = 0
 		m.counts.Created = 0
 	}

+ 6 - 6
lib/db/meta_test.go

@@ -11,7 +11,7 @@ import (
 	"sort"
 	"testing"
 
-	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -52,7 +52,7 @@ func TestEachFlagBit(t *testing.T) {
 func TestMetaDevices(t *testing.T) {
 	d1 := protocol.DeviceID{1}
 	d2 := protocol.DeviceID{2}
-	meta := newMetadataTracker(nil)
+	meta := newMetadataTracker(nil, events.NoopLogger)
 
 	meta.addFile(d1, protocol.FileInfo{Sequence: 1})
 	meta.addFile(d1, protocol.FileInfo{Sequence: 2, LocalFlags: 1})
@@ -85,7 +85,7 @@ func TestMetaDevices(t *testing.T) {
 
 func TestMetaSequences(t *testing.T) {
 	d1 := protocol.DeviceID{1}
-	meta := newMetadataTracker(nil)
+	meta := newMetadataTracker(nil, events.NoopLogger)
 
 	meta.addFile(d1, protocol.FileInfo{Sequence: 1})
 	meta.addFile(d1, protocol.FileInfo{Sequence: 2, RawInvalid: true})
@@ -105,11 +105,11 @@ func TestMetaSequences(t *testing.T) {
 }
 
 func TestRecalcMeta(t *testing.T) {
-	ldb := NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	// Add some files
-	s1 := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
+	s1 := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
 	files := []protocol.FileInfo{
 		{Name: "a", Size: 1000},
 		{Name: "b", Size: 2000},
@@ -161,7 +161,7 @@ func TestRecalcMeta(t *testing.T) {
 	}
 
 	// Create a new fileset, which will realize the inconsistency and recalculate
-	s2 := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
+	s2 := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, "fake"), ldb)
 
 	// Verify local/global size
 	snap = s2.Snapshot()

+ 4 - 6
lib/db/namespaced_test.go

@@ -9,12 +9,10 @@ package db
 import (
 	"testing"
 	"time"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
 )
 
 func TestNamespacedInt(t *testing.T) {
-	ldb := NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	n1 := NewNamespacedKV(ldb, "foo")
@@ -62,7 +60,7 @@ func TestNamespacedInt(t *testing.T) {
 }
 
 func TestNamespacedTime(t *testing.T) {
-	ldb := NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	n1 := NewNamespacedKV(ldb, "foo")
@@ -86,7 +84,7 @@ func TestNamespacedTime(t *testing.T) {
 }
 
 func TestNamespacedString(t *testing.T) {
-	ldb := NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	n1 := NewNamespacedKV(ldb, "foo")
@@ -109,7 +107,7 @@ func TestNamespacedString(t *testing.T) {
 }
 
 func TestNamespacedReset(t *testing.T) {
-	ldb := NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	n1 := NewNamespacedKV(ldb, "foo")

+ 1 - 1
lib/db/schemaupdater.go

@@ -719,7 +719,7 @@ func (db *schemaUpdater) updateSchemaTo14(_ int) error {
 	var key, gk []byte
 	for _, folderStr := range db.ListFolders() {
 		folder := []byte(folderStr)
-		meta := newMetadataTracker(db.keyer)
+		meta := newMetadataTracker(db.keyer, db.evLogger)
 		meta.counts.Created = 0 // Recalculate metadata afterwards
 
 		t, err := db.newReadWriteTransaction(meta.CommitHook(folder))

+ 10 - 14
lib/db/set.go

@@ -13,9 +13,7 @@
 package db
 
 import (
-	"errors"
 	"fmt"
-	"os"
 
 	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/fs"
@@ -38,17 +36,22 @@ type FileSet struct {
 // continue iteration, false to stop.
 type Iterator func(f protocol.FileIntf) bool
 
-func NewFileSet(folder string, fs fs.Filesystem, db *Lowlevel) *FileSet {
+func NewFileSet(folder string, fs fs.Filesystem, db *Lowlevel) (*FileSet, error) {
 	select {
 	case <-db.oneFileSetCreated:
 	default:
 		close(db.oneFileSetCreated)
 	}
+	meta, err := db.loadMetadataTracker(folder)
+	if err != nil {
+		db.handleFailure(err)
+		return nil, err
+	}
 	s := &FileSet{
 		folder:      folder,
 		fs:          fs,
 		db:          db,
-		meta:        db.loadMetadataTracker(folder),
+		meta:        meta,
 		updateMutex: sync.NewMutex(),
 	}
 	if id := s.IndexID(protocol.LocalDeviceID); id == 0 {
@@ -59,7 +62,7 @@ func NewFileSet(folder string, fs fs.Filesystem, db *Lowlevel) *FileSet {
 			fatalError(err, fmt.Sprintf("%s Creating new IndexID", s.folder), s.db)
 		}
 	}
-	return s
+	return s, nil
 }
 
 func (s *FileSet) Drop(device protocol.DeviceID) {
@@ -500,14 +503,7 @@ func nativeFileIterator(fn Iterator) Iterator {
 }
 
 func fatalError(err error, opStr string, db *Lowlevel) {
-	if errors.Is(err, errEntryFromGlobalMissing) || errors.Is(err, errEmptyGlobal) {
-		// Inconsistency error, mark db for repair on next start.
-		if path := db.needsRepairPath(); path != "" {
-			if fd, err := os.Create(path); err == nil {
-				fd.Close()
-			}
-		}
-	}
+	db.checkErrorForRepair(err)
 	l.Warnf("Fatal error: %v: %v", opStr, err)
-	obfuscateAndPanic(err)
+	panic(ldbPathRe.ReplaceAllString(err.Error(), "$1 x: "))
 }

+ 92 - 69
lib/db/set_test.go

@@ -18,6 +18,7 @@ import (
 	"github.com/d4l3k/messagediff"
 	"github.com/syncthing/syncthing/lib/db"
 	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -142,10 +143,10 @@ func setBlocksHash(files fileList) {
 }
 
 func TestGlobalSet(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local0 := fileList{
 		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
@@ -448,10 +449,10 @@ func TestGlobalSet(t *testing.T) {
 }
 
 func TestNeedWithInvalid(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	localHave := fileList{
 		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
@@ -488,11 +489,11 @@ func TestNeedWithInvalid(t *testing.T) {
 }
 
 func TestUpdateToInvalid(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	f := db.NewBlockFinder(ldb)
 
 	localHave := fileList{
@@ -545,10 +546,10 @@ func TestUpdateToInvalid(t *testing.T) {
 }
 
 func TestInvalidAvailability(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	remote0Have := fileList{
 		protocol.FileInfo{Name: "both", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
@@ -587,10 +588,10 @@ func TestInvalidAvailability(t *testing.T) {
 }
 
 func TestGlobalReset(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local := []protocol.FileInfo{
 		{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -626,10 +627,10 @@ func TestGlobalReset(t *testing.T) {
 }
 
 func TestNeed(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local := []protocol.FileInfo{
 		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -667,10 +668,10 @@ func TestNeed(t *testing.T) {
 }
 
 func TestSequence(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local1 := []protocol.FileInfo{
 		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -698,10 +699,10 @@ func TestSequence(t *testing.T) {
 }
 
 func TestListDropFolder(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s0 := db.NewFileSet("test0", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s0 := newFileSet(t, "test0", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	local1 := []protocol.FileInfo{
 		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
 		{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -709,7 +710,7 @@ func TestListDropFolder(t *testing.T) {
 	}
 	replace(s0, protocol.LocalDeviceID, local1)
 
-	s1 := db.NewFileSet("test1", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s1 := newFileSet(t, "test1", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	local2 := []protocol.FileInfo{
 		{Name: "d", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
 		{Name: "e", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}},
@@ -749,10 +750,10 @@ func TestListDropFolder(t *testing.T) {
 }
 
 func TestGlobalNeedWithInvalid(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test1", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test1", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	rem0 := fileList{
 		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1002}}}, Blocks: genBlocks(4)},
@@ -792,10 +793,10 @@ func TestGlobalNeedWithInvalid(t *testing.T) {
 }
 
 func TestLongPath(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	var b bytes.Buffer
 	for i := 0; i < 100; i++ {
@@ -833,13 +834,13 @@ func BenchmarkUpdateOneFile(b *testing.B) {
 	if err != nil {
 		b.Fatal(err)
 	}
-	ldb := db.NewLowlevel(be)
+	ldb := newLowlevel(b, be)
 	defer func() {
 		ldb.Close()
 		os.RemoveAll("testdata/benchmarkupdate.db")
 	}()
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(b, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 	replace(m, protocol.LocalDeviceID, local0)
 	l := local0[4:5]
 
@@ -852,10 +853,10 @@ func BenchmarkUpdateOneFile(b *testing.B) {
 }
 
 func TestIndexID(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	// The Index ID for some random device is zero by default.
 	id := s.IndexID(remoteDevice0)
@@ -885,9 +886,9 @@ func TestIndexID(t *testing.T) {
 }
 
 func TestDropFiles(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 
-	m := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	m := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local0 := fileList{
 		protocol.FileInfo{Name: "a", Sequence: 1, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Blocks: genBlocks(1)},
@@ -948,10 +949,10 @@ func TestDropFiles(t *testing.T) {
 }
 
 func TestIssue4701(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	localHave := fileList{
 		protocol.FileInfo{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -990,11 +991,11 @@ func TestIssue4701(t *testing.T) {
 }
 
 func TestWithHaveSequence(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	// The files must not be in alphabetical order
 	localHave := fileList{
@@ -1028,11 +1029,11 @@ func TestStressWithHaveSequence(t *testing.T) {
 		t.Skip("Takes a long time")
 	}
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	var localHave []protocol.FileInfo
 	for i := 0; i < 100; i++ {
@@ -1073,11 +1074,11 @@ loop:
 }
 
 func TestIssue4925(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	localHave := fileList{
 		protocol.FileInfo{Name: "dir"},
@@ -1100,12 +1101,12 @@ func TestIssue4925(t *testing.T) {
 }
 
 func TestMoveGlobalBack(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
 	file := "foo"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	localHave := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}, Blocks: genBlocks(1), ModifiedS: 10, Size: 1}}
 	remote0Have := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}, {ID: remoteDevice0.Short(), Value: 1}}}, Blocks: genBlocks(2), ModifiedS: 0, Size: 2}}
@@ -1169,12 +1170,12 @@ func TestMoveGlobalBack(t *testing.T) {
 // needed files.
 // https://github.com/syncthing/syncthing/issues/5007
 func TestIssue5007(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
 	file := "foo"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}}
 
@@ -1199,12 +1200,12 @@ func TestIssue5007(t *testing.T) {
 // TestNeedDeleted checks that a file that doesn't exist locally isn't needed
 // when the global file is deleted.
 func TestNeedDeleted(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
 	file := "foo"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}, Deleted: true}}
 
@@ -1237,11 +1238,11 @@ func TestNeedDeleted(t *testing.T) {
 }
 
 func TestReceiveOnlyAccounting(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local := protocol.DeviceID{1}
 	remote := protocol.DeviceID{2}
@@ -1342,12 +1343,12 @@ func TestReceiveOnlyAccounting(t *testing.T) {
 }
 
 func TestNeedAfterUnignore(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	folder := "test"
 	file := "foo"
-	s := db.NewFileSet(folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	remID := remoteDevice0.Short()
 
@@ -1376,9 +1377,9 @@ func TestNeedAfterUnignore(t *testing.T) {
 func TestRemoteInvalidNotAccounted(t *testing.T) {
 	// Remote files with the invalid bit should not count.
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	files := []protocol.FileInfo{
 		{Name: "a", Size: 1234, Sequence: 42, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1003}}}},                   // valid, should count
@@ -1396,10 +1397,10 @@ func TestRemoteInvalidNotAccounted(t *testing.T) {
 }
 
 func TestNeedWithNewerInvalid(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("default", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "default", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	rem0ID := remoteDevice0.Short()
 	rem1ID := remoteDevice1.Short()
@@ -1437,11 +1438,11 @@ func TestNeedWithNewerInvalid(t *testing.T) {
 }
 
 func TestNeedAfterDeviceRemove(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	file := "foo"
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	fs := fileList{{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1}}}}}
 
@@ -1466,9 +1467,9 @@ func TestNeedAfterDeviceRemove(t *testing.T) {
 func TestCaseSensitive(t *testing.T) {
 	// Normal case sensitive lookup should work
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local := []protocol.FileInfo{
 		{Name: filepath.FromSlash("D1/f1"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -1504,9 +1505,9 @@ func TestSequenceIndex(t *testing.T) {
 
 	// Set up a db and a few files that we will manipulate.
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	local := []protocol.FileInfo{
 		{Name: filepath.FromSlash("banana"), Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}},
@@ -1598,11 +1599,11 @@ func TestSequenceIndex(t *testing.T) {
 }
 
 func TestIgnoreAfterReceiveOnly(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	file := "foo"
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
 
 	fs := fileList{{
 		Name:       file,
@@ -1629,11 +1630,11 @@ func TestIgnoreAfterReceiveOnly(t *testing.T) {
 
 // https://github.com/syncthing/syncthing/issues/6650
 func TestUpdateWithOneFileTwice(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
 	file := "foo"
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 
 	fs := fileList{{
 		Name:     file,
@@ -1667,10 +1668,10 @@ func TestUpdateWithOneFileTwice(t *testing.T) {
 
 // https://github.com/syncthing/syncthing/issues/6668
 func TestNeedRemoteOnly(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 
 	remote0Have := fileList{
 		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
@@ -1685,10 +1686,10 @@ func TestNeedRemoteOnly(t *testing.T) {
 
 // https://github.com/syncthing/syncthing/issues/6784
 func TestNeedRemoteAfterReset(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 
 	files := fileList{
 		protocol.FileInfo{Name: "b", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1001}}}, Blocks: genBlocks(2)},
@@ -1711,10 +1712,10 @@ func TestNeedRemoteAfterReset(t *testing.T) {
 
 // https://github.com/syncthing/syncthing/issues/6850
 func TestIgnoreLocalChanged(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 
 	// Add locally changed file
 	files := fileList{
@@ -1745,10 +1746,10 @@ func TestIgnoreLocalChanged(t *testing.T) {
 // an Index (as opposed to an IndexUpdate), and we don't want to loose the index
 // ID when that happens.
 func TestNoIndexIDResetOnDrop(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
+	ldb := newLowlevelMemory(t)
 	defer ldb.Close()
 
-	s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+	s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 
 	s.SetIndexID(remoteDevice0, 1)
 	s.Drop(remoteDevice0)
@@ -1770,8 +1771,8 @@ func TestConcurrentIndexID(t *testing.T) {
 		max = 10
 	}
 	for i := 0; i < max; i++ {
-		ldb := db.NewLowlevel(backend.OpenMemory())
-		s := db.NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
+		ldb := newLowlevelMemory(t)
+		s := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), ldb)
 		go setID(s, 0)
 		go setID(s, 1)
 		<-done
@@ -1837,3 +1838,25 @@ func checkNeed(t testing.TB, s *db.FileSet, dev protocol.DeviceID, expected []pr
 		t.Errorf("Count incorrect (%v): expected %v, got %v", dev, exp, counts)
 	}
 }
+
+func newLowlevel(t testing.TB, backend backend.Backend) *db.Lowlevel {
+	t.Helper()
+	ll, err := db.NewLowlevel(backend, events.NoopLogger)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return ll
+}
+
+func newLowlevelMemory(t testing.TB) *db.Lowlevel {
+	return newLowlevel(t, backend.OpenMemory())
+}
+
+func newFileSet(t testing.TB, folder string, fs fs.Filesystem, ll *db.Lowlevel) *db.FileSet {
+	t.Helper()
+	fset, err := db.NewFileSet(folder, fs, ll)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return fset
+}

+ 1 - 3
lib/db/smallindex_test.go

@@ -8,12 +8,10 @@ package db
 
 import (
 	"testing"
-
-	"github.com/syncthing/syncthing/lib/db/backend"
 )
 
 func TestSmallIndex(t *testing.T) {
-	db := NewLowlevel(backend.OpenMemory())
+	db := newLowlevelMemory(t)
 	idx := newSmallIndex(db, []byte{12, 34})
 
 	// ID zero should be unallocated

+ 5 - 1
lib/db/transactions.go

@@ -12,6 +12,7 @@ import (
 	"fmt"
 
 	"github.com/syncthing/syncthing/lib/db/backend"
+	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
 )
@@ -25,7 +26,8 @@ var (
 // A readOnlyTransaction represents a database snapshot.
 type readOnlyTransaction struct {
 	backend.ReadTransaction
-	keyer keyer
+	keyer    keyer
+	evLogger events.Logger
 }
 
 func (db *Lowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
@@ -36,6 +38,7 @@ func (db *Lowlevel) newReadOnlyTransaction() (readOnlyTransaction, error) {
 	return readOnlyTransaction{
 		ReadTransaction: tran,
 		keyer:           db.keyer,
+		evLogger:        db.evLogger,
 	}, nil
 }
 
@@ -800,6 +803,7 @@ func (t readWriteTransaction) removeFromGlobal(gk, keyBuf, folder, device, file
 
 	if !haveOldGlobal {
 		// Shouldn't ever happen, but doesn't hurt to handle.
+		t.evLogger.Log(events.Failure, "encountered empty global while removing item")
 		return keyBuf, t.Delete(gk)
 	}
 

+ 29 - 6
lib/db/util_test.go

@@ -10,10 +10,11 @@ import (
 	"encoding/json"
 	"io"
 	"os"
-	// "testing"
+	"testing"
 
 	"github.com/syncthing/syncthing/lib/db/backend"
-	// "github.com/syncthing/syncthing/lib/fs"
+	"github.com/syncthing/syncthing/lib/events"
+	"github.com/syncthing/syncthing/lib/fs"
 	// "github.com/syncthing/syncthing/lib/protocol"
 )
 
@@ -69,6 +70,28 @@ func openJSONS(file string) (backend.Backend, error) {
 	return db, nil
 }
 
+func newLowlevel(t testing.TB, backend backend.Backend) *Lowlevel {
+	t.Helper()
+	ll, err := NewLowlevel(backend, events.NoopLogger)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return ll
+}
+
+func newLowlevelMemory(t testing.TB) *Lowlevel {
+	return newLowlevel(t, backend.OpenMemory())
+}
+
+func newFileSet(t testing.TB, folder string, fs fs.Filesystem, db *Lowlevel) *FileSet {
+	t.Helper()
+	fset, err := NewFileSet(folder, fs, db)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return fset
+}
+
 // The following commented tests were used to generate jsons files to stdout for
 // future tests and are kept here for reference (reuse).
 
@@ -76,7 +99,7 @@ func openJSONS(file string) (backend.Backend, error) {
 // local and remote, in the format used in 0.14.48.
 // func TestGenerateIgnoredFilesDB(t *testing.T) {
 // 	db := OpenMemory()
-// 	fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
+// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
 // 	fs.Update(protocol.LocalDeviceID, []protocol.FileInfo{
 // 		{ // invalid (ignored) file
 // 			Name:    "foo",
@@ -111,7 +134,7 @@ func openJSONS(file string) (backend.Backend, error) {
 // format used in 0.14.45.
 // func TestGenerateUpdate0to3DB(t *testing.T) {
 // 	db := OpenMemory()
-// 	fs := NewFileSet(update0to3Folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
+// 	fs := newFileSet(t, update0to3Folder, fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
 // 	for devID, files := range haveUpdate0to3 {
 // 		fs.Update(devID, files)
 // 	}
@@ -119,14 +142,14 @@ func openJSONS(file string) (backend.Backend, error) {
 // }
 
 // func TestGenerateUpdateTo10(t *testing.T) {
-// 	db := NewLowlevel(backend.OpenMemory())
+// 	db := newLowlevelMemory(t)
 // 	defer db.Close()
 
 // 	if err := UpdateSchema(db); err != nil {
 // 		t.Fatal(err)
 // 	}
 
-// 	fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
+// 	fs := newFileSet(t, "test", fs.NewFilesystem(fs.FilesystemTypeFake, ""), db)
 
 // 	files := []protocol.FileInfo{
 // 		{Name: "a", Version: protocol.Vector{Counters: []protocol.Counter{{ID: myID, Value: 1000}}}, Deleted: true, Sequence: 1},

+ 2 - 0
lib/locations/locations.go

@@ -34,6 +34,7 @@ const (
 	AuditLog      LocationEnum = "auditLog"
 	GUIAssets     LocationEnum = "GUIAssets"
 	DefFolder     LocationEnum = "defFolder"
+	FailuresFile  LocationEnum = "FailuresFile"
 )
 
 type BaseDirEnum string
@@ -97,6 +98,7 @@ var locationTemplates = map[LocationEnum]string{
 	AuditLog:      "${data}/audit-${timestamp}.log",
 	GUIAssets:     "${config}/gui",
 	DefFolder:     "${userHome}/Sync",
+	FailuresFile:  "${data}/failures-unreported.txt",
 }
 
 var locations = make(map[LocationEnum]string)

+ 1 - 3
lib/model/folder_recvonly_test.go

@@ -14,8 +14,6 @@ import (
 	"time"
 
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/scanner"
@@ -457,7 +455,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder) {
 	cfg.Folders = []config.FolderConfiguration{fcfg}
 	w.Replace(cfg)
 
-	m := newModel(w, myID, "syncthing", "dev", db.NewLowlevel(backend.OpenMemory()), nil)
+	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	<-m.started
 	must(t, m.ScanFolder("ro"))

+ 23 - 23
lib/model/folder_sendrecv_test.go

@@ -91,10 +91,10 @@ func createFile(t *testing.T, name string, fs fs.Filesystem) protocol.FileInfo {
 }
 
 // Sets up a folder and model, but makes sure the services aren't actually running.
-func setupSendReceiveFolder(files ...protocol.FileInfo) (*testModel, *sendReceiveFolder) {
+func setupSendReceiveFolder(t testing.TB, files ...protocol.FileInfo) (*testModel, *sendReceiveFolder) {
 	w, fcfg := tmpDefaultWrapper()
 	// Initialise model and stop immediately.
-	model := setupModel(w)
+	model := setupModel(t, w)
 	model.cancel()
 	<-model.stopped
 	f := model.folderRunners[fcfg.ID].(*sendReceiveFolder)
@@ -129,7 +129,7 @@ func TestHandleFile(t *testing.T) {
 	requiredFile := existingFile
 	requiredFile.Blocks = blocks[1:]
 
-	m, f := setupSendReceiveFolder(existingFile)
+	m, f := setupSendReceiveFolder(t, existingFile)
 	defer cleanupSRFolder(f, m)
 
 	copyChan := make(chan copyBlocksState, 1)
@@ -171,7 +171,7 @@ func TestHandleFileWithTemp(t *testing.T) {
 	requiredFile := existingFile
 	requiredFile.Blocks = blocks[1:]
 
-	m, f := setupSendReceiveFolder(existingFile)
+	m, f := setupSendReceiveFolder(t, existingFile)
 	defer cleanupSRFolder(f, m)
 
 	if _, err := prepareTmpFile(f.Filesystem()); err != nil {
@@ -227,7 +227,7 @@ func TestCopierFinder(t *testing.T) {
 			requiredFile.Blocks = blocks[1:]
 			requiredFile.Name = "file2"
 
-			m, f := setupSendReceiveFolder(existingFile)
+			m, f := setupSendReceiveFolder(t, existingFile)
 			f.CopyRangeMethod = method
 
 			defer cleanupSRFolder(f, m)
@@ -308,7 +308,7 @@ func TestCopierFinder(t *testing.T) {
 
 func TestWeakHash(t *testing.T) {
 	// Setup the model/pull environment
-	model, fo := setupSendReceiveFolder()
+	model, fo := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(fo, model)
 	ffs := fo.Filesystem()
 
@@ -437,7 +437,7 @@ func TestCopierCleanup(t *testing.T) {
 	// Create a file
 	file := setupFile("test", []int{0})
 	file.Size = 1
-	m, f := setupSendReceiveFolder(file)
+	m, f := setupSendReceiveFolder(t, file)
 	defer cleanupSRFolder(f, m)
 
 	file.Blocks = []protocol.BlockInfo{blocks[1]}
@@ -470,7 +470,7 @@ func TestCopierCleanup(t *testing.T) {
 func TestDeregisterOnFailInCopy(t *testing.T) {
 	file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
 
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	// Set up our evet subscription early
@@ -570,7 +570,7 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
 func TestDeregisterOnFailInPull(t *testing.T) {
 	file := setupFile("filex", []int{0, 2, 0, 0, 5, 0, 0, 8})
 
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	// Set up our evet subscription early
@@ -673,7 +673,7 @@ func TestDeregisterOnFailInPull(t *testing.T) {
 }
 
 func TestIssue3164(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 	tmpDir := ffs.URI()
@@ -764,7 +764,7 @@ func TestDiffEmpty(t *testing.T) {
 // option is true and the permissions do not match between the file on disk and
 // in the db.
 func TestDeleteIgnorePerms(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 	f.IgnorePerms = true
@@ -802,7 +802,7 @@ func TestCopyOwner(t *testing.T) {
 	// Set up a folder with the CopyParentOwner bit and backed by a fake
 	// filesystem.
 
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	f.folder.FolderConfiguration = config.NewFolderConfiguration(m.id, f.ID, f.Label, fs.FilesystemTypeFake, "/TestCopyOwner")
 	f.folder.FolderConfiguration.CopyOwnershipFromParent = true
@@ -904,7 +904,7 @@ func TestCopyOwner(t *testing.T) {
 // TestSRConflictReplaceFileByDir checks that a conflict is created when an existing file
 // is replaced with a directory and versions are conflicting
 func TestSRConflictReplaceFileByDir(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -936,7 +936,7 @@ func TestSRConflictReplaceFileByDir(t *testing.T) {
 // TestSRConflictReplaceFileByLink checks that a conflict is created when an existing file
 // is replaced with a link and versions are conflicting
 func TestSRConflictReplaceFileByLink(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -969,7 +969,7 @@ func TestSRConflictReplaceFileByLink(t *testing.T) {
 // TestDeleteBehindSymlink checks that we don't delete or schedule a scan
 // when trying to delete a file behind a symlink.
 func TestDeleteBehindSymlink(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -1020,7 +1020,7 @@ func TestDeleteBehindSymlink(t *testing.T) {
 
 // Reproduces https://github.com/syncthing/syncthing/issues/6559
 func TestPullCtxCancel(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	pullChan := make(chan pullBlockState)
@@ -1062,7 +1062,7 @@ func TestPullCtxCancel(t *testing.T) {
 }
 
 func TestPullDeleteUnscannedDir(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -1091,7 +1091,7 @@ func TestPullDeleteUnscannedDir(t *testing.T) {
 }
 
 func TestPullCaseOnlyPerformFinish(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -1152,7 +1152,7 @@ func TestPullCaseOnlySymlink(t *testing.T) {
 }
 
 func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 	ffs := f.Filesystem()
 
@@ -1207,7 +1207,7 @@ func testPullCaseOnlyDirOrSymlink(t *testing.T, dir bool) {
 }
 
 func TestPullTempFileCaseConflict(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	copyChan := make(chan copyBlocksState, 1)
@@ -1233,7 +1233,7 @@ func TestPullTempFileCaseConflict(t *testing.T) {
 }
 
 func TestPullCaseOnlyRename(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	// tempNameConfl := fs.TempName(confl)
@@ -1276,7 +1276,7 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
 		t.Skip()
 	}
 
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	name := "foo"
@@ -1316,7 +1316,7 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
 }
 
 func TestPullDeleteCaseConflict(t *testing.T) {
-	m, f := setupSendReceiveFolder()
+	m, f := setupSendReceiveFolder(t)
 	defer cleanupSRFolder(f, m)
 
 	name := "foo"

+ 59 - 14
lib/model/model.go

@@ -134,6 +134,7 @@ type model struct {
 	// folderIOLimiter limits the number of concurrent I/O heavy operations,
 	// such as scans and pulls.
 	folderIOLimiter *byteSemaphore
+	fatalChan       chan error
 
 	// fields protected by fmut
 	fmut                           sync.RWMutex
@@ -217,6 +218,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 		shortID:              id.Short(),
 		globalRequestLimiter: newByteSemaphore(1024 * cfg.Options().MaxConcurrentIncomingRequestKiB()),
 		folderIOLimiter:      newByteSemaphore(cfg.Options().MaxFolderConcurrency()),
+		fatalChan:            make(chan error),
 
 		// fields protected by fmut
 		fmut:                           sync.NewRWMutex(),
@@ -253,7 +255,27 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
 }
 
 func (m *model) serve(ctx context.Context) error {
-	// Add and start folders
+	defer m.closeAllConnectionsAndWait()
+
+	m.cfg.Subscribe(m)
+	defer m.cfg.Unsubscribe(m)
+
+	if err := m.initFolders(); err != nil {
+		close(m.started)
+		return util.AsFatalErr(err, util.ExitError)
+	}
+
+	close(m.started)
+
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case err := <-m.fatalChan:
+		return util.AsFatalErr(err, util.ExitError)
+	}
+}
+
+func (m *model) initFolders() error {
 	cacheIgnoredFiles := m.cfg.Options().CacheIgnoredFiles
 	existingDevices := m.cfg.Devices()
 	existingFolders := m.cfg.Folders()
@@ -263,7 +285,10 @@ func (m *model) serve(ctx context.Context) error {
 			folderCfg.CreateRoot()
 			continue
 		}
-		m.newFolder(folderCfg, cacheIgnoredFiles)
+		err := m.newFolder(folderCfg, cacheIgnoredFiles)
+		if err != nil {
+			return err
+		}
 		clusterConfigDevices.add(folderCfg.DeviceIDs())
 	}
 
@@ -271,12 +296,10 @@ func (m *model) serve(ctx context.Context) error {
 	m.cleanPending(existingDevices, existingFolders, ignoredDevices, nil)
 
 	m.resendClusterConfig(clusterConfigDevices.AsSlice())
-	m.cfg.Subscribe(m)
-
-	close(m.started)
-	<-ctx.Done()
+	return nil
+}
 
-	m.cfg.Unsubscribe(m)
+func (m *model) closeAllConnectionsAndWait() {
 	m.pmut.RLock()
 	closed := make([]chan struct{}, 0, len(m.conn))
 	for id, conn := range m.conn {
@@ -287,7 +310,13 @@ func (m *model) serve(ctx context.Context) error {
 	for _, c := range closed {
 		<-c
 	}
-	return nil
+}
+
+func (m *model) fatal(err error) {
+	select {
+	case m.fatalChan <- err:
+	default:
+	}
 }
 
 // StartDeadlockDetector starts a deadlock detector on the models locks which
@@ -472,7 +501,7 @@ func (m *model) cleanupFolderLocked(cfg config.FolderConfiguration) {
 	delete(m.folderVersioners, cfg.ID)
 }
 
-func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredFiles bool) {
+func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredFiles bool) error {
 	if len(to.ID) == 0 {
 		panic("bug: cannot restart empty folder ID")
 	}
@@ -512,7 +541,11 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
 			// Create a new fset. Might take a while and we do it under
 			// locking, but it's unsafe to create fset:s concurrently so
 			// that's the price we pay.
-			fset = db.NewFileSet(folder, to.Filesystem(), m.db)
+			var err error
+			fset, err = db.NewFileSet(folder, to.Filesystem(), m.db)
+			if err != nil {
+				return fmt.Errorf("restarting %v: %w", to.Description(), err)
+			}
 		}
 		m.addAndStartFolderLocked(to, fset, cacheIgnoredFiles)
 	}
@@ -547,12 +580,17 @@ func (m *model) restartFolder(from, to config.FolderConfiguration, cacheIgnoredF
 		infoMsg = "Restarted"
 	}
 	l.Infof("%v folder %v (%v)", infoMsg, to.Description(), to.Type)
+
+	return nil
 }
 
-func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool) {
+func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool) error {
 	// Creating the fileset can take a long time (metadata calculation) so
 	// we do it outside of the lock.
-	fset := db.NewFileSet(cfg.ID, cfg.Filesystem(), m.db)
+	fset, err := db.NewFileSet(cfg.ID, cfg.Filesystem(), m.db)
+	if err != nil {
+		return fmt.Errorf("adding %v: %w", cfg.Description(), err)
+	}
 
 	m.fmut.Lock()
 	defer m.fmut.Unlock()
@@ -569,6 +607,7 @@ func (m *model) newFolder(cfg config.FolderConfiguration, cacheIgnoredFiles bool
 	m.pmut.RUnlock()
 
 	m.addAndStartFolderLocked(cfg, fset, cacheIgnoredFiles)
+	return nil
 }
 
 func (m *model) UsageReportingStats(report *contract.Report, version int, preview bool) {
@@ -2579,7 +2618,10 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 				l.Infoln("Paused folder", cfg.Description())
 			} else {
 				l.Infoln("Adding folder", cfg.Description())
-				m.newFolder(cfg, to.Options.CacheIgnoredFiles)
+				if err := m.newFolder(cfg, to.Options.CacheIgnoredFiles); err != nil {
+					m.fatal(err)
+					return true
+				}
 			}
 			clusterConfigDevices.add(cfg.DeviceIDs())
 		}
@@ -2603,7 +2645,10 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
 		// This folder exists on both sides. Settings might have changed.
 		// Check if anything differs that requires a restart.
 		if !reflect.DeepEqual(fromCfg.RequiresRestartOnly(), toCfg.RequiresRestartOnly()) || from.Options.CacheIgnoredFiles != to.Options.CacheIgnoredFiles {
-			m.restartFolder(fromCfg, toCfg, to.Options.CacheIgnoredFiles)
+			if err := m.restartFolder(fromCfg, toCfg, to.Options.CacheIgnoredFiles); err != nil {
+				m.fatal(err)
+				return true
+			}
 			clusterConfigDevices.add(fromCfg.DeviceIDs())
 			clusterConfigDevices.add(toCfg.DeviceIDs())
 		}

+ 106 - 115
lib/model/model_test.go

@@ -118,10 +118,10 @@ func createTmpWrapper(cfg config.Configuration) config.Wrapper {
 	return wrapper
 }
 
-func newState(cfg config.Configuration) *testModel {
+func newState(t testing.TB, cfg config.Configuration) *testModel {
 	wcfg := createTmpWrapper(cfg)
 
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 
 	for _, dev := range cfg.Devices {
 		m.AddConnection(&fakeConnection{id: dev.DeviceID, model: m}, protocol.Hello{})
@@ -154,7 +154,7 @@ func addFolderDevicesToClusterConfig(cc protocol.ClusterConfig, remote protocol.
 }
 
 func TestRequest(t *testing.T) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	// Existing, shared file
@@ -227,7 +227,7 @@ func BenchmarkIndex_100(b *testing.B) {
 }
 
 func benchmarkIndex(b *testing.B, nfiles int) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(b, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	files := genFiles(nfiles)
@@ -253,7 +253,7 @@ func BenchmarkIndexUpdate_10000_1(b *testing.B) {
 }
 
 func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(b, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	files := genFiles(nfiles)
@@ -269,7 +269,7 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
 }
 
 func BenchmarkRequestOut(b *testing.B) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(b, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	const n = 1000
@@ -295,7 +295,7 @@ func BenchmarkRequestOut(b *testing.B) {
 }
 
 func BenchmarkRequestInSingleFile(b *testing.B) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(b, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	buf := make([]byte, 128<<10)
@@ -331,8 +331,7 @@ func TestDeviceRename(t *testing.T) {
 	}
 	cfg := config.Wrap("testdata/tmpconfig.xml", rawCfg, device1, events.NoopLogger)
 
-	db := db.NewLowlevel(backend.OpenMemory())
-	m := newModel(cfg, myID, "syncthing", "dev", db, nil)
+	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
 
 	if cfg.Devices()[device1].Name != "" {
 		t.Errorf("Device already has a name")
@@ -427,10 +426,8 @@ func TestClusterConfig(t *testing.T) {
 		},
 	}
 
-	db := db.NewLowlevel(backend.OpenMemory())
-
 	wrapper := createTmpWrapper(cfg)
-	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -497,7 +494,7 @@ func TestIntroducer(t *testing.T) {
 		return false
 	}
 
-	m := newState(config.Configuration{
+	m := newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -538,7 +535,7 @@ func TestIntroducer(t *testing.T) {
 	}
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -589,7 +586,7 @@ func TestIntroducer(t *testing.T) {
 	}
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -637,7 +634,7 @@ func TestIntroducer(t *testing.T) {
 	// 1. Introducer flag no longer set on device
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -684,7 +681,7 @@ func TestIntroducer(t *testing.T) {
 	// 2. SkipIntroductionRemovals is set
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:                 device1,
@@ -737,7 +734,7 @@ func TestIntroducer(t *testing.T) {
 	// Test device not being removed as it's shared without an introducer.
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -784,7 +781,7 @@ func TestIntroducer(t *testing.T) {
 	// Test device not being removed as it's shared by a different introducer.
 
 	cleanupModel(m)
-	m = newState(config.Configuration{
+	m = newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -831,7 +828,7 @@ func TestIntroducer(t *testing.T) {
 }
 
 func TestIssue4897(t *testing.T) {
-	m := newState(config.Configuration{
+	m := newState(t, config.Configuration{
 		Devices: []config.DeviceConfiguration{
 			{
 				DeviceID:   device1,
@@ -863,7 +860,7 @@ func TestIssue4897(t *testing.T) {
 // PR-comments: https://github.com/syncthing/syncthing/pull/5069/files#r203146546
 // Issue: https://github.com/syncthing/syncthing/pull/5509
 func TestIssue5063(t *testing.T) {
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	defer cleanupModel(m)
 
 	m.pmut.Lock()
@@ -918,7 +915,7 @@ func TestAutoAcceptRejected(t *testing.T) {
 	for i := range tcfg.Devices {
 		tcfg.Devices[i].AutoAcceptFolders = false
 	}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	id := srand.String(8)
 	defer os.RemoveAll(id)
@@ -931,7 +928,7 @@ func TestAutoAcceptRejected(t *testing.T) {
 
 func TestAutoAcceptNewFolder(t *testing.T) {
 	// New folder
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	defer cleanupModel(m)
 	id := srand.String(8)
 	defer os.RemoveAll(id)
@@ -942,7 +939,7 @@ func TestAutoAcceptNewFolder(t *testing.T) {
 }
 
 func TestAutoAcceptNewFolderFromTwoDevices(t *testing.T) {
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	defer cleanupModel(m)
 	id := srand.String(8)
 	defer os.RemoveAll(id)
@@ -962,7 +959,7 @@ func TestAutoAcceptNewFolderFromTwoDevices(t *testing.T) {
 func TestAutoAcceptNewFolderFromOnlyOneDevice(t *testing.T) {
 	modifiedCfg := defaultAutoAcceptCfg.Copy()
 	modifiedCfg.Devices[2].AutoAcceptFolders = false
-	m := newState(modifiedCfg)
+	m := newState(t, modifiedCfg)
 	id := srand.String(8)
 	defer os.RemoveAll(id)
 	defer cleanupModel(m)
@@ -1005,7 +1002,7 @@ func TestAutoAcceptNewFolderPremutationsNoPanic(t *testing.T) {
 						fcfg.Paused = localFolderPaused
 						cfg.Folders = append(cfg.Folders, fcfg)
 					}
-					m := newState(cfg)
+					m := newState(t, cfg)
 					m.ClusterConfig(device1, protocol.ClusterConfig{
 						Folders: []protocol.Folder{dev1folder},
 					})
@@ -1027,7 +1024,7 @@ func TestAutoAcceptMultipleFolders(t *testing.T) {
 	defer os.RemoveAll(id1)
 	id2 := srand.String(8)
 	defer os.RemoveAll(id2)
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	defer cleanupModel(m)
 	m.ClusterConfig(device1, createClusterConfig(device1, id1, id2))
 	if fcfg, ok := m.cfg.Folder(id1); !ok || !fcfg.SharedWith(device1) {
@@ -1052,7 +1049,7 @@ func TestAutoAcceptExistingFolder(t *testing.T) {
 			Path: idOther, // To check that path does not get changed.
 		},
 	}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	if fcfg, ok := m.cfg.Folder(id); !ok || fcfg.SharedWith(device1) {
 		t.Error("missing folder, or shared", id)
@@ -1078,7 +1075,7 @@ func TestAutoAcceptNewAndExistingFolder(t *testing.T) {
 			Path: id1, // from previous test case, to verify that path doesn't get changed.
 		},
 	}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	if fcfg, ok := m.cfg.Folder(id1); !ok || fcfg.SharedWith(device1) {
 		t.Error("missing folder, or shared", id1)
@@ -1108,7 +1105,7 @@ func TestAutoAcceptAlreadyShared(t *testing.T) {
 			},
 		},
 	}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
 		t.Error("missing folder, or not shared", id)
@@ -1129,7 +1126,7 @@ func TestAutoAcceptNameConflict(t *testing.T) {
 	testOs.MkdirAll(label, 0777)
 	defer os.RemoveAll(id)
 	defer os.RemoveAll(label)
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	defer cleanupModel(m)
 	m.ClusterConfig(device1, protocol.ClusterConfig{
 		Folders: []protocol.Folder{
@@ -1146,7 +1143,7 @@ func TestAutoAcceptNameConflict(t *testing.T) {
 
 func TestAutoAcceptPrefersLabel(t *testing.T) {
 	// Prefers label, falls back to ID.
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	id := srand.String(8)
 	label := srand.String(8)
 	defer os.RemoveAll(id)
@@ -1169,7 +1166,7 @@ func TestAutoAcceptFallsBackToID(t *testing.T) {
 	testOs := &fatalOs{t}
 
 	// Prefers label, falls back to ID.
-	m := newState(defaultAutoAcceptCfg)
+	m := newState(t, defaultAutoAcceptCfg)
 	id := srand.String(8)
 	label := srand.String(8)
 	t.Log(id, label)
@@ -1207,7 +1204,7 @@ func TestAutoAcceptPausedWhenFolderConfigChanged(t *testing.T) {
 		DeviceID: device1,
 	})
 	tcfg.Folders = []config.FolderConfiguration{fcfg}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
 		t.Error("missing folder, or not shared", id)
@@ -1257,7 +1254,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
 		},
 	}, fcfg.Devices...) // Need to ensure this device order to avoid folder restart.
 	tcfg.Folders = []config.FolderConfiguration{fcfg}
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 	if fcfg, ok := m.cfg.Folder(id); !ok || !fcfg.SharedWith(device1) {
 		t.Error("missing folder, or not shared", id)
@@ -1289,7 +1286,7 @@ func TestAutoAcceptPausedWhenFolderConfigNotChanged(t *testing.T) {
 
 func TestAutoAcceptEnc(t *testing.T) {
 	tcfg := defaultAutoAcceptCfg.Copy()
-	m := newState(tcfg)
+	m := newState(t, tcfg)
 	defer cleanupModel(m)
 
 	id := srand.String(8)
@@ -1460,10 +1457,10 @@ func TestIgnores(t *testing.T) {
 	mustRemove(t, defaultFs.MkdirAll(config.DefaultMarkerName, 0644))
 	writeFile(defaultFs, ".stignore", []byte(".*\nquux\n"), 0644)
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
-	folderIgnoresAlwaysReload(m, defaultFolderConfig)
+	folderIgnoresAlwaysReload(t, m, defaultFolderConfig)
 
 	// Make sure the initial scan has finished (ScanFolders is blocking)
 	m.ScanFolders()
@@ -1521,7 +1518,7 @@ func TestEmptyIgnores(t *testing.T) {
 	mustRemove(t, defaultFs.RemoveAll(config.DefaultMarkerName))
 	must(t, defaultFs.MkdirAll(config.DefaultMarkerName, 0644))
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	if err := m.SetIgnores("default", []string{}); err != nil {
@@ -1573,12 +1570,6 @@ func waitForState(t *testing.T, sub events.Subscription, folder, expected string
 func TestROScanRecovery(t *testing.T) {
 	testOs := &fatalOs{t}
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
-	set := db.NewFileSet("default", defaultFs, ldb)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
-	})
-
 	fcfg := config.FolderConfiguration{
 		ID:              "default",
 		Path:            "rotestfolder",
@@ -1594,10 +1585,15 @@ func TestROScanRecovery(t *testing.T) {
 			},
 		},
 	})
+	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
+
+	set := newFileSet(t, "default", defaultFs, m.db)
+	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
+	})
 
 	testOs.RemoveAll(fcfg.Path)
 
-	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
 	sub := m.evLogger.Subscribe(events.StateChanged)
 	defer sub.Unsubscribe()
 	m.ServeBackground()
@@ -1626,12 +1622,6 @@ func TestROScanRecovery(t *testing.T) {
 func TestRWScanRecovery(t *testing.T) {
 	testOs := &fatalOs{t}
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
-	set := db.NewFileSet("default", defaultFs, ldb)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
-	})
-
 	fcfg := config.FolderConfiguration{
 		ID:              "default",
 		Path:            "rwtestfolder",
@@ -1647,10 +1637,15 @@ func TestRWScanRecovery(t *testing.T) {
 			},
 		},
 	})
+	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
 
 	testOs.RemoveAll(fcfg.Path)
 
-	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
+	set := newFileSet(t, "default", defaultFs, m.db)
+	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+		{Name: "dummyfile", Version: protocol.Vector{Counters: []protocol.Counter{{ID: 42, Value: 1}}}},
+	})
+
 	sub := m.evLogger.Subscribe(events.StateChanged)
 	defer sub.Unsubscribe()
 	m.ServeBackground()
@@ -1678,7 +1673,7 @@ func TestRWScanRecovery(t *testing.T) {
 
 func TestGlobalDirectoryTree(t *testing.T) {
 	w, fcfg := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	b := func(isfile bool, path ...string) protocol.FileInfo {
@@ -1928,7 +1923,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
 
 func TestGlobalDirectorySelfFixing(t *testing.T) {
 	w, fcfg := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	b := func(isfile bool, path ...string) protocol.FileInfo {
@@ -2101,8 +2096,7 @@ func BenchmarkTree_100_10(b *testing.B) {
 }
 
 func benchmarkTree(b *testing.B, n1, n2 int) {
-	db := db.NewLowlevel(backend.OpenMemory())
-	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(b, defaultCfgWrapper, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -2130,7 +2124,7 @@ func TestIssue3028(t *testing.T) {
 
 	// Create a model and default folder
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	// Get a count of how many files are there now
@@ -2164,11 +2158,10 @@ func TestIssue3028(t *testing.T) {
 }
 
 func TestIssue4357(t *testing.T) {
-	db := db.NewLowlevel(backend.OpenMemory())
 	cfg := defaultCfgWrapper.RawCopy()
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
-	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -2271,7 +2264,7 @@ func TestIssue2782(t *testing.T) {
 	}
 	defer os.RemoveAll(testDir)
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	if err := m.ScanFolder("default"); err != nil {
@@ -2287,9 +2280,9 @@ func TestIssue2782(t *testing.T) {
 }
 
 func TestIndexesForUnknownDevicesDropped(t *testing.T) {
-	dbi := db.NewLowlevel(backend.OpenMemory())
+	m := newModel(t, defaultCfgWrapper, myID, "syncthing", "dev", nil)
 
-	files := db.NewFileSet("default", defaultFs, dbi)
+	files := newFileSet(t, "default", defaultFs, m.db)
 	files.Drop(device1)
 	files.Update(device1, genFiles(1))
 	files.Drop(device2)
@@ -2299,12 +2292,11 @@ func TestIndexesForUnknownDevicesDropped(t *testing.T) {
 		t.Error("expected two devices")
 	}
 
-	m := newModel(defaultCfgWrapper, myID, "syncthing", "dev", dbi, nil)
 	m.newFolder(defaultFolderConfig, false)
 	defer cleanupModel(m)
 
 	// Remote sequence is cached, hence need to recreated.
-	files = db.NewFileSet("default", defaultFs, dbi)
+	files = newFileSet(t, "default", defaultFs, m.db)
 
 	if l := len(files.ListDevices()); l != 1 {
 		t.Errorf("Expected one device got %v", l)
@@ -2319,7 +2311,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
 	wcfg.SetFolder(fcfg)
 	defer os.Remove(wcfg.ConfigPath())
 
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	conn1 := &fakeConnection{id: device1, model: m}
@@ -2413,7 +2405,7 @@ func TestIssue3496(t *testing.T) {
 	// percentages. Lets make sure that doesn't happen. Also do some general
 	// checks on the completion calculation stuff.
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	m.ScanFolder("default")
@@ -2484,7 +2476,7 @@ func TestIssue3496(t *testing.T) {
 }
 
 func TestIssue3804(t *testing.T) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	// Subdirs ending in slash should be accepted
@@ -2495,7 +2487,7 @@ func TestIssue3804(t *testing.T) {
 }
 
 func TestIssue3829(t *testing.T) {
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	// Empty subdirs should be accepted
@@ -2514,7 +2506,7 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
 	fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
 	wcfg.SetFolder(fcfg)
 
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	file := testDataExpected["foo"]
@@ -2598,7 +2590,7 @@ func TestIssue2571(t *testing.T) {
 		fd.Close()
 	}
 
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModel(m)
 
 	must(t, testFs.RemoveAll("toLink"))
@@ -2637,7 +2629,7 @@ func TestIssue4573(t *testing.T) {
 	must(t, err)
 	fd.Close()
 
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModel(m)
 
 	must(t, testFs.Chmod("inaccessible", 0000))
@@ -2689,7 +2681,7 @@ func TestInternalScan(t *testing.T) {
 		}
 	}
 
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModel(m)
 
 	for _, dir := range baseDirs {
@@ -2714,12 +2706,6 @@ func TestInternalScan(t *testing.T) {
 func TestCustomMarkerName(t *testing.T) {
 	testOs := &fatalOs{t}
 
-	ldb := db.NewLowlevel(backend.OpenMemory())
-	set := db.NewFileSet("default", defaultFs, ldb)
-	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
-		{Name: "dummyfile"},
-	})
-
 	fcfg := testFolderConfigTmp()
 	fcfg.ID = "default"
 	fcfg.RescanIntervalS = 1
@@ -2735,7 +2721,12 @@ func TestCustomMarkerName(t *testing.T) {
 
 	testOs.RemoveAll(fcfg.Path)
 
-	m := newModel(cfg, myID, "syncthing", "dev", ldb, nil)
+	m := newModel(t, cfg, myID, "syncthing", "dev", nil)
+	set := newFileSet(t, "default", defaultFs, m.db)
+	set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
+		{Name: "dummyfile"},
+	})
+
 	sub := m.evLogger.Subscribe(events.StateChanged)
 	defer sub.Unsubscribe()
 	m.ServeBackground()
@@ -2761,7 +2752,7 @@ func TestRemoveDirWithContent(t *testing.T) {
 	must(t, err)
 	fd.Close()
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	dir, ok := m.CurrentFolderFile("default", "dirwith")
@@ -2812,7 +2803,7 @@ func TestRemoveDirWithContent(t *testing.T) {
 }
 
 func TestIssue4475(t *testing.T) {
-	m, conn, fcfg := setupModelWithConnection()
+	m, conn, fcfg := setupModelWithConnection(t)
 	defer cleanupModel(m)
 	testFs := fcfg.Filesystem()
 
@@ -2884,7 +2875,7 @@ func TestVersionRestore(t *testing.T) {
 	}
 	cfg := createTmpWrapper(rawConfig)
 
-	m := setupModel(cfg)
+	m := setupModel(t, cfg)
 	defer cleanupModel(m)
 	m.ScanFolder("default")
 
@@ -3062,7 +3053,7 @@ func TestVersionRestore(t *testing.T) {
 func TestPausedFolders(t *testing.T) {
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(defaultCfgWrapper.RawCopy())
-	m := setupModel(wrapper)
+	m := setupModel(t, wrapper)
 	defer cleanupModel(m)
 
 	if err := m.ScanFolder("default"); err != nil {
@@ -3087,10 +3078,9 @@ func TestPausedFolders(t *testing.T) {
 func TestIssue4094(t *testing.T) {
 	testOs := &fatalOs{t}
 
-	db := db.NewLowlevel(backend.OpenMemory())
 	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
-	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
+	m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	defer cleanupModel(m)
 
@@ -3123,11 +3113,8 @@ func TestIssue4094(t *testing.T) {
 func TestIssue4903(t *testing.T) {
 	testOs := &fatalOs{t}
 
-	db := db.NewLowlevel(backend.OpenMemory())
-	// Create a separate wrapper not to pollute other tests.
 	wrapper := createTmpWrapper(config.Configuration{})
-	m := newModel(wrapper, myID, "syncthing", "dev", db, nil)
-	m.ServeBackground()
+	m := setupModel(t, wrapper)
 	defer cleanupModel(m)
 
 	// Force the model to wire itself and add the folders
@@ -3159,7 +3146,7 @@ func TestIssue4903(t *testing.T) {
 func TestIssue5002(t *testing.T) {
 	// recheckFile should not panic when given an index equal to the number of blocks
 
-	m := setupModel(defaultCfgWrapper)
+	m := setupModel(t, defaultCfgWrapper)
 	defer cleanupModel(m)
 
 	if err := m.ScanFolder("default"); err != nil {
@@ -3178,7 +3165,7 @@ func TestIssue5002(t *testing.T) {
 }
 
 func TestParentOfUnignored(t *testing.T) {
-	m := newState(defaultCfg)
+	m := newState(t, defaultCfg)
 	defer cleanupModel(m)
 	defer defaultFolderConfig.Filesystem().Remove(".stignore")
 
@@ -3202,7 +3189,7 @@ func TestFolderRestartZombies(t *testing.T) {
 	folderCfg.FilesystemType = fs.FilesystemTypeFake
 	wrapper.SetFolder(folderCfg)
 
-	m := setupModel(wrapper)
+	m := setupModel(t, wrapper)
 	defer cleanupModel(m)
 
 	// Make sure the folder is up and running, because we want to count it.
@@ -3248,7 +3235,7 @@ func TestRequestLimit(t *testing.T) {
 	dev, _ := wrapper.Device(device1)
 	dev.MaxRequestKiB = 1
 	wrapper.SetDevice(dev)
-	m, _ := setupModelWithConnectionFromWrapper(wrapper)
+	m, _ := setupModelWithConnectionFromWrapper(t, wrapper)
 	defer cleanupModel(m)
 
 	file := "tmpfile"
@@ -3292,7 +3279,7 @@ func TestConnCloseOnRestart(t *testing.T) {
 	}()
 
 	w, fcfg := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	br := &testutils.BlockingRW{}
@@ -3331,7 +3318,7 @@ func TestModTimeWindow(t *testing.T) {
 	tfs := fcfg.Filesystem()
 	fcfg.RawModTimeWindowS = 2
 	w.SetFolder(fcfg)
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	name := "foo"
@@ -3383,7 +3370,7 @@ func TestModTimeWindow(t *testing.T) {
 }
 
 func TestDevicePause(t *testing.T) {
-	m, _, fcfg := setupModelWithConnection()
+	m, _, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	sub := m.evLogger.Subscribe(events.DevicePaused)
@@ -3411,7 +3398,7 @@ func TestDevicePause(t *testing.T) {
 }
 
 func TestDeviceWasSeen(t *testing.T) {
-	m, _, fcfg := setupModelWithConnection()
+	m, _, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	m.deviceWasSeen(device1)
@@ -3458,7 +3445,7 @@ func TestSummaryPausedNoError(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
 	fcfg.Paused = true
 	wcfg.SetFolder(fcfg)
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	fss := NewFolderSummaryService(wcfg, m, myID, events.NoopLogger)
@@ -3471,7 +3458,7 @@ func TestFolderAPIErrors(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
 	fcfg.Paused = true
 	wcfg.SetFolder(fcfg)
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	methods := []func(folder string) error{
@@ -3501,7 +3488,7 @@ func TestFolderAPIErrors(t *testing.T) {
 
 func TestRenameSequenceOrder(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	numFiles := 20
@@ -3571,7 +3558,7 @@ func TestRenameSequenceOrder(t *testing.T) {
 
 func TestRenameSameFile(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
@@ -3621,7 +3608,7 @@ func TestRenameSameFile(t *testing.T) {
 
 func TestRenameEmptyFile(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
@@ -3697,7 +3684,7 @@ func TestRenameEmptyFile(t *testing.T) {
 
 func TestBlockListMap(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
@@ -3764,7 +3751,7 @@ func TestBlockListMap(t *testing.T) {
 
 func TestScanRenameCaseOnly(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	ffs := fcfg.Filesystem()
@@ -3922,7 +3909,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
 	fcfg.Type = config.FolderTypeReceiveOnly
 	waiter, _ := w.SetFolder(fcfg)
 	waiter.Wait()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModel(m)
 	name := "foo"
 	ffs := fcfg.Filesystem()
@@ -3961,7 +3948,7 @@ func TestScanDeletedROChangedOnSR(t *testing.T) {
 func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSecond bool, pre func(config.Wrapper), fn func(config.Wrapper)) {
 	t.Helper()
 	wcfg, _ := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModel(m)
 
 	_, err := wcfg.SetDevice(config.NewDeviceConfiguration(device2, "device2"))
@@ -4037,9 +4024,13 @@ func TestIssue6961(t *testing.T) {
 	fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
 	wcfg.SetFolder(fcfg)
 	// Always recalc/repair when opening a fileset.
-	// db := db.NewLowlevel(backend.OpenMemory(), db.WithRecheckInterval(time.Millisecond))
-	db := db.NewLowlevel(backend.OpenMemory())
-	m := newModel(wcfg, myID, "syncthing", "dev", db, nil)
+	m := newModel(t, wcfg, myID, "syncthing", "dev", nil)
+	m.db.Close()
+	var err error
+	m.db, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond))
+	if err != nil {
+		t.Fatal(err)
+	}
 	m.ServeBackground()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 	m.ScanFolders()
@@ -4101,7 +4092,7 @@ func TestIssue6961(t *testing.T) {
 
 func TestCompletionEmptyGlobal(t *testing.T) {
 	wcfg, fcfg := tmpDefaultWrapper()
-	m := setupModel(wcfg)
+	m := setupModel(t, wcfg)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 	files := []protocol.FileInfo{{Name: "foo", Version: protocol.Vector{}.Update(myID.Short()), Sequence: 1}}
 	m.fmut.Lock()
@@ -4123,7 +4114,7 @@ func TestNeedMetaAfterIndexReset(t *testing.T) {
 	fcfg.Devices = append(fcfg.Devices, config.FolderDeviceConfiguration{DeviceID: device2})
 	waiter, _ = w.SetFolder(fcfg)
 	waiter.Wait()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, fcfg.Path)
 
 	var seq int64 = 1
@@ -4158,7 +4149,7 @@ func TestNeedMetaAfterIndexReset(t *testing.T) {
 
 func TestCcCheckEncryption(t *testing.T) {
 	w, fcfg := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	m.cancel()
 	defer cleanupModel(m)
 
@@ -4299,7 +4290,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 	// Create the folder, but don't start it.
 	w, fcfg := tmpDefaultWrapper()
 	tfs := fcfg.Filesystem()
-	m := newModel(w, myID, "syncthing", "dev", db.NewLowlevel(backend.OpenMemory()), nil)
+	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	// A connection can happen before all the folders are started.
@@ -4325,7 +4316,7 @@ func TestCCFolderNotRunning(t *testing.T) {
 
 func TestPendingFolder(t *testing.T) {
 	w, _ := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModel(m)
 
 	waiter, err := w.SetDevice(config.DeviceConfiguration{DeviceID: device2})

+ 26 - 26
lib/model/requests_test.go

@@ -20,8 +20,6 @@ import (
 	"time"
 
 	"github.com/syncthing/syncthing/lib/config"
-	"github.com/syncthing/syncthing/lib/db"
-	"github.com/syncthing/syncthing/lib/db/backend"
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/fs"
 	"github.com/syncthing/syncthing/lib/protocol"
@@ -31,7 +29,7 @@ func TestRequestSimple(t *testing.T) {
 	// Verify that the model performs a request and creates a file based on
 	// an incoming index update.
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -74,7 +72,7 @@ func TestSymlinkTraversalRead(t *testing.T) {
 		return
 	}
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -117,7 +115,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 		return
 	}
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -176,7 +174,7 @@ func TestSymlinkTraversalWrite(t *testing.T) {
 func TestRequestCreateTmpSymlink(t *testing.T) {
 	// Test that an update for a temporary file is invalidated
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	// We listen for incoming index updates and trigger when we see one for
@@ -226,7 +224,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
 
 	fcfg.Versioning = config.VersioningConfiguration{Type: "trashcan"}
 	w.SetFolder(fcfg)
-	m, fc := setupModelWithConnectionFromWrapper(w)
+	m, fc := setupModelWithConnectionFromWrapper(t, w)
 	defer cleanupModel(m)
 
 	// Create a temporary directory that we will use as target to see if
@@ -300,10 +298,10 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 	fss := fcfg.Filesystem()
 	fcfg.Type = ft
 	w.SetFolder(fcfg)
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, fss.URI())
 
-	folderIgnoresAlwaysReload(m, fcfg)
+	folderIgnoresAlwaysReload(t, m, fcfg)
 
 	fc := addFakeConn(m, device1)
 	fc.folder = "default"
@@ -422,7 +420,7 @@ func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
 }
 
 func TestIssue4841(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	received := make(chan []protocol.FileInfo)
@@ -466,7 +464,7 @@ func TestIssue4841(t *testing.T) {
 }
 
 func TestRescanIfHaveInvalidContent(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -532,7 +530,7 @@ func TestRescanIfHaveInvalidContent(t *testing.T) {
 }
 
 func TestParentDeletion(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	testFs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, testFs.URI())
 
@@ -611,7 +609,7 @@ func TestRequestSymlinkWindows(t *testing.T) {
 		t.Skip("windows specific test")
 	}
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	defer cleanupModelAndRemoveDir(m, fcfg.Filesystem().URI())
 
 	received := make(chan []protocol.FileInfo)
@@ -679,7 +677,7 @@ func equalContents(path string, contents []byte) error {
 }
 
 func TestRequestRemoteRenameChanged(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
@@ -814,7 +812,7 @@ func TestRequestRemoteRenameChanged(t *testing.T) {
 }
 
 func TestRequestRemoteRenameConflict(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
@@ -905,7 +903,7 @@ func TestRequestRemoteRenameConflict(t *testing.T) {
 }
 
 func TestRequestDeleteChanged(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -974,7 +972,7 @@ func TestRequestDeleteChanged(t *testing.T) {
 }
 
 func TestNeedFolderFiles(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	tmpDir := tfs.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
@@ -1023,12 +1021,12 @@ func TestNeedFolderFiles(t *testing.T) {
 // https://github.com/syncthing/syncthing/issues/6038
 func TestIgnoreDeleteUnignore(t *testing.T) {
 	w, fcfg := tmpDefaultWrapper()
-	m := setupModel(w)
+	m := setupModel(t, w)
 	fss := fcfg.Filesystem()
 	tmpDir := fss.URI()
 	defer cleanupModelAndRemoveDir(m, tmpDir)
 
-	folderIgnoresAlwaysReload(m, fcfg)
+	folderIgnoresAlwaysReload(t, m, fcfg)
 	m.ScanFolders()
 
 	fc := addFakeConn(m, device1)
@@ -1122,7 +1120,7 @@ func TestIgnoreDeleteUnignore(t *testing.T) {
 // TestRequestLastFileProgress checks that the last pulled file (here only) is registered
 // as in progress.
 func TestRequestLastFileProgress(t *testing.T) {
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -1158,7 +1156,7 @@ func TestRequestIndexSenderPause(t *testing.T) {
 	done := make(chan struct{})
 	defer close(done)
 
-	m, fc, fcfg := setupModelWithConnection()
+	m, fc, fcfg := setupModelWithConnection(t)
 	tfs := fcfg.Filesystem()
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
@@ -1279,7 +1277,6 @@ func TestRequestIndexSenderPause(t *testing.T) {
 }
 
 func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
-	ldb := db.NewLowlevel(backend.OpenMemory())
 	w, fcfg := tmpDefaultWrapper()
 	tfs := fcfg.Filesystem()
 	dir1 := "foo"
@@ -1287,16 +1284,19 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
 
 	// Initialise db with an entry and then stop everything again
 	must(t, tfs.Mkdir(dir1, 0777))
-	m := newModel(w, myID, "syncthing", "dev", ldb, nil)
+	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 	m.ServeBackground()
 	m.ScanFolders()
 	m.cancel()
-	m.evCancel()
 	<-m.stopped
 
 	// Add connection (sends incoming cluster config) before starting the new model
-	m = newModel(w, myID, "syncthing", "dev", ldb, nil)
+	m = &testModel{
+		model:    NewModel(m.cfg, m.id, m.clientName, m.clientVersion, m.db, m.protectedFiles, m.evLogger).(*model),
+		evCancel: m.evCancel,
+		stopped:  make(chan struct{}),
+	}
 	defer cleanupModel(m)
 	fc := addFakeConn(m, device1)
 	done := make(chan struct{})
@@ -1351,7 +1351,7 @@ func TestRequestReceiveEncryptedLocalNoSend(t *testing.T) {
 	must(t, tfs.Mkdir(config.DefaultMarkerName, 0777))
 	must(t, writeEncryptionToken(encToken, fcfg))
 
-	m := setupModel(w)
+	m := setupModel(t, w)
 	defer cleanupModelAndRemoveDir(m, tfs.URI())
 
 	files := genFiles(2)

+ 27 - 10
lib/model/testutils_test.go

@@ -96,14 +96,16 @@ func testFolderConfigFake() config.FolderConfiguration {
 	return cfg
 }
 
-func setupModelWithConnection() (*testModel, *fakeConnection, config.FolderConfiguration) {
+func setupModelWithConnection(t testing.TB) (*testModel, *fakeConnection, config.FolderConfiguration) {
+	t.Helper()
 	w, fcfg := tmpDefaultWrapper()
-	m, fc := setupModelWithConnectionFromWrapper(w)
+	m, fc := setupModelWithConnectionFromWrapper(t, w)
 	return m, fc, fcfg
 }
 
-func setupModelWithConnectionFromWrapper(w config.Wrapper) (*testModel, *fakeConnection) {
-	m := setupModel(w)
+func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testModel, *fakeConnection) {
+	t.Helper()
+	m := setupModel(t, w)
 
 	fc := addFakeConn(m, device1)
 	fc.folder = "default"
@@ -113,9 +115,9 @@ func setupModelWithConnectionFromWrapper(w config.Wrapper) (*testModel, *fakeCon
 	return m, fc
 }
 
-func setupModel(w config.Wrapper) *testModel {
-	db := db.NewLowlevel(backend.OpenMemory())
-	m := newModel(w, myID, "syncthing", "dev", db, nil)
+func setupModel(t testing.TB, w config.Wrapper) *testModel {
+	t.Helper()
+	m := newModel(t, w, myID, "syncthing", "dev", nil)
 	m.ServeBackground()
 	<-m.started
 
@@ -131,8 +133,13 @@ type testModel struct {
 	stopped  chan struct{}
 }
 
-func newModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string) *testModel {
+func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, protectedFiles []string) *testModel {
+	t.Helper()
 	evLogger := events.NewLogger()
+	ldb, err := db.NewLowlevel(backend.OpenMemory(), evLogger)
+	if err != nil {
+		t.Fatal(err)
+	}
 	m := NewModel(cfg, id, clientName, clientVersion, ldb, protectedFiles, evLogger).(*model)
 	ctx, cancel := context.WithCancel(context.Background())
 	go evLogger.Serve(ctx)
@@ -250,9 +257,10 @@ func dbSnapshot(t *testing.T, m Model, folder string) *db.Snapshot {
 // reloads when asked to, instead of checking file mtimes. This is
 // because we will be changing the files on disk often enough that the
 // mtimes will be unreliable to determine change status.
-func folderIgnoresAlwaysReload(m *testModel, fcfg config.FolderConfiguration) {
+func folderIgnoresAlwaysReload(t testing.TB, m *testModel, fcfg config.FolderConfiguration) {
+	t.Helper()
 	m.removeFolder(fcfg)
-	fset := db.NewFileSet(fcfg.ID, fcfg.Filesystem(), m.db)
+	fset := newFileSet(t, fcfg.ID, fcfg.Filesystem(), m.db)
 	ignores := ignore.New(fcfg.Filesystem(), ignore.WithCache(true), ignore.WithChangeDetector(newAlwaysChanged()))
 	m.fmut.Lock()
 	m.addAndStartFolderLockedWithIgnores(fcfg, fset, ignores)
@@ -296,3 +304,12 @@ func localIndexUpdate(m *testModel, folder string, fs []protocol.FileInfo) {
 		"version":   seq, // legacy for sequence
 	})
 }
+
+func newFileSet(t testing.TB, folder string, fs fs.Filesystem, ldb *db.Lowlevel) *db.FileSet {
+	t.Helper()
+	fset, err := db.NewFileSet(folder, fs, ldb)
+	if err != nil {
+		t.Fatal(err)
+	}
+	return fset
+}

+ 7 - 3
lib/syncthing/syncthing.go

@@ -80,17 +80,21 @@ type App struct {
 	stopped           chan struct{}
 }
 
-func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) *App {
+func New(cfg config.Wrapper, dbBackend backend.Backend, evLogger events.Logger, cert tls.Certificate, opts Options) (*App, error) {
+	ll, err := db.NewLowlevel(dbBackend, evLogger, db.WithRecheckInterval(opts.DBRecheckInterval), db.WithIndirectGCInterval(opts.DBIndirectGCInterval))
+	if err != nil {
+		return nil, err
+	}
 	a := &App{
 		cfg:      cfg,
-		ll:       db.NewLowlevel(dbBackend, db.WithRecheckInterval(opts.DBRecheckInterval), db.WithIndirectGCInterval(opts.DBIndirectGCInterval)),
+		ll:       ll,
 		evLogger: evLogger,
 		opts:     opts,
 		cert:     cert,
 		stopped:  make(chan struct{}),
 	}
 	close(a.stopped) // Hasn't been started, so shouldn't block on Wait.
-	return a
+	return a, nil
 }
 
 // Start executes the app and returns once all the startup operations are done,

+ 4 - 1
lib/syncthing/syncthing_test.go

@@ -80,7 +80,10 @@ func TestStartupFail(t *testing.T) {
 	defer os.Remove(cfg.ConfigPath())
 
 	db := backend.OpenMemory()
-	app := New(cfg, db, events.NoopLogger, cert, Options{})
+	app, err := New(cfg, db, events.NoopLogger, cert, Options{})
+	if err != nil {
+		t.Fatal(err)
+	}
 	startErr := app.Start()
 	if startErr == nil {
 		t.Fatal("Expected an error from Start, got nil")

+ 14 - 0
lib/util/utils.go

@@ -8,6 +8,7 @@ package util
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"net"
 	"net/url"
@@ -262,6 +263,19 @@ type FatalErr struct {
 	Status ExitStatus
 }
 
+// AsFatalErr wraps the given error creating a FatalErr. If the given error
+// already is of type FatalErr, it is not wrapped again.
+func AsFatalErr(err error, status ExitStatus) *FatalErr {
+	var ferr *FatalErr
+	if errors.As(err, &ferr) {
+		return ferr
+	}
+	return &FatalErr{
+		Err:    err,
+		Status: status,
+	}
+}
+
 func (e *FatalErr) Error() string {
 	return e.Err.Error()
 }