3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-12-23 03:02:48 +01:00
ergo/irc/fakelag.go
Shivaram Lingamneni f6d2dade4e fix fakelag double-rate issue
Basically, fakelag was counting the time imposed by its own sleeps as though
the user had themselves paused for that amount of time. Therefore, if a user
sent a large number of consecutive commands, every other command would pause
for the expected throttle interval, but the subsequent command would be
processed instantly (you'd get two back-to-back commands). This resulted in
throttled users being able to send at double the expected rate.
2018-04-16 04:32:57 -04:00

95 lines
2.5 KiB
Go

// Copyright (c) 2018 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"time"
)
// fakelag is a system for artificially delaying commands when a user issues
// them too rapidly
type FakelagState uint
const (
// initially, the client is "bursting" and can send n commands without
// encountering fakelag
FakelagBursting FakelagState = iota
// after that, they're "throttled" and we sleep in between commands until
// they're spaced sufficiently far apart
FakelagThrottled
)
// this is intentionally not threadsafe, because it should only be touched
// from the loop that accepts the client's input and runs commands
type Fakelag struct {
window time.Duration
burstLimit uint
throttleMessagesPerWindow uint
cooldown time.Duration
nowFunc func() time.Time
sleepFunc func(time.Duration)
state FakelagState
burstCount uint // number of messages sent in the current burst
lastTouch time.Time
}
func NewFakelag(window time.Duration, burstLimit uint, throttleMessagesPerWindow uint, cooldown time.Duration) *Fakelag {
return &Fakelag{
window: window,
burstLimit: burstLimit,
throttleMessagesPerWindow: throttleMessagesPerWindow,
cooldown: cooldown,
nowFunc: time.Now,
sleepFunc: time.Sleep,
state: FakelagBursting,
}
}
// register a new command, sleep if necessary to delay it
func (fl *Fakelag) Touch() {
if fl == nil {
return
}
now := fl.nowFunc()
// XXX if lastTouch.IsZero(), treat it as "very far in the past", which is fine
elapsed := now.Sub(fl.lastTouch)
fl.lastTouch = now
if fl.state == FakelagBursting {
// determine if the previous burst is over
if elapsed > fl.cooldown {
fl.burstCount = 0
}
fl.burstCount++
if fl.burstCount > fl.burstLimit {
// reset burst window for next time
fl.burstCount = 0
// transition to throttling
fl.state = FakelagThrottled
// continue to throttling logic
} else {
return
}
}
if fl.state == FakelagThrottled {
if elapsed > fl.cooldown {
// let them burst again
fl.state = FakelagBursting
return
}
// space them out by at least window/messagesperwindow
sleepDuration := time.Duration((int64(fl.window) / int64(fl.throttleMessagesPerWindow)) - int64(elapsed))
if sleepDuration > 0 {
fl.sleepFunc(sleepDuration)
// the touch time should take into account the time we slept
fl.lastTouch = fl.nowFunc()
}
}
}