Repository: syncthing/syncthing (language: go)

Issue:
Watching for changes using too many system resources on BSDs (kqueue)
Kqueue is a very heavyweight mechanism to watch for changes. Issues about systems being brought to their knees by Syncthing's watching for changes with large folders come in every now and then, see e.g. https://forum.syncthing.net/t/truenas-freenas-folder-watcher-unusably-slow-crashes-host/17161

We should somehow prevent this. Either disabling kqueue alltogether or adding a limit for items in a folder (disable kqueue once reached). The latter will be somewhat annoying, as I guess users will want to adjust/override that, and we'd need to enable watching only after the initial scan (so we know how many items there are). However just disabling it will probably make a lot of people mad.


Discussion / hints:
Thanks for adding this issue @imsodin. As you noted via the above forum post link, this issue bit me hard this week, so I would have benefited from some kind of mitigation.

Changing the defaults when the host is BSD would have been helpful, but honestly would not have helped me **that** much, because I probably would have simply turned it on when I set everything up.

Honestly, as a sysadmin, I would cast my vote for a UI warning along with some clear documentation around the risks and tradeoffs of enabling the feature. I think that would have saved me a couple days of hair-pulling.

To further clarify, I don't think the biggest issue is the performance impact itself. I think the biggest issue was the mystery of trying to figure out **what was causing** the performance impact. So a clear UI warning would have been incredibly helpful.

Thanks again for working on Syncthing. It's pretty incredible!
I agree, it took me several weeks of screwing around to finally figure out why anytime Syncthing was enabled my system would CRAWL and be literally unusable (which was tough to troubleshoot because the shell and webgui would actually time out before getting a response.) I was trying to install custom Syncthing jails with resource limits and all kinds of circular goose chasing, only to find out it was just an otherwise innocent looking setting... 
So perhaps a one time notification similar to the "you should set up authentication" one, like "my dude, you're on a system with kqueue, use filesystem notifications with care and be aware that enabling it for large folders will use lots of system resources", perhaps with a link to some suitable doc article / faq...
Exactly. Just something to make the average user aware of the performance impact. For me, the filesystem watcher works flawlessly on my Windows and my prior Linux setup, but the FreeBSD version (that i only started using/learning a month ago) basically left me completely unaware.

Otherwise, its a pretty nice bit of code you've got here.
"Average user" and "BSD" in the same paragraph, wat?
Right. That is a fair point you have there.. Perhaps rookie, inexperienced or just plain dumbass (me) users might have been more precise - hard to say. Bahaha
i think, regardless of the user-model, (this) software should give relevant information to the user in order to not waste their time. So a notification should be fine. This would be in the bsd-spirit  :smile:
Is this a scalability issue? Could I fix the hang by adding another 128GB RAM to the box running Syncthing?
Its probably cheaper to move to linux, if you can.
I already tried and it didn't help
Tried what and what didn't help?
256GB RAM in the box, the issue described above still happens (~11M files)
Yes, on BSD watching large folders does not work, this is what issue talks about.
I can not confirm this general statement.
I have one TrueNAS server which totally works fine detecting changes on a summarized size of ~ 33TB (774 k files).
The other TrueNAS server I have is struggling as soon as I switch the on the watcher. So there must be something that allows it or cripples it.

Both systems are Supermicro ones.
The first system is my beafy main one (16 Cores, 2 Sockets, 128 GB RAM).
The second system is my backup system (4 Cores, 1 Socket propagated, 32 GB RAM).
Both have a X9 board and are using LSI/Broadcom JBOD controllers.
You have relatively few files, and by default the system limit on fd:s is relative to the amount of RAM.
So it isn't FreeBSD in general then, it is the amount of RAM - you say.
But what about 

> 256GB RAM in the box, the issue described above still happens (~11M files)


What about it? 
> Yes, on BSD watching large folders does not work, this is what issue talks about.

To which the question arose, what is the limiting factor. Not memory, apparently.
Seems like this question should be asked on the BSD forums?
I don't think any of the syncthing maintainers can answer questions about BSD scalability concerns.
Yeah. The limit is max possible fd:s. This apparently scales with ram somehow, in at least some configurations. But really, the details are up to you as system administrator of your system to figure out. I don't think questioning us why 775k files hit the limit on 32 gigs when it doesn't on 128 gigs and how that rhymes with 11m files on 256 gigs is the thing. These are freebsd limits so apply freebsd knowledge. 

