patternMinor
Rate limiter utility class
Viewed 0 times
rateclassutilitylimiter
Problem
I had a problem in my system where the class that sends messages to other systems sometimes gets flooded and so I needed to add rate limiting. I threw together something that would work, but I would really like to see what I can do to improve and/or clean it up.
This class does not do the actual rate limiting, it only provides timing guidelines on when the rate limited class should perform its next action.
* and statistics
*/
def execute() = {
val now = System.currentTimeMillis()
last = now
if (currentEnd == 0l) {
initializeWindows()
}
else {
while (now > currentEnd) {
dropOnePeriod()
queueLastPeriod()
}
currentCount += 1
totalCount += 1
}
}
def initializeWindows(): Unit = {
currentEnd = System.currentTimeMillis() + periodInterval
c
This class does not do the actual rate limiting, it only provides timing guidelines on when the rate limited class should perform its next action.
package edu.stsci.util
import scala.collection.mutable
/**
* Used to rate limit a repeated action based on three parameters. This class doesn't
* actually control the action, but tells the calling class how long to wait before
* performing the action again by calling nextWait. Caller also indicates every time
* an action is performed using execute.
*
* @param minSpacing The minimum amount of time between actions for burst throughput (in ms)
* @param maxPer The number of actions that can happen during any one movingPeriod time window
* @param movingPeriod The length of the moving time window for controlling sustained throughput (in ms)
*/
class RateLimiter(val minSpacing: Int, val maxPer: Int, val movingPeriod: Int) {
val periodRate: Double = maxPer / RateLimiter.SUBWINDOW_COUNT.toDouble
val periodInterval = movingPeriod / RateLimiter.SUBWINDOW_COUNT
val sustainedInterval: Long = movingPeriod / maxPer
var last: Long = 0
var totalCount = 0
var movingData = new mutable.Queue[Int]()
var currentCount = 0
var currentEnd = 0l
/**
* execute` must be called every time an action occurs to update last* and statistics
*/
def execute() = {
val now = System.currentTimeMillis()
last = now
if (currentEnd == 0l) {
initializeWindows()
}
else {
while (now > currentEnd) {
dropOnePeriod()
queueLastPeriod()
}
currentCount += 1
totalCount += 1
}
}
def initializeWindows(): Unit = {
currentEnd = System.currentTimeMillis() + periodInterval
c
Solution
Slow tests
The tests run slow because of the
It would be better to inject a "time provider" in the implementation,
so that you can fake
Cause and effect
The connection between the numbers used in the fixture setup step and in the assertions is not obvious.
Take for example the assertion
It's not obvious where 80 comes from and how it's related to the fixture setup.
The consequence is that if something changes in the implementation,
it won't be obvious how to change the test.
I'm guessing these are the lines that contribute to the threshold of 80:
I'm guessing that elapsed should be > 20 * 4.
It would be good to make the connection obvious by using more local variables with descriptive names, for example:
Cosmetic issues
A minor thing,
Instead of this:
I'm guessing you meant to write this:
The tests run slow because of the
Thread.sleep calls.It would be better to inject a "time provider" in the implementation,
so that you can fake
System.currentTimeMillis() and Thread.sleep(next) and keep the tests fast.Cause and effect
The connection between the numbers used in the fixture setup step and in the assertions is not obvious.
Take for example the assertion
elapsed should be > 80l in test("burst case").It's not obvious where 80 comes from and how it's related to the fixture setup.
The consequence is that if something changes in the implementation,
it won't be obvious how to change the test.
I'm guessing these are the lines that contribute to the threshold of 80:
val testObject = new RateLimiter(20, 100, 5000)
for (...) {
// ...
for (count 80lI'm guessing that elapsed should be > 20 * 4.
It would be good to make the connection obvious by using more local variables with descriptive names, for example:
val minSpacing = 20
val testObject = new RateLimiter(minSpacing, 100, 5000)
for (...) {
// ...
val samples = 4
for (count (minSpacing * samples).toLongCosmetic issues
A minor thing,
zero is redundant here:val zero: Long = testObject.nextWait()
zero should be (0l)Instead of this:
println("..., wait ", next)I'm guessing you meant to write this:
println("... , wait " + next)Code Snippets
val testObject = new RateLimiter(20, 100, 5000)
for (...) {
// ...
for (count <- 1 to 4) {
// ...
}
// ...
elapsed should be > 80lval minSpacing = 20
val testObject = new RateLimiter(minSpacing, 100, 5000)
for (...) {
// ...
val samples = 4
for (count <- 1 to samples) {
// ...
}
// ...
elapsed should be > (minSpacing * samples).toLongval zero: Long = testObject.nextWait()
zero should be (0l)println("..., wait ", next)println("... , wait " + next)Context
StackExchange Code Review Q#128241, answer score: 6
Revisions (0)
No revisions yet.