Basic implementation of a leaky bucket, the idea is you call it by saying I will allow e.g. 5 login attempts per 60 seconds.
//Leaky Bucket algorithm with chrono
class RateController {
int n;
std::chrono::steady_clock::duration interval;
int cnt;
std::chrono::time_point<std::chrono::steady_clock> last;
public:
RateController(int limit, int seconds) :
n(limit), interval(std::chrono::seconds(seconds/limit)), cnt(0) {}
bool ok() {
auto t = std::chrono::steady_clock::now();
if (cnt) {
cnt -= (t - last) / interval;
if (cnt < 0) cnt = 0;
if (cnt >= n) return false;
}
++cnt;
last = t;
return true;
}
};
1 Answer 1
Create a type alias for the clock you want to use
Things become more maintainable and easier to write if you create a type alias for the clock, like so:
using clock = std::chrono::steady_clock;
That way, you can write:
clock::duration interval;
clock::time_point last;
...
auto t = clock::now();
Use an unsigned type for the counters
I recommend using unsigned types for the counters, as they should never be negative. In order to leak without the counter going negative, write:
cnt -= std::min(cnt, (t - last) / interval);
Let the caller pass the interval as a std::chrono::duration
Instead of forcing the caller to pass the duration as an integer describing seconds, and then having to convert this to a std::chrono::duration
type yourself, consider changing the parameter seconds
of the constructor to be of type clock::duration
. The caller can then pass in any std::chrono::duration
it likes.
Be aware of integer division
The expression seconds / limit
will evaluate to 0
if limit > seconds
. This is problematic as it will cause a division by zero later in ok()
. I would avoid the division entirely, and have interval
mean the interval in which at most limit
calls to ok()
will return true
. Then to leak you just write:
cnt -= std::min(cnt, (t - last) * n / interval);
Naming things
Some things are abbreviated unnecessarily. Instead of cnt
, write count
. Instead of n
, write max_count
or limit
like you do in the constructor. Instead of t
, write now
or current_time
. It's just a few more characters to type, but it will make the code much clearer to anyone reading it, including yourself in a few months when you've forgotten the details of your implementation.
-
\$\begingroup\$ Thanks, in particular the division by zero was a serious bug. I wonder, however, is there a chance for integer overflow when you multiply by n before dividing ? \$\endgroup\$CaptainCodeman– CaptainCodeman2022年03月29日 19:42:43 +00:00Commented Mar 29, 2022 at 19:42
-
\$\begingroup\$ Yes, that's possible if both
n
andt - last
are very large. However, at least there is no division by zero, so it should not cause a crash, and if there are frequent calls took()
then it's very likely thatt - last
is going to be small, so this is a case where I would not worry about it. Also note that you could precomputeclock::duration(interval) / n
, and perhapsassert()
that this is not zero to be very sure. \$\endgroup\$G. Sliepen– G. Sliepen2022年03月29日 20:35:26 +00:00Commented Mar 29, 2022 at 20:35 -
\$\begingroup\$ I feel this is a problem with using "magical" types such as duration, because we can't know if it stores the value in pico-seconds and whether it's safe to multiply.. \$\endgroup\$CaptainCodeman– CaptainCodeman2022年04月07日 17:08:45 +00:00Commented Apr 7, 2022 at 17:08
-
\$\begingroup\$ I agree it's not very explicit. However, the
std::chrono::clock
types need to have enough precision to store the system clock's timer value, this typically means they store nanoseconds. Also consider that their range must span centuries, so unless you think(t - last) * n
would ever be more than a century, it should be fine. If you want to be defensive, think of a duration of only having a precision of 1 second and being stored in anint
. Even then(t - last) * n / interval
should work fine. \$\endgroup\$G. Sliepen– G. Sliepen2022年04月08日 05:49:46 +00:00Commented Apr 8, 2022 at 5:49