Fix patch:
diff --git a/go.mod b/go.mod
index a2a55bc0736..c1ae4cd3766 100644
--- a/go.mod
+++ b/go.mod
@@ -53,7 +53,7 @@ require (
 	golang.org/x/sys v0.0.0-20211013075003-97ac67df715c
 	golang.org/x/text v0.3.7
 	golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
-	golang.org/x/tools v0.1.6
+	golang.org/x/tools v0.1.7
 	google.golang.org/protobuf v1.27.1
 )
 
diff --git a/go.sum b/go.sum
index abc7c800474..9f4fcebeefa 100644
--- a/go.sum
+++ b/go.sum
@@ -654,6 +654,8 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4=
 golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
+golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/lib/fs/basicfs_watch_eventtypes_kqueue.go b/lib/fs/basicfs_watch_eventtypes_kqueue.go
index af6fd018673..c8e34dfd61d 100644
--- a/lib/fs/basicfs_watch_eventtypes_kqueue.go
+++ b/lib/fs/basicfs_watch_eventtypes_kqueue.go
@@ -18,4 +18,7 @@ const (
 	subEventMask  = notify.NoteDelete | notify.NoteWrite | notify.NoteRename | notify.Create | notify.NoteAttrib | notify.NoteExtend
 	permEventMask = 0
 	rmEventMask   = notify.NoteDelete | notify.NoteRename
+
+	// WatchKqueue indicates if kqueue is used for filesystem watching
+	WatchKqueue = true
 )
diff --git a/lib/fs/basicfs_watch_notkqueue.go b/lib/fs/basicfs_watch_notkqueue.go
new file mode 100644
index 00000000000..7d529cc2777
--- /dev/null
+++ b/lib/fs/basicfs_watch_notkqueue.go
@@ -0,0 +1,13 @@
+// Copyright (C) 2022 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//go:build !dragonfly && !freebsd && !netbsd && !openbsd
+// +build !dragonfly,!freebsd,!netbsd,!openbsd
+
+package fs
+
+// WatchKqueue indicates if kqueue is used for filesystem watching
+const WatchKqueue = false
diff --git a/lib/model/folder.go b/lib/model/folder.go
index 30fddd2d91a..a318a64eec1 100644
--- a/lib/model/folder.go
+++ b/lib/model/folder.go
@@ -34,6 +34,9 @@ import (
 	"github.com/syncthing/syncthing/lib/watchaggregator"
 )
 
