chanfitness: cool down flap count for rate limiting
Since we store all-time flap count for a peer, we add a cooldown factor which will discount poor flap counts in the past. This is only applied to peers that have not flapped for at least a cooldown period, so that we do not downgrade our rate limiting for badly behaved peers.
This commit is contained in:
parent
a550ca3d64
commit
6cf66aea47
@ -117,6 +117,16 @@ func newChannelInfo(openedAt time.Time) *channelInfo {
|
|||||||
func (p *peerLog) onlineEvent(online bool) {
|
func (p *peerLog) onlineEvent(online bool) {
|
||||||
eventTime := p.clock.Now()
|
eventTime := p.clock.Now()
|
||||||
|
|
||||||
|
// If we have a non-nil last flap time, potentially apply a cooldown
|
||||||
|
// factor to the peer's flap count before we rate limit it. This allows
|
||||||
|
// us to decrease the penalty for historical flaps over time, provided
|
||||||
|
// the peer has not flapped for a while.
|
||||||
|
if p.lastFlap != nil {
|
||||||
|
p.flapCount = cooldownFlapCount(
|
||||||
|
p.clock.Now(), p.flapCount, *p.lastFlap,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Record flap count information and online state regardless of whether
|
// Record flap count information and online state regardless of whether
|
||||||
// we have any channels open with this peer.
|
// we have any channels open with this peer.
|
||||||
p.flapCount++
|
p.flapCount++
|
||||||
|
@ -204,6 +204,47 @@ func TestRateLimitAdd(t *testing.T) {
|
|||||||
require.Equal(t, []*event{peerEvent}, peerLog.onlineEvents)
|
require.Equal(t, []*event{peerEvent}, peerLog.onlineEvents)
|
||||||
require.Equal(t, nextEvent, peerLog.stagedEvent)
|
require.Equal(t, nextEvent, peerLog.stagedEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now, we test the case where a peer's flap count is cooled down
|
||||||
|
// because it has not flapped for a while. Set our peer's flap count so
|
||||||
|
// that we fall within our second rate limiting tier and assert that we
|
||||||
|
// are at this level.
|
||||||
|
peerLog.flapCount = rateLimitScale + 1
|
||||||
|
rateLimit := getRateLimit(peerLog.flapCount)
|
||||||
|
require.Equal(t, rateLimits[1], rateLimit)
|
||||||
|
|
||||||
|
// Progress our clock to the point where we will have our flap count
|
||||||
|
// cooled.
|
||||||
|
newNow = mockedClock.Now().Add(flapCountCooldownPeriod)
|
||||||
|
mockedClock.SetTime(newNow)
|
||||||
|
|
||||||
|
// Add an online event, and expect it to be staged.
|
||||||
|
onlineEvent := &event{
|
||||||
|
timestamp: newNow,
|
||||||
|
eventType: peerOnlineEvent,
|
||||||
|
}
|
||||||
|
peerLog.onlineEvent(true)
|
||||||
|
require.Equal(t, onlineEvent, peerLog.stagedEvent)
|
||||||
|
|
||||||
|
// Progress our clock by the rate limit level that we will be on if
|
||||||
|
// our flap rate is cooled down to a lower level.
|
||||||
|
newNow = mockedClock.Now().Add(rateLimits[0] + 1)
|
||||||
|
mockedClock.SetTime(newNow)
|
||||||
|
|
||||||
|
// Add another event. We expect this event to be staged and our previous
|
||||||
|
// event to be flushed to the event log (because our cooldown has been
|
||||||
|
// applied).
|
||||||
|
offlineEvent := &event{
|
||||||
|
timestamp: newNow,
|
||||||
|
eventType: peerOfflineEvent,
|
||||||
|
}
|
||||||
|
peerLog.onlineEvent(false)
|
||||||
|
require.Equal(t, offlineEvent, peerLog.stagedEvent)
|
||||||
|
|
||||||
|
flushedEventIdx := len(peerLog.onlineEvents) - 1
|
||||||
|
require.Equal(
|
||||||
|
t, onlineEvent, peerLog.onlineEvents[flushedEventIdx],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetOnlinePeriod tests the getOnlinePeriod function. It tests the case
|
// TestGetOnlinePeriod tests the getOnlinePeriod function. It tests the case
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
package chanfitness
|
package chanfitness
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
// rateLimitScale is the number of events we allow per rate limited tier.
|
const (
|
||||||
// Increasing this value makes our rate limiting more lenient, decreasing it
|
// rateLimitScale is the number of events we allow per rate limited
|
||||||
// makes us less lenient.
|
// tier. Increasing this value makes our rate limiting more lenient,
|
||||||
const rateLimitScale = 200
|
// decreasing it makes us less lenient.
|
||||||
|
rateLimitScale = 200
|
||||||
|
|
||||||
|
// flapCountCooldownFactor is the factor by which we decrease a peer's
|
||||||
|
// flap count if they have not flapped for the cooldown period.
|
||||||
|
flapCountCooldownFactor = 0.95
|
||||||
|
|
||||||
|
// flapCountCooldownPeriod is the amount of time that we require a peer
|
||||||
|
// has not flapped for before we reduce their all time flap count using
|
||||||
|
// our cooldown factor.
|
||||||
|
flapCountCooldownPeriod = time.Hour * 8
|
||||||
|
)
|
||||||
|
|
||||||
// rateLimits is the set of rate limit tiers we apply to our peers based on
|
// rateLimits is the set of rate limit tiers we apply to our peers based on
|
||||||
// their flap count. A peer can be placed in their tier by dividing their flap
|
// their flap count. A peer can be placed in their tier by dividing their flap
|
||||||
@ -35,3 +49,34 @@ func getRateLimit(flapCount int) time.Duration {
|
|||||||
|
|
||||||
return rateLimits[tier]
|
return rateLimits[tier]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cooldownFlapCount takes a timestamped flap count, and returns its value
|
||||||
|
// scaled down by our cooldown factor if at least our cooldown period has
|
||||||
|
// elapsed since the peer last flapped. We do this because we store all-time
|
||||||
|
// flap count for peers, and want to allow downgrading of peers that have not
|
||||||
|
// flapped for a long time.
|
||||||
|
func cooldownFlapCount(now time.Time, flapCount int,
|
||||||
|
lastFlap time.Time) int {
|
||||||
|
|
||||||
|
// Calculate time since our last flap, and the number of times we need
|
||||||
|
// to apply our cooldown factor.
|
||||||
|
timeSinceFlap := now.Sub(lastFlap)
|
||||||
|
|
||||||
|
// If our cooldown period has not elapsed yet, we just return our flap
|
||||||
|
// count. We allow fractional cooldown periods once this period has
|
||||||
|
// elapsed, so we do not want to apply a fractional cooldown before the
|
||||||
|
// full cooldown period has elapsed.
|
||||||
|
if timeSinceFlap < flapCountCooldownPeriod {
|
||||||
|
return flapCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the factor by which we need to cooldown our flap count. If
|
||||||
|
// insufficient time has passed to cooldown our flap count. Use use a
|
||||||
|
// float so that we allow fractional cooldown periods.
|
||||||
|
cooldownPeriods := float64(timeSinceFlap) /
|
||||||
|
float64(flapCountCooldownPeriod)
|
||||||
|
|
||||||
|
effectiveFactor := math.Pow(flapCountCooldownFactor, cooldownPeriods)
|
||||||
|
|
||||||
|
return int(float64(flapCount) * effectiveFactor)
|
||||||
|
}
|
||||||
|
@ -49,3 +49,57 @@ func TestGetRateLimit(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestCooldownFlapCount tests cooldown of all time flap counts.
|
||||||
|
func TestCooldownFlapCount(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
flapCount int
|
||||||
|
lastFlap time.Time
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "just flapped, do not cooldown",
|
||||||
|
flapCount: 1,
|
||||||
|
lastFlap: testNow,
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "period not elapsed, do not cooldown",
|
||||||
|
flapCount: 1,
|
||||||
|
lastFlap: testNow.Add(flapCountCooldownPeriod / 2 * -1),
|
||||||
|
expected: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rounded to 0",
|
||||||
|
flapCount: 1,
|
||||||
|
lastFlap: testNow.Add(flapCountCooldownPeriod * -1),
|
||||||
|
expected: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "decreased to integer value",
|
||||||
|
flapCount: 10,
|
||||||
|
lastFlap: testNow.Add(flapCountCooldownPeriod * -1),
|
||||||
|
expected: 9,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple cooldown periods",
|
||||||
|
flapCount: 10,
|
||||||
|
lastFlap: testNow.Add(flapCountCooldownPeriod * -3),
|
||||||
|
expected: 8,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
flapCount := cooldownFlapCount(
|
||||||
|
testNow, test.flapCount, test.lastFlap,
|
||||||
|
)
|
||||||
|
require.Equal(t, test.expected, flapCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user