all: Support multiple device connections (fixes #141) (#8918)

This adds the ability to have multiple concurrent connections to a single device. This is primarily useful when the network has multiple physical links for aggregated bandwidth. A single connection will never see a higher rate than a single link can give, but multiple connections are load-balanced over multiple links.

It is also incidentally useful for older multi-core CPUs, where bandwidth could be limited by the TLS performance of a single CPU core -- using multiple connections achieves concurrency in the required crypto calculations...

Co-authored-by: Simon Frei <freisim93@gmail.com>
Co-authored-by: tomasz1986 <twilczynski@naver.com>
Co-authored-by: bt90 <btom1990@googlemail.com>
This commit is contained in:
Jakob Borg
2023-09-06 12:52:01 +02:00
committed by GitHub
co-authored by Simon Frei tomasz1986 bt90
parent 38bbdebffa
commit c6334e61aa
41 changed files with 1640 additions and 933 deletions
+1
View File
@@ -34,6 +34,7 @@ func newFakeConnection(id protocol.DeviceID, model Model) *fakeConnection {
return f.fileData[name], nil
})
f.DeviceIDReturns(id)
f.ConnectionIDReturns(rand.String(16))
f.CloseCalls(func(err error) {
f.closeOnce.Do(func() {
close(f.closed)
+1 -1
View File
@@ -530,7 +530,7 @@ func setupROFolder(t *testing.T) (*testModel, *receiveOnlyFolder, context.Cancel
cfg.Folders = []config.FolderConfiguration{fcfg}
replace(t, w, cfg)
m := newModel(t, w, myID, "syncthing", "dev", nil)
m := newModel(t, w, myID, nil)
m.ServeBackground()
<-m.started
must(t, m.ScanFolder("ro"))
+1 -1
View File
@@ -507,7 +507,7 @@ nextFile:
devices := snap.Availability(fileName)
for _, dev := range devices {
if _, ok := f.model.Connection(dev); ok {
if f.model.ConnectedTo(dev) {
// Handle the file normally, by copying and pulling, etc.
f.handleFile(fi, snap, copyChan)
continue nextFile
+48 -192
View File
@@ -76,18 +76,16 @@ type Model struct {
result1 model.FolderCompletion
result2 error
}
ConnectionStub func(protocol.DeviceID) (protocol.Connection, bool)
connectionMutex sync.RWMutex
connectionArgsForCall []struct {
ConnectedToStub func(protocol.DeviceID) bool
connectedToMutex sync.RWMutex
connectedToArgsForCall []struct {
arg1 protocol.DeviceID
}
connectionReturns struct {
result1 protocol.Connection
result2 bool
connectedToReturns struct {
result1 bool
}
connectionReturnsOnCall map[int]struct {
result1 protocol.Connection
result2 bool
connectedToReturnsOnCall map[int]struct {
result1 bool
}
ConnectionStatsStub func() map[string]interface{}
connectionStatsMutex sync.RWMutex
@@ -262,17 +260,6 @@ type Model struct {
result1 map[string][]versioner.FileVersion
result2 error
}
GetHelloStub func(protocol.DeviceID) protocol.HelloIntf
getHelloMutex sync.RWMutex
getHelloArgsForCall []struct {
arg1 protocol.DeviceID
}
getHelloReturns struct {
result1 protocol.HelloIntf
}
getHelloReturnsOnCall map[int]struct {
result1 protocol.HelloIntf
}
GetMtimeMappingStub func(string, string) (fs.MtimeMapping, error)
getMtimeMappingMutex sync.RWMutex
getMtimeMappingArgsForCall []struct {
@@ -378,16 +365,6 @@ type Model struct {
result3 []db.FileInfoTruncated
result4 error
}
NumConnectionsStub func() int
numConnectionsMutex sync.RWMutex
numConnectionsArgsForCall []struct {
}
numConnectionsReturns struct {
result1 int
}
numConnectionsReturnsOnCall map[int]struct {
result1 int
}
OnHelloStub func(protocol.DeviceID, net.Addr, protocol.Hello) error
onHelloMutex sync.RWMutex
onHelloArgsForCall []struct {
@@ -888,68 +865,65 @@ func (fake *Model) CompletionReturnsOnCall(i int, result1 model.FolderCompletion
}{result1, result2}
}
func (fake *Model) Connection(arg1 protocol.DeviceID) (protocol.Connection, bool) {
fake.connectionMutex.Lock()
ret, specificReturn := fake.connectionReturnsOnCall[len(fake.connectionArgsForCall)]
fake.connectionArgsForCall = append(fake.connectionArgsForCall, struct {
func (fake *Model) ConnectedTo(arg1 protocol.DeviceID) bool {
fake.connectedToMutex.Lock()
ret, specificReturn := fake.connectedToReturnsOnCall[len(fake.connectedToArgsForCall)]
fake.connectedToArgsForCall = append(fake.connectedToArgsForCall, struct {
arg1 protocol.DeviceID
}{arg1})
stub := fake.ConnectionStub
fakeReturns := fake.connectionReturns
fake.recordInvocation("Connection", []interface{}{arg1})
fake.connectionMutex.Unlock()
stub := fake.ConnectedToStub
fakeReturns := fake.connectedToReturns
fake.recordInvocation("ConnectedTo", []interface{}{arg1})
fake.connectedToMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1, ret.result2
return ret.result1
}
return fakeReturns.result1, fakeReturns.result2
return fakeReturns.result1
}
func (fake *Model) ConnectionCallCount() int {
fake.connectionMutex.RLock()
defer fake.connectionMutex.RUnlock()
return len(fake.connectionArgsForCall)
func (fake *Model) ConnectedToCallCount() int {
fake.connectedToMutex.RLock()
defer fake.connectedToMutex.RUnlock()
return len(fake.connectedToArgsForCall)
}
func (fake *Model) ConnectionCalls(stub func(protocol.DeviceID) (protocol.Connection, bool)) {
fake.connectionMutex.Lock()
defer fake.connectionMutex.Unlock()
fake.ConnectionStub = stub
func (fake *Model) ConnectedToCalls(stub func(protocol.DeviceID) bool) {
fake.connectedToMutex.Lock()
defer fake.connectedToMutex.Unlock()
fake.ConnectedToStub = stub
}
func (fake *Model) ConnectionArgsForCall(i int) protocol.DeviceID {
fake.connectionMutex.RLock()
defer fake.connectionMutex.RUnlock()
argsForCall := fake.connectionArgsForCall[i]
func (fake *Model) ConnectedToArgsForCall(i int) protocol.DeviceID {
fake.connectedToMutex.RLock()
defer fake.connectedToMutex.RUnlock()
argsForCall := fake.connectedToArgsForCall[i]
return argsForCall.arg1
}
func (fake *Model) ConnectionReturns(result1 protocol.Connection, result2 bool) {
fake.connectionMutex.Lock()
defer fake.connectionMutex.Unlock()
fake.ConnectionStub = nil
fake.connectionReturns = struct {
result1 protocol.Connection
result2 bool
}{result1, result2}
func (fake *Model) ConnectedToReturns(result1 bool) {
fake.connectedToMutex.Lock()
defer fake.connectedToMutex.Unlock()
fake.ConnectedToStub = nil
fake.connectedToReturns = struct {
result1 bool
}{result1}
}
func (fake *Model) ConnectionReturnsOnCall(i int, result1 protocol.Connection, result2 bool) {
fake.connectionMutex.Lock()
defer fake.connectionMutex.Unlock()
fake.ConnectionStub = nil
if fake.connectionReturnsOnCall == nil {
fake.connectionReturnsOnCall = make(map[int]struct {
result1 protocol.Connection
result2 bool
func (fake *Model) ConnectedToReturnsOnCall(i int, result1 bool) {
fake.connectedToMutex.Lock()
defer fake.connectedToMutex.Unlock()
fake.ConnectedToStub = nil
if fake.connectedToReturnsOnCall == nil {
fake.connectedToReturnsOnCall = make(map[int]struct {
result1 bool
})
}
fake.connectionReturnsOnCall[i] = struct {
result1 protocol.Connection
result2 bool
}{result1, result2}
fake.connectedToReturnsOnCall[i] = struct {
result1 bool
}{result1}
}
func (fake *Model) ConnectionStats() map[string]interface{} {
@@ -1797,67 +1771,6 @@ func (fake *Model) GetFolderVersionsReturnsOnCall(i int, result1 map[string][]ve
}{result1, result2}
}
func (fake *Model) GetHello(arg1 protocol.DeviceID) protocol.HelloIntf {
fake.getHelloMutex.Lock()
ret, specificReturn := fake.getHelloReturnsOnCall[len(fake.getHelloArgsForCall)]
fake.getHelloArgsForCall = append(fake.getHelloArgsForCall, struct {
arg1 protocol.DeviceID
}{arg1})
stub := fake.GetHelloStub
fakeReturns := fake.getHelloReturns
fake.recordInvocation("GetHello", []interface{}{arg1})
fake.getHelloMutex.Unlock()
if stub != nil {
return stub(arg1)
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) GetHelloCallCount() int {
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
return len(fake.getHelloArgsForCall)
}
func (fake *Model) GetHelloCalls(stub func(protocol.DeviceID) protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = stub
}
func (fake *Model) GetHelloArgsForCall(i int) protocol.DeviceID {
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
argsForCall := fake.getHelloArgsForCall[i]
return argsForCall.arg1
}
func (fake *Model) GetHelloReturns(result1 protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = nil
fake.getHelloReturns = struct {
result1 protocol.HelloIntf
}{result1}
}
func (fake *Model) GetHelloReturnsOnCall(i int, result1 protocol.HelloIntf) {
fake.getHelloMutex.Lock()
defer fake.getHelloMutex.Unlock()
fake.GetHelloStub = nil
if fake.getHelloReturnsOnCall == nil {
fake.getHelloReturnsOnCall = make(map[int]struct {
result1 protocol.HelloIntf
})
}
fake.getHelloReturnsOnCall[i] = struct {
result1 protocol.HelloIntf
}{result1}
}
func (fake *Model) GetMtimeMapping(arg1 string, arg2 string) (fs.MtimeMapping, error) {
fake.getMtimeMappingMutex.Lock()
ret, specificReturn := fake.getMtimeMappingReturnsOnCall[len(fake.getMtimeMappingArgsForCall)]
@@ -2331,59 +2244,6 @@ func (fake *Model) NeedFolderFilesReturnsOnCall(i int, result1 []db.FileInfoTrun
}{result1, result2, result3, result4}
}
func (fake *Model) NumConnections() int {
fake.numConnectionsMutex.Lock()
ret, specificReturn := fake.numConnectionsReturnsOnCall[len(fake.numConnectionsArgsForCall)]
fake.numConnectionsArgsForCall = append(fake.numConnectionsArgsForCall, struct {
}{})
stub := fake.NumConnectionsStub
fakeReturns := fake.numConnectionsReturns
fake.recordInvocation("NumConnections", []interface{}{})
fake.numConnectionsMutex.Unlock()
if stub != nil {
return stub()
}
if specificReturn {
return ret.result1
}
return fakeReturns.result1
}
func (fake *Model) NumConnectionsCallCount() int {
fake.numConnectionsMutex.RLock()
defer fake.numConnectionsMutex.RUnlock()
return len(fake.numConnectionsArgsForCall)
}
func (fake *Model) NumConnectionsCalls(stub func() int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = stub
}
func (fake *Model) NumConnectionsReturns(result1 int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = nil
fake.numConnectionsReturns = struct {
result1 int
}{result1}
}
func (fake *Model) NumConnectionsReturnsOnCall(i int, result1 int) {
fake.numConnectionsMutex.Lock()
defer fake.numConnectionsMutex.Unlock()
fake.NumConnectionsStub = nil
if fake.numConnectionsReturnsOnCall == nil {
fake.numConnectionsReturnsOnCall = make(map[int]struct {
result1 int
})
}
fake.numConnectionsReturnsOnCall[i] = struct {
result1 int
}{result1}
}
func (fake *Model) OnHello(arg1 protocol.DeviceID, arg2 net.Addr, arg3 protocol.Hello) error {
fake.onHelloMutex.Lock()
ret, specificReturn := fake.onHelloReturnsOnCall[len(fake.onHelloArgsForCall)]
@@ -3419,8 +3279,8 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.clusterConfigMutex.RUnlock()
fake.completionMutex.RLock()
defer fake.completionMutex.RUnlock()
fake.connectionMutex.RLock()
defer fake.connectionMutex.RUnlock()
fake.connectedToMutex.RLock()
defer fake.connectedToMutex.RUnlock()
fake.connectionStatsMutex.RLock()
defer fake.connectionStatsMutex.RUnlock()
fake.currentFolderFileMutex.RLock()
@@ -3449,8 +3309,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.folderStatisticsMutex.RUnlock()
fake.getFolderVersionsMutex.RLock()
defer fake.getFolderVersionsMutex.RUnlock()
fake.getHelloMutex.RLock()
defer fake.getHelloMutex.RUnlock()
fake.getMtimeMappingMutex.RLock()
defer fake.getMtimeMappingMutex.RUnlock()
fake.globalDirectoryTreeMutex.RLock()
@@ -3465,8 +3323,6 @@ func (fake *Model) Invocations() map[string][][]interface{} {
defer fake.localChangedFolderFilesMutex.RUnlock()
fake.needFolderFilesMutex.RLock()
defer fake.needFolderFilesMutex.RUnlock()
fake.numConnectionsMutex.RLock()
defer fake.numConnectionsMutex.RUnlock()
fake.onHelloMutex.RLock()
defer fake.onHelloMutex.RUnlock()
fake.overrideMutex.RLock()
+377 -198
View File
@@ -37,6 +37,7 @@ import (
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/scanner"
"github.com/syncthing/syncthing/lib/semaphore"
"github.com/syncthing/syncthing/lib/stats"
@@ -108,6 +109,7 @@ type Model interface {
DeviceStatistics() (map[protocol.DeviceID]stats.DeviceStatistics, error)
FolderStatistics() (map[string]stats.FolderStatistics, error)
UsageReportingStats(report *contract.Report, version int, preview bool)
ConnectedTo(remoteID protocol.DeviceID) bool
PendingDevices() (map[protocol.DeviceID]db.ObservedDevice, error)
PendingFolders(device protocol.DeviceID) (map[string]db.PendingFolder, error)
@@ -124,8 +126,6 @@ type model struct {
// constructor parameters
cfg config.Wrapper
id protocol.DeviceID
clientName string
clientVersion string
db *db.Lowlevel
protectedFiles []string
evLogger events.Logger
@@ -143,6 +143,7 @@ type model struct {
fatalChan chan error
started chan struct{}
keyGen *protocol.KeyGenerator
promotionTimer *time.Timer
// fields protected by fmut
fmut sync.RWMutex
@@ -158,9 +159,11 @@ type model struct {
// fields protected by pmut
pmut sync.RWMutex
conn map[protocol.DeviceID]protocol.Connection
connections map[string]protocol.Connection // connection ID -> connection
deviceConnIDs map[protocol.DeviceID][]string // device -> connection IDs (invariant: if the key exists, the value is len >= 1, with the primary connection at the start of the slice)
promotedConnID map[protocol.DeviceID]string // device -> latest promoted connection ID
connRequestLimiters map[protocol.DeviceID]*semaphore.Semaphore
closed map[protocol.DeviceID]chan struct{}
closed map[string]chan struct{} // connection ID -> closed channel
helloMessages map[protocol.DeviceID]protocol.Hello
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
remoteFolderStates map[protocol.DeviceID]map[string]remoteFolderState // deviceID -> folders
@@ -179,13 +182,11 @@ var folderFactories = make(map[config.FolderType]folderFactory)
var (
errDeviceUnknown = errors.New("unknown device")
errDevicePaused = errors.New("device is paused")
errDeviceRemoved = errors.New("device has been removed")
ErrFolderPaused = errors.New("folder is paused")
ErrFolderNotRunning = errors.New("folder is not running")
ErrFolderMissing = errors.New("no such folder")
errNoVersioner = errors.New("folder has no versioner")
// errors about why a connection is closed
errReplacingConnection = errors.New("replacing connection")
errStopped = errors.New("Syncthing is being stopped")
errEncryptionInvConfigLocal = errors.New("can't encrypt outgoing data because local data is encrypted (folder-type receive-encrypted)")
errEncryptionInvConfigRemote = errors.New("remote has encrypted data and encrypts that data for us - this is impossible")
@@ -203,7 +204,7 @@ var (
// NewModel creates and starts a new model. The model starts in read-only mode,
// where it sends index information to connected peers and responds to requests
// for file data without altering the local folder in any way.
func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model {
func NewModel(cfg config.Wrapper, id protocol.DeviceID, ldb *db.Lowlevel, protectedFiles []string, evLogger events.Logger, keyGen *protocol.KeyGenerator) Model {
spec := svcutil.SpecWithDebugLogger(l)
m := &model{
Supervisor: suture.New("model", spec),
@@ -211,8 +212,6 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
// constructor parameters
cfg: cfg,
id: id,
clientName: clientName,
clientVersion: clientVersion,
db: ldb,
protectedFiles: protectedFiles,
evLogger: evLogger,
@@ -226,6 +225,7 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
fatalChan: make(chan error),
started: make(chan struct{}),
keyGen: keyGen,
promotionTimer: time.NewTimer(0),
// fields protected by fmut
fmut: sync.NewRWMutex(),
@@ -240,16 +240,19 @@ func NewModel(cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersio
// fields protected by pmut
pmut: sync.NewRWMutex(),
conn: make(map[protocol.DeviceID]protocol.Connection),
connections: make(map[string]protocol.Connection),
deviceConnIDs: make(map[protocol.DeviceID][]string),
promotedConnID: make(map[protocol.DeviceID]string),
connRequestLimiters: make(map[protocol.DeviceID]*semaphore.Semaphore),
closed: make(map[protocol.DeviceID]chan struct{}),
closed: make(map[string]chan struct{}),
helloMessages: make(map[protocol.DeviceID]protocol.Hello),
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
remoteFolderStates: make(map[protocol.DeviceID]map[string]remoteFolderState),
indexHandlers: newServiceMap[protocol.DeviceID, *indexHandlerRegistry](evLogger),
}
for devID := range cfg.Devices() {
for devID, cfg := range cfg.Devices() {
m.deviceStatRefs[devID] = stats.NewDeviceStatisticsReference(m.db, devID)
m.setConnRequestLimitersPLocked(cfg)
}
m.Add(m.folderRunners)
m.Add(m.progressEmitter)
@@ -272,11 +275,18 @@ func (m *model) serve(ctx context.Context) error {
close(m.started)
select {
case <-ctx.Done():
return ctx.Err()
case err := <-m.fatalChan:
return svcutil.AsFatalErr(err, svcutil.ExitError)
for {
select {
case <-ctx.Done():
l.Debugln(m, "context closed, stopping", ctx.Err())
return ctx.Err()
case err := <-m.fatalChan:
l.Debugln(m, "fatal error, stopping", err)
return svcutil.AsFatalErr(err, svcutil.ExitError)
case <-m.promotionTimer.C:
l.Debugln("promotion timer fired")
m.promoteConnections()
}
}
}
@@ -303,9 +313,9 @@ func (m *model) initFolders(cfg config.Configuration) error {
func (m *model) closeAllConnectionsAndWait() {
m.pmut.RLock()
closed := make([]chan struct{}, 0, len(m.conn))
for id, conn := range m.conn {
closed = append(closed, m.closed[id])
closed := make([]chan struct{}, 0, len(m.connections))
for connID, conn := range m.connections {
closed = append(closed, m.closed[connID])
go conn.Close(errStopped)
}
m.pmut.RUnlock()
@@ -635,7 +645,7 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
// Transport stats
m.pmut.RLock()
for _, conn := range m.conn {
for _, conn := range m.connections {
report.TransportStats[conn.Transport()]++
}
m.pmut.RUnlock()
@@ -699,22 +709,28 @@ func (m *model) UsageReportingStats(report *contract.Report, version int, previe
}
}
type ConnectionInfo struct {
protocol.Statistics
type ConnectionStats struct {
protocol.Statistics // Total for primary + secondaries
Connected bool `json:"connected"`
Paused bool `json:"paused"`
Address string `json:"address"`
ClientVersion string `json:"clientVersion"`
Type string `json:"type"`
IsLocal bool `json:"isLocal"`
Crypto string `json:"crypto"`
Address string `json:"address"` // mirror values from Primary, for compatibility with <1.24.0
Type string `json:"type"` // mirror values from Primary, for compatibility with <1.24.0
IsLocal bool `json:"isLocal"` // mirror values from Primary, for compatibility with <1.24.0
Crypto string `json:"crypto"` // mirror values from Primary, for compatibility with <1.24.0
Primary ConnectionInfo `json:"primary,omitempty"`
Secondary []ConnectionInfo `json:"secondary,omitempty"`
}
// NumConnections returns the current number of active connected devices.
func (m *model) NumConnections() int {
m.pmut.RLock()
defer m.pmut.RUnlock()
return len(m.conn)
type ConnectionInfo struct {
protocol.Statistics
Address string `json:"address"`
Type string `json:"type"`
IsLocal bool `json:"isLocal"`
Crypto string `json:"crypto"`
}
// ConnectionStats returns a map with connection statistics for each device.
@@ -724,29 +740,59 @@ func (m *model) ConnectionStats() map[string]interface{} {
res := make(map[string]interface{})
devs := m.cfg.Devices()
conns := make(map[string]ConnectionInfo, len(devs))
conns := make(map[string]ConnectionStats, len(devs))
for device, deviceCfg := range devs {
if device == m.id {
continue
}
hello := m.helloMessages[device]
versionString := hello.ClientVersion
if hello.ClientName != "syncthing" {
versionString = hello.ClientName + " " + hello.ClientVersion
}
ci := ConnectionInfo{
ClientVersion: strings.TrimSpace(versionString),
connIDs, ok := m.deviceConnIDs[device]
cs := ConnectionStats{
Connected: ok,
Paused: deviceCfg.Paused,
ClientVersion: strings.TrimSpace(versionString),
}
if conn, ok := m.conn[device]; ok {
ci.Type = conn.Type()
ci.IsLocal = conn.IsLocal()
ci.Crypto = conn.Crypto()
ci.Connected = ok
ci.Statistics = conn.Statistics()
if addr := conn.RemoteAddr(); addr != nil {
ci.Address = addr.String()
if ok {
conn := m.connections[connIDs[0]]
cs.Primary.Type = conn.Type()
cs.Primary.IsLocal = conn.IsLocal()
cs.Primary.Crypto = conn.Crypto()
cs.Primary.Statistics = conn.Statistics()
cs.Primary.Address = conn.RemoteAddr().String()
cs.Type = cs.Primary.Type
cs.IsLocal = cs.Primary.IsLocal
cs.Crypto = cs.Primary.Crypto
cs.Address = cs.Primary.Address
cs.Statistics = cs.Primary.Statistics
for _, connID := range connIDs[1:] {
conn = m.connections[connID]
sec := ConnectionInfo{
Statistics: conn.Statistics(),
Address: conn.RemoteAddr().String(),
Type: conn.Type(),
IsLocal: conn.IsLocal(),
Crypto: conn.Crypto(),
}
if sec.At.After(cs.At) {
cs.At = sec.At
}
if sec.StartedAt.Before(cs.StartedAt) {
cs.StartedAt = sec.StartedAt
}
cs.InBytesTotal += sec.InBytesTotal
cs.OutBytesTotal += sec.OutBytesTotal
cs.Secondary = append(cs.Secondary, sec)
}
}
conns[device.String()] = ci
conns[device.String()] = cs
}
res["connections"] = conns
@@ -1138,17 +1184,16 @@ func (m *model) handleIndex(conn protocol.Connection, folder string, fs []protoc
}
m.pmut.RLock()
indexHandler, ok := m.indexHandlers.Get(deviceID)
indexHandler, ok := m.getIndexHandlerPRLocked(conn)
m.pmut.RUnlock()
if !ok {
// This should be impossible, as an index handler always exists for an
// open connection, and this method can't be called on a closed
// connection
// This should be impossible, as an index handler is registered when
// we send a cluster config, and that is what triggers index
// sending.
m.evLogger.Log(events.Failure, "index sender does not exist for connection on which indexes were received")
l.Debugf("%v for folder (ID %q) sent from device %q: missing index handler", op, folder, deviceID)
return fmt.Errorf("index handler missing: %s", folder)
return fmt.Errorf("%s: %w", folder, ErrFolderNotRunning)
}
return indexHandler.ReceiveIndex(folder, fs, update, op)
}
@@ -1161,24 +1206,26 @@ type ClusterConfigReceivedEventData struct {
}
func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfig) error {
deviceID := conn.DeviceID()
if cm.Secondary {
// No handling of secondary connection ClusterConfigs; they merely
// indicate the connection is ready to start.
l.Debugf("Skipping secondary ClusterConfig from %v at %s", deviceID.Short(), conn)
return nil
}
// Check the peer device's announced folders against our own. Emits events
// for folders that we don't expect (unknown or not shared).
// Also, collect a list of folders we do share, and if he's interested in
// temporary indexes, subscribe the connection.
deviceID := conn.DeviceID()
l.Debugf("Handling ClusterConfig from %v", deviceID.Short())
m.pmut.RLock()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
m.pmut.RUnlock()
if !ok {
panic("bug: ClusterConfig called on closed or nonexistent connection")
}
l.Debugf("Handling ClusterConfig from %v at %s", deviceID.Short(), conn)
indexHandlerRegistry := m.ensureIndexHandler(conn)
deviceCfg, ok := m.cfg.Device(deviceID)
if !ok {
l.Debugln("Device disappeared from config while processing cluster-config")
l.Debugf("Device %s disappeared from config while processing cluster-config", deviceID.Short())
return errDeviceUnknown
}
@@ -1198,11 +1245,11 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
}
}
if info.remote.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID, folder.Description())
l.Infof("Device %v sent cluster-config without the device info for the remote on folder %v", deviceID.Short(), folder.Description())
return errMissingRemoteInClusterConfig
}
if info.local.ID == protocol.EmptyDeviceID {
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID, folder.Description())
l.Infof("Device %v sent cluster-config without the device info for us locally on folder %v", deviceID.Short(), folder.Description())
return errMissingLocalInClusterConfig
}
ccDeviceInfos[folder.ID] = info
@@ -1260,12 +1307,16 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
})
if len(tempIndexFolders) > 0 {
var connOK bool
var conn protocol.Connection
m.pmut.RLock()
conn, ok := m.conn[deviceID]
if connIDs, connIDOK := m.deviceConnIDs[deviceID]; connIDOK {
conn, connOK = m.connections[connIDs[0]]
}
m.pmut.RUnlock()
// In case we've got ClusterConfig, and the connection disappeared
// from infront of our nose.
if ok {
if connOK {
m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders)
}
}
@@ -1291,6 +1342,57 @@ func (m *model) ClusterConfig(conn protocol.Connection, cm protocol.ClusterConfi
return nil
}
func (m *model) ensureIndexHandler(conn protocol.Connection) *indexHandlerRegistry {
deviceID := conn.DeviceID()
connID := conn.ConnectionID()
m.pmut.Lock()
defer m.pmut.Unlock()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
if ok && indexHandlerRegistry.conn.ConnectionID() == connID {
// This is an existing and proper index handler for this connection.
return indexHandlerRegistry
}
if ok {
// A handler exists, but it's for another connection than the one we
// now got a ClusterConfig on. This should be unusual as it means
// the other side has decided to start using a new primary
// connection but we haven't seen it close yet. Ideally it will
// close shortly by itself...
l.Infof("Abandoning old index handler for %s (%s) in favour of %s", deviceID.Short(), indexHandlerRegistry.conn.ConnectionID(), connID)
m.indexHandlers.RemoveAndWait(deviceID, 0)
}
// Create a new index handler for this device.
indexHandlerRegistry = newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], m.evLogger)
for id, fcfg := range m.folderCfgs {
l.Debugln("Registering folder", id, "for", deviceID.Short())
runner, _ := m.folderRunners.Get(id)
indexHandlerRegistry.RegisterFolderState(fcfg, m.folderFiles[id], runner)
}
m.indexHandlers.Add(deviceID, indexHandlerRegistry)
return indexHandlerRegistry
}
func (m *model) getIndexHandlerPRLocked(conn protocol.Connection) (*indexHandlerRegistry, bool) {
// Reads from index handlers, which requires pmut to be read locked
deviceID := conn.DeviceID()
connID := conn.ConnectionID()
indexHandlerRegistry, ok := m.indexHandlers.Get(deviceID)
if ok && indexHandlerRegistry.conn.ConnectionID() == connID {
// This is an existing and proper index handler for this connection.
return indexHandlerRegistry, true
}
// There is no index handler, or it's not registered for this connection.
return nil, false
}
func (m *model) ccHandleFolders(folders []protocol.Folder, deviceCfg config.DeviceConfiguration, ccDeviceInfos map[string]*clusterConfigDeviceInfo, indexHandlers *indexHandlerRegistry) ([]string, map[string]remoteFolderState, error) {
var folderDevice config.FolderDeviceConfiguration
tempIndexFolders := make([]string, 0, len(folders))
@@ -1539,8 +1641,8 @@ func (m *model) sendClusterConfig(ids []protocol.DeviceID) {
ccConns := make([]protocol.Connection, 0, len(ids))
m.pmut.RLock()
for _, id := range ids {
if conn, ok := m.conn[id]; ok {
ccConns = append(ccConns, conn)
if connIDs, ok := m.deviceConnIDs[id]; ok {
ccConns = append(ccConns, m.connections[connIDs[0]])
}
}
m.pmut.RUnlock()
@@ -1777,33 +1879,62 @@ func (m *model) introduceDevice(device protocol.Device, introducerCfg config.Dev
// Closed is called when a connection has been closed
func (m *model) Closed(conn protocol.Connection, err error) {
device := conn.DeviceID()
connID := conn.ConnectionID()
deviceID := conn.DeviceID()
m.pmut.Lock()
conn, ok := m.conn[device]
conn, ok := m.connections[connID]
if !ok {
m.pmut.Unlock()
return
}
delete(m.conn, device)
delete(m.connRequestLimiters, device)
delete(m.helloMessages, device)
delete(m.deviceDownloads, device)
delete(m.remoteFolderStates, device)
closed := m.closed[device]
delete(m.closed, device)
wait := m.indexHandlers.RemoveAndWaitChan(device, 0)
closed := m.closed[connID]
delete(m.closed, connID)
delete(m.connections, connID)
removedIsPrimary := m.promotedConnID[deviceID] == connID
remainingConns := without(m.deviceConnIDs[deviceID], connID)
var wait <-chan error
if removedIsPrimary {
m.progressEmitter.temporaryIndexUnsubscribe(conn)
if idxh, ok := m.indexHandlers.Get(deviceID); ok && idxh.conn.ConnectionID() == connID {
wait = m.indexHandlers.RemoveAndWaitChan(deviceID, 0)
}
m.scheduleConnectionPromotion()
}
if len(remainingConns) == 0 {
// All device connections closed
delete(m.deviceConnIDs, deviceID)
delete(m.promotedConnID, deviceID)
delete(m.connRequestLimiters, deviceID)
delete(m.helloMessages, deviceID)
delete(m.remoteFolderStates, deviceID)
delete(m.deviceDownloads, deviceID)
} else {
// Some connections remain
m.deviceConnIDs[deviceID] = remainingConns
}
m.pmut.Unlock()
<-wait
if wait != nil {
<-wait
}
m.progressEmitter.temporaryIndexUnsubscribe(conn)
m.deviceDidClose(device, time.Since(conn.EstablishedAt()))
m.fmut.RLock()
m.deviceDidCloseFRLocked(deviceID, time.Since(conn.EstablishedAt()))
m.fmut.RUnlock()
l.Infof("Connection to %s at %s closed: %v", device, conn, err)
m.evLogger.Log(events.DeviceDisconnected, map[string]string{
"id": device.String(),
"error": err.Error(),
})
k := map[bool]string{false: "secondary", true: "primary"}[removedIsPrimary]
l.Infof("Lost %s connection to %s at %s: %v (%d remain)", k, deviceID.Short(), conn, err, len(remainingConns))
if len(remainingConns) == 0 {
l.Infof("Connection to %s at %s closed: %v", deviceID.Short(), conn, err)
m.evLogger.Log(events.DeviceDisconnected, map[string]string{
"id": deviceID.String(),
"error": err.Error(),
})
}
close(closed)
}
@@ -1852,36 +1983,36 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if !ok {
// The folder might be already unpaused in the config, but not yet
// in the model.
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID, name, folder)
l.Debugf("Request from %s for file %s in unstarted folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric
}
if !folderCfg.SharedWith(deviceID) {
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric
}
if folderCfg.Paused {
l.Debugf("Request from %s for file %s in paused folder %q", deviceID, name, folder)
l.Debugf("Request from %s for file %s in paused folder %q", deviceID.Short(), name, folder)
return nil, protocol.ErrGeneric
}
// Make sure the path is valid and in canonical form
if name, err = fs.Canonicalize(name); err != nil {
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID, folder, name)
l.Debugf("Request from %s in folder %q for invalid filename %s", deviceID.Short(), folder, name)
return nil, protocol.ErrGeneric
}
if deviceID != protocol.LocalDeviceID {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID, folder, name, offset, size, fromTemporary)
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d t=%v", m, deviceID.Short(), folder, name, offset, size, fromTemporary)
}
if fs.IsInternal(name) {
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrInvalid
}
if folderIgnores.Match(name).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrInvalid
}
@@ -1908,7 +2039,7 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
folderFs := folderCfg.Filesystem(nil)
if err := osutil.TraversesSymlink(folderFs, filepath.Dir(name)); err != nil {
l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) traversal check: %s - %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
}
@@ -1920,7 +2051,7 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if info, err := folderFs.Lstat(tempFn); err != nil || !info.IsRegular() {
// Reject reads for anything that doesn't exist or is something
// other than a regular file.
l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) failed stating temp file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
}
_, err := readOffsetIntoBuf(folderFs, tempFn, offset, res.data)
@@ -1934,13 +2065,13 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
if info, err := folderFs.Lstat(name); err != nil || !info.IsRegular() {
// Reject reads for anything that doesn't exist or is something
// other than a regular file.
l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) failed stating file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
}
n, err := readOffsetIntoBuf(folderFs, name, offset, res.data)
if fs.IsNotExist(err) {
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) file doesn't exist: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
} else if err == io.EOF {
// Read beyond end of file. This might indicate a problem, or it
@@ -1949,13 +2080,13 @@ func (m *model) Request(conn protocol.Connection, folder, name string, _, size i
// next step take care of it, by only hashing the part we actually
// managed to read.
} else if err != nil {
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) failed reading file (%v): %s: %q / %q o=%d s=%d", m, err, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrGeneric
}
if folderCfg.Type != config.FolderTypeReceiveEncrypted && len(hash) > 0 && !scanner.Validate(res.data[:n], hash, weakHash) {
m.recheckFile(deviceID, folder, name, offset, hash, weakHash)
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, size)
l.Debugf("%v REQ(in) failed validating data: %s: %q / %q o=%d s=%d", m, deviceID.Short(), folder, name, offset, size)
return nil, protocol.ErrNoSuchFile
}
@@ -2074,15 +2205,12 @@ func (m *model) GetMtimeMapping(folder string, file string) (fs.MtimeMapping, er
return fs.GetMtimeMapping(fcfg.Filesystem(ffs), file)
}
// Connection returns the current connection for device, and a boolean whether a connection was found.
func (m *model) Connection(deviceID protocol.DeviceID) (protocol.Connection, bool) {
// Connection returns if we are connected to the given device.
func (m *model) ConnectedTo(deviceID protocol.DeviceID) bool {
m.pmut.RLock()
cn, ok := m.conn[deviceID]
_, ok := m.deviceConnIDs[deviceID]
m.pmut.RUnlock()
if ok {
m.deviceWasSeen(deviceID)
}
return cn, ok
return ok
}
// LoadIgnores loads or refreshes the ignore patterns from disk, if the
@@ -2200,74 +2328,29 @@ func (m *model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
return nil
}
// GetHello is called when we are about to connect to some remote device.
func (m *model) GetHello(id protocol.DeviceID) protocol.HelloIntf {
name := ""
if _, ok := m.cfg.Device(id); ok {
// Set our name (from the config of our device ID) only if we already know about the other side device ID.
if myCfg, ok := m.cfg.Device(m.id); ok {
name = myCfg.Name
}
}
return &protocol.Hello{
DeviceName: name,
ClientName: m.clientName,
ClientVersion: m.clientVersion,
}
}
// AddConnection adds a new peer connection to the model. An initial index will
// be sent to the connected peer, thereafter index updates whenever the local
// folder changes.
func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
deviceID := conn.DeviceID()
device, ok := m.cfg.Device(deviceID)
deviceCfg, ok := m.cfg.Device(deviceID)
if !ok {
l.Infoln("Trying to add connection to unknown device")
return
}
// The slightly unusual locking sequence here is because we must acquire
// fmut before pmut. (The locks can be *released* in any order.)
m.fmut.RLock()
m.pmut.Lock()
if oldConn, ok := m.conn[deviceID]; ok {
l.Infoln("Replacing old connection", oldConn, "with", conn, "for", deviceID)
// There is an existing connection to this device that we are
// replacing. We must close the existing connection and wait for the
// close to complete before adding the new connection. We do the
// actual close without holding pmut as the connection will call
// back into Closed() for the cleanup.
closed := m.closed[deviceID]
m.fmut.RUnlock()
m.pmut.Unlock()
oldConn.Close(errReplacingConnection)
<-closed
// Again, lock fmut before pmut.
m.fmut.RLock()
m.pmut.Lock()
}
m.conn[deviceID] = conn
connID := conn.ConnectionID()
closed := make(chan struct{})
m.closed[deviceID] = closed
m.deviceDownloads[deviceID] = newDeviceDownloadState()
indexRegistry := newIndexHandlerRegistry(conn, m.deviceDownloads[deviceID], m.evLogger)
for id, fcfg := range m.folderCfgs {
runner, _ := m.folderRunners.Get(id)
indexRegistry.RegisterFolderState(fcfg, m.folderFiles[id], runner)
}
m.indexHandlers.Add(deviceID, indexRegistry)
m.fmut.RUnlock()
// 0: default, <0: no limiting
switch {
case device.MaxRequestKiB > 0:
m.connRequestLimiters[deviceID] = semaphore.New(1024 * device.MaxRequestKiB)
case device.MaxRequestKiB == 0:
m.connRequestLimiters[deviceID] = semaphore.New(1024 * defaultPullerPendingKiB)
}
m.pmut.Lock()
m.connections[connID] = conn
m.closed[connID] = closed
m.helloMessages[deviceID] = hello
m.deviceConnIDs[deviceID] = append(m.deviceConnIDs[deviceID], connID)
if m.deviceDownloads[deviceID] == nil {
m.deviceDownloads[deviceID] = newDeviceDownloadState()
}
event := map[string]string{
"id": deviceID.String(),
@@ -2284,17 +2367,15 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
m.evLogger.Log(events.DeviceConnected, event)
l.Infof(`Device %s client is "%s %s" named "%s" at %s`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName, conn)
if len(m.deviceConnIDs[deviceID]) == 1 {
l.Infof(`Device %s client is "%s %s" named "%s" at %s`, deviceID.Short(), hello.ClientName, hello.ClientVersion, hello.DeviceName, conn)
} else {
l.Infof(`Additional connection (+%d) for device %s at %s`, len(m.deviceConnIDs[deviceID])-1, deviceID.Short(), conn)
}
conn.Start()
m.pmut.Unlock()
// Acquires fmut, so has to be done outside of pmut.
cm, passwords := m.generateClusterConfig(deviceID)
conn.SetFolderPasswords(passwords)
conn.ClusterConfig(cm)
if (device.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
if (deviceCfg.Name == "" || m.cfg.Options().OverwriteRemoteDevNames) && hello.DeviceName != "" {
m.cfg.Modify(func(cfg *config.Configuration) {
for i := range cfg.Devices {
if cfg.Devices[i].DeviceID == deviceID {
@@ -2308,26 +2389,78 @@ func (m *model) AddConnection(conn protocol.Connection, hello protocol.Hello) {
}
m.deviceWasSeen(deviceID)
m.scheduleConnectionPromotion()
}
func (m *model) scheduleConnectionPromotion() {
// Keeps deferring to prevent multiple executions in quick succession,
// e.g. if multiple connections to a single device are closed.
m.promotionTimer.Reset(time.Second)
}
// promoteConnections checks for devices that have connections, but where
// the primary connection hasn't started index handlers etc. yet, and
// promotes the primary connection to be the index handling one. This should
// be called after adding new connections, and after closing a primary
// device connection.
func (m *model) promoteConnections() {
m.fmut.RLock() // for generateClusterConfigFRLocked
defer m.fmut.RUnlock()
m.pmut.Lock() // for most other things
defer m.pmut.Unlock()
for deviceID, connIDs := range m.deviceConnIDs {
cm, passwords := m.generateClusterConfigFRLocked(deviceID)
if m.promotedConnID[deviceID] != connIDs[0] {
// The previously promoted connection is not the current
// primary; we should promote the primary connection to be the
// index handling one. We do this by sending a ClusterConfig on
// it, which will cause the other side to start sending us index
// messages there. (On our side, we manage index handlers based
// on where we get ClusterConfigs from the peer.)
conn := m.connections[connIDs[0]]
l.Debugf("Promoting connection to %s at %s", deviceID.Short(), conn)
if conn.Statistics().StartedAt.IsZero() {
conn.SetFolderPasswords(passwords)
conn.Start()
}
conn.ClusterConfig(cm)
m.promotedConnID[deviceID] = connIDs[0]
}
// Make sure any other new connections also get started, and that
// they get a secondary-marked ClusterConfig.
for _, connID := range connIDs[1:] {
conn := m.connections[connID]
if conn.Statistics().StartedAt.IsZero() {
conn.SetFolderPasswords(passwords)
conn.Start()
conn.ClusterConfig(protocol.ClusterConfig{Secondary: true})
}
}
}
}
func (m *model) DownloadProgress(conn protocol.Connection, folder string, updates []protocol.FileDownloadProgressUpdate) error {
deviceID := conn.DeviceID()
m.fmut.RLock()
cfg, ok := m.folderCfgs[folder]
m.fmut.RUnlock()
device := conn.DeviceID()
if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(device) {
if !ok || cfg.DisableTempIndexes || !cfg.SharedWith(deviceID) {
return nil
}
m.pmut.RLock()
downloads := m.deviceDownloads[device]
downloads := m.deviceDownloads[deviceID]
m.pmut.RUnlock()
downloads.Update(folder, updates)
state := downloads.GetBlockCounts(folder)
m.evLogger.Log(events.RemoteDownloadProgress, map[string]interface{}{
"device": device.String(),
"device": deviceID.String(),
"folder": folder,
"state": state,
})
@@ -2344,27 +2477,47 @@ func (m *model) deviceWasSeen(deviceID protocol.DeviceID) {
}
}
func (m *model) deviceDidClose(deviceID protocol.DeviceID, duration time.Duration) {
m.fmut.RLock()
sr, ok := m.deviceStatRefs[deviceID]
m.fmut.RUnlock()
if ok {
func (m *model) deviceDidCloseFRLocked(deviceID protocol.DeviceID, duration time.Duration) {
if sr, ok := m.deviceStatRefs[deviceID]; ok {
_ = sr.LastConnectionDuration(duration)
}
}
func (m *model) requestGlobal(ctx context.Context, deviceID protocol.DeviceID, folder, name string, blockNo int, offset int64, size int, hash []byte, weakHash uint32, fromTemporary bool) ([]byte, error) {
m.pmut.RLock()
nc, ok := m.conn[deviceID]
m.pmut.RUnlock()
if !ok {
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
conn, connOK := m.requestConnectionForDevice(deviceID)
if !connOK {
return nil, fmt.Errorf("requestGlobal: no connection to device: %s", deviceID.Short())
}
l.Debugf("%v REQ(out): %s: %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
l.Debugf("%v REQ(out): %s (%s): %q / %q b=%d o=%d s=%d h=%x wh=%x ft=%t", m, deviceID.Short(), conn, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
return conn.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
}
return nc.Request(ctx, folder, name, blockNo, offset, size, hash, weakHash, fromTemporary)
// requestConnectionForDevice returns a connection to the given device, to
// be used for sending a request. If there is only one device connection,
// this is the one to use. If there are multiple then we avoid the first
// ("primary") connection, which is dedicated to index data, and pick a
// random one of the others.
func (m *model) requestConnectionForDevice(deviceID protocol.DeviceID) (protocol.Connection, bool) {
m.pmut.RLock()
defer m.pmut.RUnlock()
connIDs, ok := m.deviceConnIDs[deviceID]
if !ok {
return nil, false
}
// If there is an entry in deviceConns, it always contains at least one
// connection.
connID := connIDs[0]
if len(connIDs) > 1 {
// Pick a random connection of the non-primary ones
idx := rand.Intn(len(connIDs)-1) + 1
connID = connIDs[idx]
}
conn, connOK := m.connections[connID]
return conn, connOK
}
func (m *model) ScanFolders() map[string]error {
@@ -2455,11 +2608,13 @@ func (m *model) numHashers(folder string) int {
// generateClusterConfig returns a ClusterConfigMessage that is correct and the
// set of folder passwords for the given peer device
func (m *model) generateClusterConfig(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
var message protocol.ClusterConfig
m.fmut.RLock()
defer m.fmut.RUnlock()
return m.generateClusterConfigFRLocked(device)
}
func (m *model) generateClusterConfigFRLocked(device protocol.DeviceID) (protocol.ClusterConfig, map[string]string) {
var message protocol.ClusterConfig
folders := m.cfg.FolderList()
passwords := make(map[string]string, len(folders))
for _, folderCfg := range folders {
@@ -2771,7 +2926,7 @@ func (m *model) availabilityInSnapshotPRlocked(cfg config.FolderConfiguration, s
if state := m.remoteFolderStates[device][cfg.ID]; state != remoteFolderValid {
continue
}
_, ok := m.conn[device]
_, ok := m.deviceConnIDs[device]
if ok {
availabilities = append(availabilities, Availability{ID: device, FromTemporary: false})
}
@@ -2904,13 +3059,6 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
}
}
// Removing a device. We actually don't need to do anything.
// Because folder config has changed (since the device lists do not match)
// Folders for that had device got "restarted", which involves killing
// connections to all devices that we were sharing the folder with.
// At some point model.Close() will get called for that device which will
// clean residue device state that is not part of any folder.
// Pausing a device, unpausing is handled by the connection service.
fromDevices := from.DeviceMap()
toDevices := to.DeviceMap()
@@ -2941,7 +3089,14 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
l.Infoln("Resuming", deviceID)
m.evLogger.Log(events.DeviceResumed, map[string]string{"device": deviceID.String()})
}
if toCfg.MaxRequestKiB != fromCfg.MaxRequestKiB {
m.pmut.Lock()
m.setConnRequestLimitersPLocked(toCfg)
m.pmut.Unlock()
}
}
// Clean up after removed devices
removedDevices := make([]protocol.DeviceID, 0, len(fromDevices))
m.fmut.Lock()
@@ -2955,14 +3110,18 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
m.pmut.RLock()
for _, id := range closeDevices {
delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok {
go conn.Close(errDevicePaused)
if conns, ok := m.deviceConnIDs[id]; ok {
for _, connID := range conns {
go m.connections[connID].Close(errDevicePaused)
}
}
}
for _, id := range removedDevices {
delete(clusterConfigDevices, id)
if conn, ok := m.conn[id]; ok {
go conn.Close(errDeviceRemoved)
if conns, ok := m.deviceConnIDs[id]; ok {
for _, connID := range conns {
go m.connections[connID].Close(errDevicePaused)
}
}
}
m.pmut.RUnlock()
@@ -2986,6 +3145,17 @@ func (m *model) CommitConfiguration(from, to config.Configuration) bool {
return true
}
func (m *model) setConnRequestLimitersPLocked(cfg config.DeviceConfiguration) {
// Touches connRequestLimiters which is protected by pmut.
// 0: default, <0: no limiting
switch {
case cfg.MaxRequestKiB > 0:
m.connRequestLimiters[cfg.DeviceID] = semaphore.New(1024 * cfg.MaxRequestKiB)
case cfg.MaxRequestKiB == 0:
m.connRequestLimiters[cfg.DeviceID] = semaphore.New(1024 * defaultPullerPendingKiB)
}
}
func (m *model) cleanPending(existingDevices map[protocol.DeviceID]config.DeviceConfiguration, existingFolders map[string]config.FolderConfiguration, ignoredDevices deviceIDSet, removedFolders map[string]struct{}) {
var removedPendingFolders []map[string]string
pendingFolders, err := m.db.PendingFolders()
@@ -3332,3 +3502,12 @@ type redactedError struct {
error
redacted error
}
func without[E comparable, S ~[]E](s S, e E) S {
for i, x := range s {
if x == e {
return append(s[:i], s[i+1:]...)
}
}
return s
}
+14 -12
View File
@@ -270,7 +270,7 @@ func TestDeviceRename(t *testing.T) {
cfg, cfgCancel := newConfigWrapper(rawCfg)
defer cfgCancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil)
m := newModel(t, cfg, myID, nil)
if cfg.Devices()[device1].Name != "" {
t.Errorf("Device already has a name")
@@ -422,7 +422,7 @@ func TestClusterConfig(t *testing.T) {
wrapper, cancel := newConfigWrapper(cfg)
defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
m := newModel(t, wrapper, myID, nil)
m.ServeBackground()
defer cleanupModel(m)
@@ -903,7 +903,7 @@ func TestIssue5063(t *testing.T) {
defer cancel()
m.pmut.Lock()
for _, c := range m.conn {
for _, c := range m.connections {
conn := c.(*fakeConnection)
conn.CloseCalls(func(_ error) {})
defer m.Closed(c, errStopped) // to unblock deferred m.Stop()
@@ -1626,7 +1626,7 @@ func TestROScanRecovery(t *testing.T) {
},
})
defer cancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil)
m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -1673,7 +1673,7 @@ func TestRWScanRecovery(t *testing.T) {
},
})
defer cancel()
m := newModel(t, cfg, myID, "syncthing", "dev", nil)
m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -2073,7 +2073,7 @@ func TestIssue4357(t *testing.T) {
// Create a separate wrapper not to pollute other tests.
wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
m := newModel(t, wrapper, myID, nil)
m.ServeBackground()
defer cleanupModel(m)
@@ -2125,7 +2125,7 @@ func TestIssue4357(t *testing.T) {
}
func TestIndexesForUnknownDevicesDropped(t *testing.T) {
m := newModel(t, defaultCfgWrapper, myID, "syncthing", "dev", nil)
m := newModel(t, defaultCfgWrapper, myID, nil)
files := newFileSet(t, "default", m.db)
files.Drop(device1)
@@ -2231,7 +2231,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("device still in config")
}
if _, ok := m.conn[device2]; ok {
if _, ok := m.deviceConnIDs[device2]; ok {
t.Error("conn not missing")
}
@@ -2374,7 +2374,7 @@ func TestCustomMarkerName(t *testing.T) {
ffs := fcfg.Filesystem(nil)
m := newModel(t, cfg, myID, "syncthing", "dev", nil)
m := newModel(t, cfg, myID, nil)
set := newFileSet(t, "default", m.db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
@@ -2736,7 +2736,7 @@ func TestIssue4094(t *testing.T) {
// Create a separate wrapper not to pollute other tests.
wrapper, cancel := newConfigWrapper(config.Configuration{Version: config.CurrentVersion})
defer cancel()
m := newModel(t, wrapper, myID, "syncthing", "dev", nil)
m := newModel(t, wrapper, myID, nil)
m.ServeBackground()
defer cleanupModel(m)
@@ -2971,6 +2971,7 @@ func TestConnCloseOnRestart(t *testing.T) {
br := &testutil.BlockingRW{}
nw := &testutil.NoopRW{}
ci := &protocolmocks.ConnectionInfo{}
ci.ConnectionIDReturns(srand.String(16))
m.AddConnection(protocol.NewConnection(device1, br, nw, testutil.NoopCloser{}, m, ci, protocol.CompressionNever, nil, m.keyGen), protocol.Hello{})
m.pmut.RLock()
if len(m.closed) != 1 {
@@ -3639,6 +3640,7 @@ func testConfigChangeTriggersClusterConfigs(t *testing.T, expectFirst, expectSec
})
m.AddConnection(fc1, protocol.Hello{})
m.AddConnection(fc2, protocol.Hello{})
m.promoteConnections()
// Initial CCs
select {
@@ -3690,7 +3692,7 @@ func TestIssue6961(t *testing.T) {
must(t, err)
waiter.Wait()
// Always recalc/repair when opening a fileset.
m := newModel(t, wcfg, myID, "syncthing", "dev", nil)
m := newModel(t, wcfg, myID, nil)
m.db.Close()
m.db, err = db.NewLowlevel(backend.OpenMemory(), m.evLogger, db.WithRecheckInterval(time.Millisecond))
if err != nil {
@@ -3952,7 +3954,7 @@ func TestCCFolderNotRunning(t *testing.T) {
w, fcfg, wCancel := newDefaultCfgWrapper()
defer wCancel()
tfs := fcfg.Filesystem(nil)
m := newModel(t, w, myID, "syncthing", "dev", nil)
m := newModel(t, w, myID, nil)
defer cleanupModelAndRemoveDir(m, tfs.URI())
// A connection can happen before all the folders are started.
+2 -2
View File
@@ -1214,7 +1214,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
// Initialise db with an entry and then stop everything again
must(t, tfs.Mkdir(dir1, 0o777))
m := newModel(t, w, myID, "syncthing", "dev", nil)
m := newModel(t, w, myID, nil)
defer cleanupModelAndRemoveDir(m, tfs.URI())
m.ServeBackground()
m.ScanFolders()
@@ -1223,7 +1223,7 @@ func TestRequestIndexSenderClusterConfigBeforeStart(t *testing.T) {
// Add connection (sends incoming cluster config) before starting the new model
m = &testModel{
model: NewModel(m.cfg, m.id, m.clientName, m.clientVersion, m.db, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model),
model: NewModel(m.cfg, m.id, m.db, m.protectedFiles, m.evLogger, protocol.NewKeyGenerator()).(*model),
evCancel: m.evCancel,
stopped: make(chan struct{}),
}
+5 -3
View File
@@ -39,7 +39,9 @@ func init() {
device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
device1Conn.DeviceIDReturns(device1)
device1Conn.ConnectionIDReturns(rand.String(16))
device2Conn.DeviceIDReturns(device2)
device2Conn.ConnectionIDReturns(rand.String(16))
cfg := config.New(myID)
cfg.Options.MinHomeDiskFree.Value = 0 // avoids unnecessary free space checks
@@ -127,7 +129,7 @@ func setupModelWithConnectionFromWrapper(t testing.TB, w config.Wrapper) (*testM
func setupModel(t testing.TB, w config.Wrapper) *testModel {
t.Helper()
m := newModel(t, w, myID, "syncthing", "dev", nil)
m := newModel(t, w, myID, nil)
m.ServeBackground()
<-m.started
@@ -144,14 +146,14 @@ type testModel struct {
stopped chan struct{}
}
func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, clientName, clientVersion string, protectedFiles []string) *testModel {
func newModel(t testing.TB, cfg config.Wrapper, id protocol.DeviceID, 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, protocol.NewKeyGenerator()).(*model)
m := NewModel(cfg, id, ldb, protectedFiles, evLogger, protocol.NewKeyGenerator()).(*model)
ctx, cancel := context.WithCancel(context.Background())
go evLogger.Serve(ctx)
return &testModel{