+// Arbitrary limit that triggers a warning on kqueue systems
+const kqueueItemCountThreshold = 10000
+
 type folder struct {
 	stateTracker
 	config.FolderConfiguration
@@ -81,6 +84,8 @@ type folder struct {
 
 	puller    puller
 	versioner versioner.Versioner
+
+	warnedKqueue bool
 }
 
 type syncRequest struct {
@@ -980,6 +985,19 @@ func (f *folder) monitorWatch(ctx context.Context) {
 	warnedOutside := false
 	var lastWatch time.Time
 	pause := time.Minute
+	// Subscribe to folder summaries only on kqueue systems, to warn about potential high resource usage
+	var summarySub events.Subscription
+	var summaryChan <-chan events.Event
+	if fs.WatchKqueue && !f.warnedKqueue {
+		summarySub = f.evLogger.Subscribe(events.FolderCompletion)
+		summaryChan = summarySub.C()
+	}
+	defer func() {
+		aggrCancel() // aggrCancel might e re-assigned -> call within closure
+		if summaryChan != nil {
+			summarySub.Unsubscribe()
+		}
+	}()
 	for {
 		select {
 		case <-failTimer.C:
@@ -1024,6 +1042,15 @@ func (f *folder) monitorWatch(ctx context.Context) {
 			aggrCancel()
 			errChan = nil
 			aggrCtx, aggrCancel = context.WithCancel(ctx)
+		case ev := <-summaryChan:
+			if data, ok := ev.Data.(FolderSummaryEventData); !ok {
+				f.evLogger.Log(events.Failure, "Unexpected type of folder-summary event in folder.monitorWatch")
+			} else if data.Summary.LocalTotalItems > kqueueItemCountThreshold {
+				f.warnedKqueue = true
+				summarySub.Unsubscribe()
+				summaryChan = nil
+				l.Warnf("Filesystem watching (kqueue) is enabled on %v with a lot of files/directories, and that requires a lot of resources and might slow down your system significantly", f.Description())
+			}
 		case <-ctx.Done():
 			return
 		}
diff --git a/lib/model/folder_summary.go b/lib/model/folder_summary.go
index 58427b31835..405a8e54bc9 100644
--- a/lib/model/folder_summary.go
+++ b/lib/model/folder_summary.go
@@ -29,7 +29,7 @@ const maxDurationSinceLastEventReq = time.Minute
 
 type FolderSummaryService interface {
 	suture.Service
-	Summary(folder string) (map[string]interface{}, error)
+	Summary(folder string) (*FolderSummary, error)
 	OnEventRequest()
 }
 
@@ -76,8 +76,58 @@ func (c *folderSummaryService) String() string {
 	return fmt.Sprintf("FolderSummaryService@%p", c)
 }
 
-func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, error) {
-	var res = make(map[string]interface{})
+// FolderSummary replaces the previously used map[string]interface{}, and needs
+// to keep the structure/naming for api backwards compatibility
+type FolderSummary struct {
+	Errors     int `json:"errors"`
+	PullErrors int `json:"pullErrors"` // deprecated
+
+	Invalid string `json:"invalid"` // deprecated
+
+	GlobalFiles       int   `json:"globalFiles"`
+	GlobalDirectories int   `json:"globalDirectories"`
+	GlobalSymlinks    int   `json:"globalSymlinks"`
+	GlobalDeleted     int   `json:"globalDeleted"`
+	GlobalBytes       int64 `json:"globalBytes"`
+	GlobalTotalItems  int   `json:"globalTotalItems"`
+
+	LocalFiles       int   `json:"localFiles"`
+	LocalDirectories int   `json:"localDirectories"`
+	LocalSymlinks    int   `json:"localSymlinks"`
+	LocalDeleted     int   `json:"localDeleted"`
+	LocalBytes       int64 `json:"localBytes"`
+	LocalTotalItems  int   `json:"localTotalItems"`
+
+	NeedFiles       int   `json:"needFiles"`
+	NeedDirectories int   `json:"needDirectories"`
+	NeedSymlinks    int   `json:"needSymlinks"`
+	NeedDeletes     int   `json:"needDeletes"`
+	NeedBytes       int64 `json:"needBytes"`
+	NeedTotalItems  int   `json:"needTotalItems"`
+
+	ReceiveOnlyChangedFiles       int   `json:"receiveOnlyChangedFiles"`
+	ReceiveOnlyChangedDirectories int   `json:"receiveOnlyChangedDirectories"`
+	ReceiveOnlyChangedSymlinks    int   `json:"receiveOnlyChangedSymlinks"`
+	ReceiveOnlyChangedDeletes     int   `json:"receiveOnlyChangedDeletes"`
+	ReceiveOnlyChangedBytes       int64 `json:"receiveOnlyChangedBytes"`
+	ReceiveOnlyTotalItems         int   `json:"receiveOnlyTotalItems"`
+
+	InSyncFiles int   `json:"inSyncFiles"`
+	InSyncBytes int64 `json:"inSyncBytes"`
+
+	State        string    `json:"state"`
+	StateChanged time.Time `json:"stateChanged"`
+	Error        string    `json:"error"`
+
+	Version  int64 `json:"version"` // deprecated
+	Sequence int64 `json:"sequence"`
+
+	IgnorePatterns bool   `json:"ignorePatterns"`
+	WatchError     string `json:"watchError"`
+}
+
+func (c *folderSummaryService) Summary(folder string) (*FolderSummary, error) {
+	res := new(FolderSummary)
 
 	var local, global, need, ro db.Counts
 	var ourSeq, remoteSeq int64
@@ -101,14 +151,14 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
 		return nil, err
 	}
 
-	res["errors"] = len(errors)
-	res["pullErrors"] = len(errors) // deprecated
+	res.Errors = len(errors)
+	res.PullErrors = len(errors) // deprecated
 
-	res["invalid"] = "" // Deprecated, retains external API for now
+	res.Invalid = "" // Deprecated, retains external API for now
 
-	res["globalFiles"], res["globalDirectories"], res["globalSymlinks"], res["globalDeleted"], res["globalBytes"], res["globalTotalItems"] = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
+	res.GlobalFiles, res.GlobalDirectories, res.GlobalSymlinks, res.GlobalDeleted, res.GlobalBytes, res.GlobalTotalItems = global.Files, global.Directories, global.Symlinks, global.Deleted, global.Bytes, global.TotalItems()
 
-	res["localFiles"], res["localDirectories"], res["localSymlinks"], res["localDeleted"], res["localBytes"], res["localTotalItems"] = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
+	res.LocalFiles, res.LocalDirectories, res.LocalSymlinks, res.LocalDeleted, res.LocalBytes, res.LocalTotalItems = local.Files, local.Directories, local.Symlinks, local.Deleted, local.Bytes, local.TotalItems()
 
 	fcfg, haveFcfg := c.cfg.Folder(folder)
 
@@ -122,41 +172,41 @@ func (c *folderSummaryService) Summary(folder string) (map[string]interface{}, e
 	if need.Bytes < 0 {
 		need.Bytes = 0
 	}
-	res["needFiles"], res["needDirectories"], res["needSymlinks"], res["needDeletes"], res["needBytes"], res["needTotalItems"] = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
+	res.NeedFiles, res.NeedDirectories, res.NeedSymlinks, res.NeedDeletes, res.NeedBytes, res.NeedTotalItems = need.Files, need.Directories, need.Symlinks, need.Deleted, need.Bytes, need.TotalItems()
 
 	if haveFcfg && (fcfg.Type == config.FolderTypeReceiveOnly || fcfg.Type == config.FolderTypeReceiveEncrypted) {
 		// Add statistics for things that have changed locally in a receive
 		// only or receive encrypted folder.
-		res["receiveOnlyChangedFiles"] = ro.Files
-		res["receiveOnlyChangedDirectories"] = ro.Directories
-		res["receiveOnlyChangedSymlinks"] = ro.Symlinks
-		res["receiveOnlyChangedDeletes"] = ro.Deleted
-		res["receiveOnlyChangedBytes"] = ro.Bytes
-		res["receiveOnlyTotalItems"] = ro.TotalItems()
+		res.ReceiveOnlyChangedFiles = ro.Files
+		res.ReceiveOnlyChangedDirectories = ro.Directories
+		res.ReceiveOnlyChangedSymlinks = ro.Symlinks
+		res.ReceiveOnlyChangedDeletes = ro.Deleted
+		res.ReceiveOnlyChangedBytes = ro.Bytes
+		res.ReceiveOnlyTotalItems = ro.TotalItems()
 	}
 
-	res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes
+	res.InSyncFiles, res.InSyncBytes = global.Files-need.Files, global.Bytes-need.Bytes
 
-	res["state"], res["stateChanged"], err = c.model.State(folder)
+	res.State, res.StateChanged, err = c.model.State(folder)
 	if err != nil {
-		res["error"] = err.Error()
+		res.Error = err.Error()
 	}
 
-	res["version"] = ourSeq + remoteSeq  // legacy
-	res["sequence"] = ourSeq + remoteSeq // new name
+	res.Version = ourSeq + remoteSeq  // legacy
+	res.Sequence = ourSeq + remoteSeq // new name
 
 	ignorePatterns, _, _ := c.model.CurrentIgnores(folder)
-	res["ignorePatterns"] = false
+	res.IgnorePatterns = false
 	for _, line := range ignorePatterns {
 		if len(line) > 0 && !strings.HasPrefix(line, "//") {
-			res["ignorePatterns"] = true
+			res.IgnorePatterns = true
 			break
 		}
 	}
 
 	err = c.model.WatchError(folder)
 	if err != nil {
-		res["watchError"] = err.Error()
+		res.WatchError = err.Error()
 	}
 
 	return res, nil
@@ -322,6 +372,11 @@ func (c *folderSummaryService) foldersToHandle() []string {
 	return res
 }
 
+type FolderSummaryEventData struct {
+	Folder  string         `json:"folder"`
+	Summary *FolderSummary `json:"summary"`
+}
+
 // sendSummary send the summary events for a single folder
 func (c *folderSummaryService) sendSummary(ctx context.Context, folder string) {
 	// The folder summary contains how many bytes, files etc
@@ -330,9 +385,9 @@ func (c *folderSummaryService) sendSummary(ctx context.Context, folder string) {
 	if err != nil {
 		return
 	}
-	c.evLogger.Log(events.FolderSummary, map[string]interface{}{
-		"folder":  folder,
-		"summary": data,
+	c.evLogger.Log(events.FolderSummary, FolderSummaryEventData{
+		Folder:  folder,
+		Summary: data,
 	})
 
 	for _, devCfg := range c.cfg.Folders()[folder].Devices {
diff --git a/lib/model/mocks/folderSummaryService.go b/lib/model/mocks/folderSummaryService.go
index d4285d8c53d..4dca3505f84 100644
--- a/lib/model/mocks/folderSummaryService.go
+++ b/lib/model/mocks/folderSummaryService.go
@@ -24,17 +24,17 @@ type FolderSummaryService struct {
 	serveReturnsOnCall map[int]struct {
 		result1 error
 	}
-	SummaryStub        func(string) (map[string]interface{}, error)
+	SummaryStub        func(string) (*model.FolderSummary, error)
 	summaryMutex       sync.RWMutex
 	summaryArgsForCall []struct {
 		arg1 string
 	}
 	summaryReturns struct {
-		result1 map[string]interface{}
+		result1 *model.FolderSummary
 		result2 error
 	}
 	summaryReturnsOnCall map[int]struct {
-		result1 map[string]interface{}
+		result1 *model.FolderSummary
 		result2 error
 	}
 	invocations      map[string][][]interface{}
@@ -126,7 +126,7 @@ func (fake *FolderSummaryService) ServeReturnsOnCall(i int, result1 error) {
 	}{result1}
 }
 
-func (fake *FolderSummaryService) Summary(arg1 string) (map[string]interface{}, error) {
+func (fake *FolderSummaryService) Summary(arg1 string) (*model.FolderSummary, error) {
 	fake.summaryMutex.Lock()
 	ret, specificReturn := fake.summaryReturnsOnCall[len(fake.summaryArgsForCall)]
 	fake.summaryArgsForCall = append(fake.summaryArgsForCall, struct {
@@ -151,7 +151,7 @@ func (fake *FolderSummaryService) SummaryCallCount() int {
 	return len(fake.summaryArgsForCall)
 }
 
-func (fake *FolderSummaryService) SummaryCalls(stub func(string) (map[string]interface{}, error)) {
+func (fake *FolderSummaryService) SummaryCalls(stub func(string) (*model.FolderSummary, error)) {
 	fake.summaryMutex.Lock()
 	defer fake.summaryMutex.Unlock()
 	fake.SummaryStub = stub
@@ -164,28 +164,28 @@ func (fake *FolderSummaryService) SummaryArgsForCall(i int) string {
 	return argsForCall.arg1
 }
 
-func (fake *FolderSummaryService) SummaryReturns(result1 map[string]interface{}, result2 error) {
+func (fake *FolderSummaryService) SummaryReturns(result1 *model.FolderSummary, result2 error) {
 	fake.summaryMutex.Lock()
 	defer fake.summaryMutex.Unlock()
 	fake.SummaryStub = nil
 	fake.summaryReturns = struct {
-		result1 map[string]interface{}
+		result1 *model.FolderSummary
 		result2 error
 	}{result1, result2}
 }
 
-func (fake *FolderSummaryService) SummaryReturnsOnCall(i int, result1 map[string]interface{}, result2 error) {
+func (fake *FolderSummaryService) SummaryReturnsOnCall(i int, result1 *model.FolderSummary, result2 error) {
 	fake.summaryMutex.Lock()
 	defer fake.summaryMutex.Unlock()
 	fake.SummaryStub = nil
 	if fake.summaryReturnsOnCall == nil {
 		fake.summaryReturnsOnCall = make(map[int]struct {
-			result1 map[string]interface{}
+			result1 *model.FolderSummary
 			result2 error
 		})
 	}
 	fake.summaryReturnsOnCall[i] = struct {
-		result1 map[string]interface{}
+		result1 *model.FolderSummary
 		result2 error
 	}{result1, result2}
 }
diff --git a/lib/syncthing/verboseservice.go b/lib/syncthing/verboseservice.go
index b23027df314..a78c2ff2004 100644
--- a/lib/syncthing/verboseservice.go
+++ b/lib/syncthing/verboseservice.go
@@ -9,8 +9,10 @@ package syncthing
 import (
 	"context"
 	"fmt"
+	"regexp"
 
 	"github.com/syncthing/syncthing/lib/events"
+	"github.com/syncthing/syncthing/lib/model"
 )
 
 // The verbose logging service subscribes to events and prints these in
@@ -46,6 +48,8 @@ func (s *verboseService) Serve(ctx context.Context) error {
 	}
 }
 
+var folderSummaryRemoveDeprecatedRe = regexp.MustCompile(`(Invalid|IgnorePatterns|StateChanged):\S+\s?`)
+
 func (s *verboseService) formatEvent(ev events.Event) string {
 	switch ev.Type {
 	case events.DownloadProgress, events.LocalIndexUpdated:
@@ -116,15 +120,8 @@ func (s *verboseService) formatEvent(ev events.Event) string {
 		return fmt.Sprintf("Completion for folder %q on device %v is %v%%", data["folder"], data["device"], data["completion"])
 
 	case events.FolderSummary:
-		data := ev.Data.(map[string]interface{})
-		sum := make(map[string]interface{})
-		for k, v := range data["summary"].(map[string]interface{}) {
-			if k == "invalid" || k == "ignorePatterns" || k == "stateChanged" {
-				continue
-			}
-			sum[k] = v
-		}
-		return fmt.Sprintf("Summary for folder %q is %v", data["folder"], sum)
+		data := ev.Data.(model.FolderSummaryEventData)
+		return folderSummaryRemoveDeprecatedRe.ReplaceAllString(fmt.Sprintf("Summary for folder %q is %+v", data.Folder, data.Summary), "")
 
 	case events.FolderScanProgress:
 		data := ev.Data.(map[string]interface{})


Test patch:
diff --git a/lib/api/api_test.go b/lib/api/api_test.go
index 1bfe5df0839..944a010249b 100644
--- a/lib/api/api_test.go
+++ b/lib/api/api_test.go
@@ -35,6 +35,7 @@ import (
 	"github.com/syncthing/syncthing/lib/locations"
 	"github.com/syncthing/syncthing/lib/logger"
 	loggermocks "github.com/syncthing/syncthing/lib/logger/mocks"
+	"github.com/syncthing/syncthing/lib/model"
 	modelmocks "github.com/syncthing/syncthing/lib/model/mocks"
 	"github.com/syncthing/syncthing/lib/protocol"
 	"github.com/syncthing/syncthing/lib/svcutil"
@@ -260,7 +261,7 @@ func TestAPIServiceRequests(t *testing.T) {
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer cancel()
+	t.Cleanup(cancel)
 
 	cases := []httpTestCase{
 		// /rest/db
@@ -298,6 +299,12 @@ func TestAPIServiceRequests(t *testing.T) {
 			Type:   "application/json",
 			Prefix: "null",
 		},
+		{
+			URL:    "/rest/db/status?folder=default",
+			Code:   200,
+			Type:   "application/json",
+			Prefix: "",
+		},
 
 		// /rest/stats
 		{
@@ -466,14 +473,17 @@ func TestAPIServiceRequests(t *testing.T) {
 	}
 
 	for _, tc := range cases {
-		t.Log("Testing", tc.URL, "...")
-		testHTTPRequest(t, baseURL, tc, testAPIKey)
+		t.Run(cases[0].URL, func(t *testing.T) {
+			testHTTPRequest(t, baseURL, tc, testAPIKey)
+		})
 	}
 }
 
 // testHTTPRequest tries the given test case, comparing the result code,
 // content type, and result prefix.
 func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase, apikey string) {
+	t.Parallel()
+
 	timeout := time.Second
 	if tc.Timeout > 0 {
 		timeout = tc.Timeout
@@ -608,7 +618,7 @@ func startHTTP(cfg config.Wrapper) (string, context.CancelFunc, error) {
 	}
 	addrChan := make(chan string)
 	mockedSummary := &modelmocks.FolderSummaryService{}
-	mockedSummary.SummaryReturns(map[string]interface{}{"mocked": true}, nil)
+	mockedSummary.SummaryReturns(new(model.FolderSummary), nil)
 
 	// Instantiate the API service
 	urService := ur.New(cfg, m, connections, false)