Current section
Files
Jump to
Current section
Files
src/esockd_rate_limit.erl
%%--------------------------------------------------------------------
%% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%--------------------------------------------------------------------
%% @doc Token-bucket based Rate Limit.
%%
%% [Token Bucket](https://en.wikipedia.org/wiki/Token_bucket).
%%
%% @end
-module(esockd_rate_limit).
-export([ new/1
, new/2
, info/1
, check/2
]).
-export_type([config/0, bucket/0]).
-record(bucket, {
rate :: float(),
burst :: pos_integer(),
tokens :: non_neg_integer(),
time :: pos_integer()
}).
-opaque(bucket() :: #bucket{}).
-opaque(config() :: {float()|pos_integer(), pos_integer()}).
%% @doc Create a rate limit. The time unit of rate is second.
-spec(new(config()) -> bucket()).
new({Rate, Burst}) -> new(Rate, Burst).
-spec(new(float()|pos_integer(), pos_integer()) -> bucket()).
new(Rate, Burst) when is_integer(Burst), 0 < Rate andalso Rate =< Burst ->
#bucket{rate = Rate,
burst = Burst,
tokens = Burst,
time = erlang:system_time(milli_seconds)
}.
-spec(info(bucket()) -> map()).
info(#bucket{rate = Rate, burst = Burst, tokens = Tokens, time = Lastime}) ->
#{rate => Rate,
burst => Burst,
tokens => Tokens,
time => Lastime
}.
%% The pause time P:
%% if Tokens < Limit => P = 0;
%% if Tokens = Limit => P = 1/r;
%% if Tokens > Limit => P = (Tokens-Limit)/r
-spec(check(pos_integer(), bucket()) -> {non_neg_integer(), bucket()}).
check(Tokens, Bucket) ->
check(Tokens, erlang:system_time(milli_seconds), Bucket).
-spec(check(pos_integer(), integer(), bucket()) -> {non_neg_integer(), bucket()}).
check(Tokens, Now, Bucket = #bucket{rate = Rate,
burst = Burst,
tokens = Remaining,
time = Lastime}) ->
Limit = min(Burst, Remaining + round((Rate * (Now - Lastime)) / 1000)),
case Limit > Tokens of
true -> %% Tokens available
{0, Bucket#bucket{tokens = Limit - Tokens, time = Now}};
false -> %% Tokens not enough
Pause = round(max(Tokens - Limit, 1) * 1000 / Rate),
{Pause, Bucket#bucket{tokens = 0, time = Now}}
end.