Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib/connections, lib/config: Bandwidth throttling per remote device (fixes #4516) #4603

Merged
merged 29 commits into from Mar 26, 2018

Conversation

qepasa
Copy link
Contributor

@qepasa qepasa commented Dec 17, 2017

Purpose

Hello everyone. As described in the issue. This fix enables read/write bandwidth throttling per remote device alongside current global limiting. It can be set via device tab in advanced settings.

Testing

Testing was done using 2 remote devices. Tested scenarios include adding and removing devices and various configurations of r/w rates:
device1: 300/300, device2: unlimited/unlimited, global: unlimited/unlimited
device1: 300/300, device2: unlimited/unlimited, global: 150/150
device1: 300/300, device2: 50/50, global: 150/150
device1: unlimited/unlimited, device2: 50/50, global: 150/150
device1: unlimited/unlimited, device2: unlimited/unlimited, global: 700/700

Of course these tests may not be comprehensive. I'll be thankful for any additional suggestions or scenarios.

}

func (w *limitedWriter) Write(buf []byte) (int, error) {
if !w.isLAN || w.limiter.limitsLAN.get() {
take(w.limiter.write, len(buf))
deviceLimiter, ok := w.limiter.deviceWriteLimiter[w.remoteID]
if ok == false {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!ok

lim.deviceWriteLimiter[v.DeviceID] = rate.NewLimiter(rate.Inf, limiterBurstSize)
lim.deviceReadLimiter[v.DeviceID] = rate.NewLimiter(rate.Inf, limiterBurstSize)
}
l.Debugln("Rebuild finished")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has no context, rebuild of what, where, why

l.CommitConfiguration(prev, cfg.RawCopy())
return l
}

func (lim *limiter) newReadLimiter(r io.Reader, isLAN bool) io.Reader {
return &limitedReader{reader: r, limiter: lim, isLAN: isLAN}
func getInitialDevicesConfiguration(cfgCopy config.Configuration) config.DeviceConfigurationList {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only called once, I'd say that it's fine to wedge in the loop inline in the constructor.

}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could just call rebuildMap here?

// Compare read/write limits in configurations
func (lim *limiter) checkDeviceLimits(from, to config.Configuration) bool {
for i := range from.Devices {
if from.Devices[i].DeviceID.Compare(to.Devices[i].DeviceID) != 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use equality for device ID's they should be comparable just fine.

if to.Options.MaxRecvKbps > 0 {
recvLimitStr = fmt.Sprintf("limit is %d KiB/s", to.Options.MaxRecvKbps)
}
l.Infof("Send rate %s, receive rate %s", sendLimitStr, recvLimitStr)
l.Infof("Global send rate %s, receive rate %s", sendLimitStr, recvLimitStr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall or Total. Global usually talks about locations.

deviceLimiter, ok := r.limiter.deviceReadLimiter[r.remoteID]
if ok == false {
l.Debugln("deviceLimiter was not in the map")
deviceLimiter = rate.NewLimiter(rate.Inf, limiterBurstSize)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can this happen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It happens after removing remote device. I'm not sure about limitedWriter but I added it just to be safe.

"golang.org/x/net/context"
"golang.org/x/time/rate"
)

type deviceLimiters map[protocol.DeviceID]*rate.Limiter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably needs to be sync.Map (atomic map) as you have not implemented any locking, and there are definately multiple routines poking at it. Also, the atomic map should only be initialized once, because if you re-assign the map on the limiter struct, you still need a lock.

@imsodin
Copy link
Member

imsodin commented Dec 18, 2017

Now you are using both locks and sync.Map, why?

The rebuildMap calls in several places seem overly complicated to me: Couldn't you just iterate over devices from old and new configuration in CommitConfiguration and delete/add/adjust limiters as necessary? When initializing you can then just call CommitConfiguration with empty maps as well.

continue
}

readLimiter, _ := lim.deviceReadLimiters.Load(v.DeviceID)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not throw away the ok, in case the value is not there.
Also, you are not locking here, so you might be accessing deviceReadLimiters as the map is being swapped out.

This is also a reason why rebuildMap should potentially do it without swapping the maps.

// A device has been added or removed
if len(from.Devices) != len(to.Devices) {
lim.rebuildMap(to)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this is unnecessery if you handle it properly in checkDeviceLimits, or stop it from having side-effects.

lim.deviceWriteLimiters = deviceWriteLimiters
lim.deviceReadLimiters = deviceReadLimiters

l.Debugln("Rebuild of device limiters map finished")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better for rebuild to happen in place, this way getting rid of the lock that we have to get on every read and write.

// Compare read/write limits in configurations
func (lim *limiter) checkDeviceLimits(from, to config.Configuration) bool {
for i := range from.Devices {
if from.Devices[i].DeviceID != to.Devices[i].DeviceID {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should blow up if a device is removed, as len(to) < len(from).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should probably write a test case for this logic.
Test case for adding
Test case for removing
Test case for adding and removing in the same config commit cycle.

for i := range from.Devices {
if from.Devices[i].DeviceID != to.Devices[i].DeviceID {
// Something has changed in device configuration
lim.rebuildMap(to)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, this function as it states should only do checking, and not rebuilding (as a side-effect), and rebuilding and other cruft should happen in the CommitConfiguration.

if v.MaxRecvKbps > 0 {
recvLimitStr = fmt.Sprintf("limit is %d KiB/s", v.MaxRecvKbps)
}
l.Infof("Device %s: send rate %s, receive rate %s", v.DeviceID, sendLimitStr, recvLimitStr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will always print stuff, even if someones rate hasn't changed since before.
It will further always mess around with the rate limiters, regardless if the value changed or not.

r.limiter.mu.Lock()
deviceLimiter, ok := r.limiter.deviceReadLimiters.Load(r.remoteID)
r.limiter.mu.Unlock()
if !ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, if !ok, I'd panic (or do nothing), as it should clearly be there.
Now it's like a bandaid for a problem we can't explain.

I'd do a if deviceLimiter != nil check in take

}
return w.writer.Write(buf)
}

// take is a utility function to consume tokens from a rate.Limiter. No call
// to WaitN can be larger than the limiter burst size so we split it up into
// several calls when necessary.
func take(l *rate.Limiter, tokens int) {
func take(l, deviceLimiter *rate.Limiter, tokens int) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the l could do with a better name now, and the doc string should be updated.


// copy *limiter in case remote device is still connected, when remote device is added we create new read/write limiters
for _, v := range to.Devices {
if readLimiter, ok := lim.deviceReadLimiters.Load(v.DeviceID); ok {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if there are two rebuildMap calls in parallel due to frequent config save or something like that, this might be halfway through a swap too.

@qepasa
Copy link
Contributor Author

qepasa commented Dec 18, 2017

@AudriusButkevicius You're right. I assumed (which was pretty stupid) that only one routine at the time visits CommitConfiguration. I guess the right way will be to rewrite it so that "rebuild" happens without swapping map instances and definitely implement mentioned tests and fix other issues pointed by you. Thanks for feedback!

@imsodin My intention was to use lock in order to ensure atomic assignment in rebuildMap.

@AudriusButkevicius
Copy link
Member

@qepasa thanks for working on this.

read *rate.Limiter
limitsLAN atomicBool
deviceReadLimiters *sync.Map
deviceWriteLimiters *sync.Map
Copy link
Member

@calmh calmh Dec 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sync.Map is Go 1.9 and we're currently targeting 1.8. This is the build failure reported by the tests.

Although we could bump our requirements I think there should be little enough churn on these that we can actually use regular maps and locking...

@qepasa
Copy link
Contributor Author

qepasa commented Dec 27, 2017

Hello everyone. Sorry for delay with coding. I tried to keep your suggestions in mind. If there is anything you would like to be done differently I'll make sure to implement it faster this time.

l.deviceWriteLimiters[value.DeviceID] = rate.NewLimiter(rate.Inf, limiterBurstSize)
l.deviceReadLimiters[value.DeviceID] = rate.NewLimiter(rate.Inf, limiterBurstSize)
}
prev := config.Configuration{Options: config.OptionsConfiguration{MaxRecvKbps: -1, MaxSendKbps: -1}, Devices: devices}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the entire above block isn't necessary, as CommitConfiguration will add all missing devices anyway, so https://github.com/syncthing/syncthing/pull/4603/files#diff-da45d4f40dfa16a9aae760dd650fe79aL35 should be sufficient.

// Pass pointer to avoid copying. Pointer already points to copy of configuration
// so we don't have to worry about modifying real config.
func (lim *limiter) processDevicesConfiguration(from, to *config.Configuration) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty

if v.DeviceID != to.MyID {
devicesToRemove[v.DeviceID] = true
}
}
Copy link
Member

@imsodin imsodin Dec 27, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be map[protocol.DeviceID]struct{} and then later instead of = false do delete(devicesToRemove, v.DeviceID). Then you can just iterate over the final map in the end without checking a bool.

} else {
writeLimiter.SetLimit(1024 * rate.Limit(v.MaxSendKbps))
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this lives on the lim struct, but doesn't actually use it, meaning it should either be a utility function not part of lim, or use lim and reach into lim.device{Write,Read}Limiters directly.

// Pass pointer to avoid copying. Pointer already points to copy of configuration
// so we don't have to worry about modifying real config.
func (lim *limiter) processDevicesConfiguration(from, to *config.Configuration) {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noop space.

func (lim *limiter) newWriteLimiter(w io.Writer, isLAN bool) io.Writer {
return &limitedWriter{writer: w, limiter: lim, isLAN: isLAN}
// Compare read/write limits in configurations
func (lim *limiter) checkDeviceLimits(from, to config.Configuration) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, doesn't need to live on lim.
Also the function name doesn't explain well what this does. Should probably be called deviceLimitsChanged() and invert what it returns.

}
// len(from.Devices) == len(to.Devices) so we can do range from.Devices
for i := range from.Devices {
if from.Devices[i].DeviceID != to.Devices[i].DeviceID {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nit-pick, but we don't really need to rely on the device order here. It's possible that rates aren't changed, just the order gets changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be some sort of cfg.DeviceMap() or something like that utility function.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that should be moved from model to config: https://github.com/syncthing/syncthing/blob/master/lib/model/model.go#L2569
That also makes it easier to remove limiters, just treat it like folders in models CommitConfiguration.

// so we don't have to worry about modifying real config.
func (lim *limiter) processDevicesConfiguration(from, to *config.Configuration) {

devicesToRemove := make(map[protocol.DeviceID]bool)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be map[DeviceID]struct{}, and devicesToRemove[x] = false is essentially delete(devicesToRemove, x)

Or better would be:

for dev in to.Devices {
   seen[dev] = struct{}{}
}

for dev in from.Devices {
   if  _, ok := seen[dev]; !ok {
      lim.deleteLimiters(dev)
   }
}

@@ -73,22 +217,25 @@ func (lim *limiter) CommitConfiguration(from, to config.Configuration) bool {

lim.limitsLAN.set(to.Options.LimitBandwidthInLan)

// Delete, add or update limiters for devices
lim.processDevicesConfiguration(&from, &to)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a pointer?

// already marked it as false in devicesToRemove
panic("broken symmetry in device read/write limiters")
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too complicated. The only way these things get set are under a lock, so it's very unlikely they get out of sync.
Also, perhaps it's better to get getDevice{Read,Write}Limiter always return a new limiter even if one does not exists (and add it to the map).

I am thinking about a case where a new device gets added, and there is a race between CommitConfiguration here, and us making a connection. It's probably always better wrap every connection in a limiter (and create one if it doesn't exist) and then adjust the rates later.


l.Debugf("okR: %t, okW: %t, prevWriteLim: %d, prevReadLim: %d", okR, okW, previousReadLimit, previousWriteLimit)

lim.setLimitsForDevice(v, readLimiter, writeLimiter)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not better not to do this if the limits are the same as previously?
As in, move this after the if?

}

const limiterBurstSize = 4 * 128 << 10

func newLimiter(cfg *config.Wrapper) *limiter {
func newLimiter(deviceID protocol.DeviceID, cfg *config.Wrapper) *limiter {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty

if from.Options.MaxRecvKbps == to.Options.MaxRecvKbps &&
from.Options.MaxSendKbps == to.Options.MaxSendKbps &&
from.Options.LimitBandwidthInLan == to.Options.LimitBandwidthInLan {
from.Options.LimitBandwidthInLan == to.Options.LimitBandwidthInLan &&
lim.checkDeviceLimits(from, to) {
return true
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this global check is not very useful as global limits are reset even if just a device changed and whether device change is checked again later on. So I would either not check at all and just reset everything or check the different parameters separately.

}
// len(from.Devices) == len(to.Devices) so we can do range from.Devices
for i := range from.Devices {
if from.Devices[i].DeviceID != to.Devices[i].DeviceID {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that should be moved from model to config: https://github.com/syncthing/syncthing/blob/master/lib/model/model.go#L2569
That also makes it easier to remove limiters, just treat it like folders in models CommitConfiguration.

@imsodin
Copy link
Member

imsodin commented Dec 27, 2017

Argh this github reviews... Probably better look at the comments in the diff, this comment view is fractured badly.

Hello everyone. Sorry for delay with coding. I tried to keep your suggestions in mind. If there is anything you would like to be done differently I'll make sure to implement it faster this time.

Even if you weren't occupied with Christmas holidays and family reunions there is no reason at all to apologize for ~1week (and also if longer) followup times after a round of review. Reviewers will nag if it comes to the point of being too quiet ;)


// mapDeviceConfigs returns a map of device ID to device configuration for the given
// slice of folder configurations.
func MapDeviceConfigs(devices []DeviceConfiguration) map[protocol.DeviceID]DeviceConfiguration {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I propose func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree. And we do this in a few other places (in the model I think), so we should use this instead.

from.Devices[i].MaxRecvKbps != to.Devices[i].MaxRecvKbps {
return false
if fromMap[k].MaxSendKbps != toMap[k].MaxSendKbps ||
fromMap[k].MaxRecvKbps != toMap[k].MaxRecvKbps {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No manual line breaks (I was/am a fan of them too, but it's not the Syncthing/Go style ;) ).

// Nothing about this device has changed. Start processing next device
if okR && okW &&
rate.Limit(v.MaxRecvKbps)*1024 == previousReadLimit &&
rate.Limit(v.MaxSendKbps)*1024 == previousWriteLimit {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line breaks

// to ensure atomic update of configuration
lim.mu.Lock()
defer lim.mu.Unlock()
if deviceLimitsChanged(from, to) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove this check. You already do the same checks in processDevicesConfiguration and skip any devices which don't change.

// This function handles removing, adding and updating of device limiters.
// Pass pointer to avoid copying. Pointer already points to copy of configuration
// so we don't have to worry about modifying real config.
func (lim *limiter) processDevicesConfiguration(from, to *config.Configuration) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't need to pass configs as pointers.

@AudriusButkevicius
Copy link
Member

My previous comments on always creating limiters and always wrapping each device connection in two limiters still apply

@qepasa
Copy link
Contributor Author

qepasa commented Feb 6, 2018

I added suggested fixes. Thank you guys for patience with this pull request and for quick reviews.

readLimitStr := "is unlimited"
if dev.MaxRecvKbps > 0 {
readLimitStr = fmt.Sprintf("limit is %d KiB/s", dev.MaxRecvKbps)
if limitsChanged := lim.setLimits(dev); limitsChanged {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be if lim.setLimits(dev) {.

lim.mu.Lock()
defer lim.mu.Unlock()
deviceLimiter := lim.getWriteLimiter(remoteID)
return &limitedWriter{writer: w, limiter: lim, deviceLimiter: deviceLimiter, isLAN: isLAN}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fact that processblabla does locking outside the function, and this does inside, feels wrong.

Copy link
Member

@imsodin imsodin Feb 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AudriusButkevicius We are starting to contradict each other once again x-): My reasoning to suggest this was that the locking now happens in CommitConfiguration and in these newLimited... functions, which are all "top-level", i.e. called "externally" (not all outside of the package, but outside of the limiter code). Also locking the entirety of CommitConfiguration was considered necessary to disallow concurrent change and thus with a single lock the locking cannot happen on get...Limiter anymore (single lock is not a requirement obviously, I just consider it cleaner).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, but why can't we lock lim.mu before calling the newblabla twice in the connection service, this way we lock the same way in both places. Also a function that needs locking before called, we should add the locked word into the name like we do in the model.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I see.
@qepasa So locking should happen here instead and a whole bunch of functions needs the Locked suffix: setLimits, new..., get..., processD....

seen[dev.DeviceID] = struct{}{}

// Nothing about this device has changed. Start processing next device
if limitsChanged := lim.setLimits(dev); limitsChanged {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable assignment is pointless, you can straight do if setLimits(dev) {

}

func initConfig(t *testing.T) {
t.Helper()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need t.helper as you are not asserting anything here

@@ -169,8 +169,7 @@ func TestAddAndRemove(t *testing.T) {
checkActualAndExpected(t, actualR, actualW, expectedR, expectedW)
}

func initConfig(t *testing.T) {
t.Helper()
func initConfig() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just return a config, rather than setting some global that leaks between the tests.

@AudriusButkevicius
Copy link
Member

Implementation wise looks ok to me, needs to resolve merge conflicts and pass the tests (which I am not sure if they are failing due to anything this PR is doing).
Also perhaps needs UI to expose this in device editor.

@imsodin
Copy link
Member

imsodin commented Feb 11, 2018

Also perhaps needs UI to expose this in device editor.

I agree that this is probably a nice enough functionality to get a place there. If @qepasa wants to do it in this PR, cool, but otherwise I think it's fine to leave it in advanced settings and open an enhancement issue for someone else (or qepasa) to do that eventually.

calmh
calmh previously requested changes Feb 11, 2018
Copy link
Member

@calmh calmh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sure you've already discussed this in the 90+ comments above, but all of this juggling of mutexes and maps and stuff does not seem worth it to me compared to just creating the limiter with the appropriate settings at connection time. Yeah then we can't change the per device rate limit on the fly, but this will almost never happen and we anyway disconnect peers most of the time when we change folders or devices. That would make this whole thing like a four line diff instead.

"golang.org/x/net/context"
"golang.org/x/time/rate"
"sync"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be our lib/sync

limitsLAN atomicBool
deviceReadLimiters map[protocol.DeviceID]*rate.Limiter
deviceWriteLimiters map[protocol.DeviceID]*rate.Limiter
mu *sync.Mutex
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just sync.Mutex, no need for a pointer (regardless of which sync package)

read: rate.NewLimiter(rate.Inf, limiterBurstSize),
write: rate.NewLimiter(rate.Inf, limiterBurstSize),
read: rate.NewLimiter(rate.Inf, limiterBurstSize),
mu: &sync.Mutex{},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjust here to nothing at all (standard lib sync) or sync.NewMutex() (our sync)

func (lim *limiter) newWriteLimiter(w io.Writer, isLAN bool) io.Writer {
return &limitedWriter{writer: w, limiter: lim, isLAN: isLAN}
// This function handles removing, adding and updating of device limiters.
// Pass pointer to avoid copying. Pointer already points to copy of configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear what the pointer comments are about. The configuration parameters are not pointers.

@@ -265,8 +265,10 @@ next:
// keep up with config changes to the rate and whether or not LAN
// connections are limited.
isLAN := s.isLAN(c.RemoteAddr())
wr := s.limiter.newWriteLimiter(c, isLAN)
rd := s.limiter.newReadLimiter(c, isLAN)
s.limiter.mu.Lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unusual, we do don't want to take locks on mutexes inside other objects and then call their ...Locked functions. Wrap in a method.

rd, wr := s.limiter.getLimiters(remoteID, c, isLAN)

@imsodin
Copy link
Member

imsodin commented Feb 11, 2018

[...] all of this juggling of mutexes and maps and stuff does not seem worth it to me compared to just creating the limiter with the appropriate settings at connection time. Yeah then we can't change the per device rate limit on the fly, but this will almost never happen and we anyway disconnect peers most of the time when we change folders or devices. [...]

@calmh Isn't this dropping of connection one of the long-standing issues nobody wants to tackle because it is so entangled? What I mean is that not needing a "complete reset" and thus at least not increasing the entanglement, seems to already provide quite a bit of merit.

@calmh
Copy link
Member

calmh commented Feb 11, 2018

I don't really think the connection resetting is a big deal, but on the other hand we already have the limiter that listens to config reloads, so fine lets roll with this.

@calmh
Copy link
Member

calmh commented Feb 11, 2018

LGTM apart from above mutex nits, and that the parameters to take could have better names (l being globalLimiter or something maybe).

Has anyone tested that this actually works?

@@ -173,6 +169,14 @@ func (lim *limiter) String() string {
return "connections.limiter"
}

func (lim *limiter) getLimiters(remoteID protocol.DeviceID, c internalConn, isLAN bool) (io.Reader, io.Writer) {
lim.mu.Lock()
defer lim.mu.Unlock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no point using here defer as the scope is small, as this just prevents potential inlining.

Copy link
Member

@AudriusButkevicius AudriusButkevicius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the last nitpick this looks good to me, if it works :)


func (lim *limiter) newLimitedWriterLocked(remoteID protocol.DeviceID, w io.Writer, isLAN bool) io.Writer {
deviceLimiter := lim.getWriteLimiterLocked(remoteID)
return &limitedWriter{writer: w, limiter: lim, deviceLimiter: deviceLimiter, isLAN: isLAN}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge these two two line functions into the callee to reduce the mental stack depth.

@qepasa
Copy link
Contributor Author

qepasa commented Feb 14, 2018

I can try to make UI for this but I would prefer it to be a new issue.

@AudriusButkevicius
Copy link
Member

@imsodin @calmh pls re-review and lets put this to bed.

The person has put an immense amount of effort for dealing with my (our?) comments so far, and we just left him in silence. I am happy with this.

@calmh
Copy link
Member

calmh commented Mar 26, 2018

I have nits to pick, but i'll pick them myself in a followup at some point

@calmh calmh merged commit 2621c6f into syncthing:master Mar 26, 2018
calmh added a commit to calmh/syncthing that referenced this pull request Apr 11, 2018
* master: (24 commits)
  lib/model: Prevent warning on request in paused folder (fixes syncthing#4870)
  docker: Add README from old Docker repo (fixes syncthing#4868) (syncthing#4869)
  cmd/strelaypoolsrv: Move metric scraping to the server itself (syncthing#4866)
  gui, man: Update docs & translations
  assets: Use icon from synctrayzor (ref syncthing#4839) (syncthing#4859)
  cmd/strelaypoolsrv: Handle portless X-Forwarded-For (syncthing#4856)
  lib/fs: Don't panic when watching a folder with symlinked root (syncthing#4846)
  cmd/syncthing: Set Content-Type header regardless of asset location (syncthing#4847)
  authors: Add fuzzybear3965
  cmd/syncthing: Correct --paths in some cases (syncthing#4845)
  gui, man: Update docs & translations
  lib/osutil: Use unix lowprio implementation on Android (syncthing#4844)
  lib/scanner, lib/model: Actually assign version when un-ignoring (fixes syncthing#4841) (syncthing#4842)
  lib/scanner, lib/model: Actually assign version when un-ignoring (fixes syncthing#4841) (syncthing#4842)
  cmd/stdiscosrv, vendor: Remove remnants of golang.org/x/net/context (syncthing#4843)
  lib/connections: Wrong context snuck in somehow
  build: Add icon & file info to syncthing.exe (syncthing#4839)
  lib/connections: Slightly refactor limiter juggling
  lib/connections, lib/config: Bandwidth throttling per remote device (fixes syncthing#4516) (syncthing#4603)
  gui: Add folder label to global changes, use bootstrap table (fixes syncthing#4828) (syncthing#4838)
  ...
@st-review st-review added the frozen-due-to-age Issues closed and untouched for a long time, together with being locked for discussion label Mar 27, 2019
@syncthing syncthing locked and limited conversation to collaborators Mar 27, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
frozen-due-to-age Issues closed and untouched for a long time, together with being locked for discussion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants