Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Throttle api #184

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions guides/throttle.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ See `amoc_throttle`
## Overview

Amoc throttle is a module that allows limiting the number of users' actions per given interval, no matter how many users there are in a test.
It works in both local and distributed environments, allows for dynamic rate changes during a test and exposes metrics which show the number of requests and executions.
It works in both local and distributed environments, allows for dynamic rate changes during a test and exposes telemetry events showing the number of requests and executions.

Amoc throttle allows to:

- Setting the execution `Rate` per `Interval`
- Limiting the number of parallel executions when `Rate` is set to `infinity`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate is supposed to be the number of parrallel executions if Interval is 0

- Setting the `Interarrival` time between actions.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not different to execution rate, but just another form of setting it.


Amoc throttle allows setting the execution `Rate` per `Interval` or limiting the number of parallel executions when `Interval` is set to `0`.
Each `Rate` is identified with a `Name`.
The rate limiting mechanism allows responding to a request only when it does not exceed the given `Rate`.
Amoc throttle makes sure that the given `Rate` per `Interval` is maintained on a constant level.
Expand Down Expand Up @@ -102,8 +107,8 @@ For every `Name`, a `NoOfProcesses` are created, each responsible for keeping ex
### Distributed environment

#### Metrics
In a distributed environment every Amoc node with a throttle started, exposes metrics showing the numbers of requests and executions.
Those exposed by the master node show the sum of all metrics from all nodes.
In a distributed environment every Amoc node with a throttle started, exposes telemetry events showing the numbers of requests and executions.
Those exposed by the master node show the aggregate of all telemetry events from all nodes.
This allows to quickly see the real rates across the whole system.

#### Workflow
Expand Down
120 changes: 61 additions & 59 deletions src/throttle/amoc_throttle.erl
Original file line number Diff line number Diff line change
Expand Up @@ -3,57 +3,63 @@
-module(amoc_throttle).

%% API
-export([start/2,
start/3,
start/4,
send/3,
send/2,
send_and_wait/2,
wait/1,
run/2,
pause/1,
resume/1,
change_rate/3,
change_rate_gradually/6,
stop/1]).

-deprecated([{send_and_wait, 2, "use wait/1 instead"}]).

-define(DEFAULT_NO_PROCESSES, 10).
-define(DEFAULT_INTERVAL, 60000). %% one minute
-define(NONNEG_INT(N), (is_integer(N) andalso N >= 0)).
-define(POS_INT(N), (is_integer(N) andalso N > 0)).
-export([start/2, stop/1,
send/2, send/3, wait/1,
run/2, pause/1, resume/1, unlock/1,
change_rate/2, change_rate_gradually/2]).

-type name() :: atom().
-type rate() :: pos_integer().
%% Atom representing the name of the throttle.
DenysGonchar marked this conversation as resolved.
Show resolved Hide resolved
-type rate() :: infinity | non_neg_integer().
%% Number of events per given `t:interval/0', or infinity for effectively unlocking all throttling.
%% Note that a rate of zero means effectively pausing the throttle.
-type interarrival() :: infinity | non_neg_integer().
%% Time in milliseconds between two events, or infinity for effectively pausing the throttle. Note
%% that an interarrival of zero means effectively unlocking all throttling.
-type interval() :: non_neg_integer().
%% In milliseconds, defaults to 60000 (one minute) when not given.
%% An interval of 0 means no delay at all, only the number of simultaneous executions will be
%% controlled, which corresponds to the number of processes started
-export_type([name/0, rate/0, interval/0]).

%% @see start/4
-spec start(name(), rate()) -> ok | {error, any()}.
start(Name, Rate) ->
start(Name, Rate, ?DEFAULT_INTERVAL).
%% In milliseconds, defaults to 60000 (one minute).
-type throttle() :: #{rate := rate(), interval := interval()} |
#{interarrival := interarrival()}.
%% Throttle unit of measurement
-type config() :: #{rate := rate(),
interval => interval(),
parallelism => non_neg_integer()}
| #{interarrival := interarrival(),
parallelism => non_neg_integer()}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the meaning of 0

%% Literal throttle configuration.

-type gradual_rate_config() :: #{from_rate := non_neg_integer(),
to_rate := non_neg_integer(),
interval => interval(),
step_interval => pos_integer(),
step_size => pos_integer(),
step_count => pos_integer(),
duration => pos_integer()} |
#{from_interarrival := interarrival(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems that from_interarrival cannot be 0 nor infinity in this case, the same is applicable to to_interarrival

to_interarrival := interarrival(),
step_interval => pos_integer(),
step_size => pos_integer(),
step_count => pos_integer(),
duration => pos_integer()}.
%% Configuration for a gradual throttle rate change
%%
%% `From' and `To' rates are required. `interval' defaults to 1s and `step_size' to 1 (or -1 if applies),
%% that is, the throttle will be changed in increments of 1.
%%
%% All other values can be calculated from the provided.

%% @see start/4
-spec start(name(), rate(), non_neg_integer()) -> ok | {error, any()}.
start(Name, Rate, Interval) ->
start(Name, Rate, Interval, ?DEFAULT_NO_PROCESSES).
-export_type([name/0, rate/0, interval/0, throttle/0, config/0, gradual_rate_config/0]).

%% @doc Starts the throttle mechanism for a given `Name' with a given `Rate' per `Interval'.
%% @doc Starts the throttle mechanism for a given `Name' with a given config.
%%
%% The optional arguments are an `Interval' (default is one minute) and a ` NoOfProcesses' (default is 10).
%% `Name' is needed to identify the rate as a single test can have different rates for different tasks.
%% `Interval' is given in milliseconds and can be changed to a different value for convenience or higher granularity.
%% It also accepts a special value of `0' which limits the number of parallel executions associated with `Name' to `Rate'.
-spec start(name(), rate(), interval(), pos_integer()) -> ok | {error, any()}.
start(Name, Rate, Interval, NoOfProcesses)
when is_atom(Name), ?POS_INT(Rate), ?NONNEG_INT(Interval), ?POS_INT(NoOfProcesses) ->
amoc_throttle_controller:ensure_throttle_processes_started(Name, Rate, Interval, NoOfProcesses);
start(_Name, _Rate, _Interval, _NoOfProcesses) ->
{error, invalid_throttle}.
-spec start(name(), config() | rate()) -> {ok, started | already_started} | {error, any()}.
start(Name, #{} = Config) ->
amoc_throttle_controller:ensure_throttle_processes_started(Name, Config);
start(Name, Rate) ->
amoc_throttle_controller:ensure_throttle_processes_started(Name, #{rate => Rate}).

%% @doc Pauses executions for the given `Name' as if `Rate' was set to `0'.
%%
Expand All @@ -67,12 +73,17 @@ pause(Name) ->
resume(Name) ->
amoc_throttle_controller:resume(Name).

%% @doc Sets `Rate' and `Interval' for `Name' according to the given values.
%% @doc Unlocks executions for the given `Name' as if `Rate' was set to `infinity'.
-spec unlock(name()) -> ok | {error, any()}.
unlock(Name) ->
amoc_throttle_controller:unlock(Name).

%% @doc Sets `Throttle' for `Name' according to the given values.
%%
%% Can change whether Amoc throttle limits `Name' to parallel executions or to `Rate' per `Interval',
%% according to the given `Interval' value.
-spec change_rate(name(), rate(), interval()) -> ok | {error, any()}.
change_rate(Name, Rate, Interval) ->
-spec change_rate(name(), throttle()) -> ok | {error, any()}.
change_rate(Name, #{rate := Rate, interval := Interval}) ->
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't we want to support #{interarrival := interarrival()} format here?

amoc_throttle_controller:change_rate(Name, Rate, Interval).

%% @doc Allows to set a plan of gradual rate changes for a given `Name'.
Expand All @@ -83,19 +94,17 @@ change_rate(Name, Rate, Interval) ->
%% The rate is calculated at each step in relation to the `RateInterval', which can also be `0'.
%% There will be `NoOfSteps' steps, each taking `StepInterval' time in milliseconds.
%%
%% Be aware that, at first, the rate will be changed to `FromRate' per `RateInterval' and this is not considered a step.
-spec change_rate_gradually(name(), rate(), rate(), interval(), pos_integer(), pos_integer()) ->
%% Be aware that, at first, the rate will be changed to `FromRate' per `RateInterval'
%% and this is not considered a step.
-spec change_rate_gradually(name(), gradual_rate_config()) ->
ok | {error, any()}.
change_rate_gradually(Name, FromRate, ToRate, RateInterval, StepInterval, NoOfSteps) ->
amoc_throttle_controller:change_rate_gradually(
Name, FromRate, ToRate, RateInterval, StepInterval, NoOfSteps).
change_rate_gradually(Name, Config) ->
amoc_throttle_controller:change_rate_gradually(Name, Config).

%% @doc Executes a given function `Fn' when it does not exceed the rate for `Name'.
%%
%% `Fn' is executed in the context of a new process spawned on the same node on which
%% the process executing `run/2' runs, so a call to `run/2' is non-blocking.
%% This function is used internally by both `send' and `send_and_wait/2' functions,
%% so all those actions will be limited to the same rate when called with the same `Name'.
%%
%% Diagram showing function execution flow in distributed environment,
%% generated using https://sequencediagram.org/:
Expand Down Expand Up @@ -145,13 +154,6 @@ send(Name, Msg) ->
send(Name, Pid, Msg) ->
amoc_throttle_runner:throttle(Name, {Pid, Msg}).

%% @doc Sends and receives the given message `Msg'.
%%
%% Deprecated in favour of `wait/1'
-spec send_and_wait(name(), any()) -> ok | {error, any()}.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this interface is actively used by amoc_arsenal_xmpp. if it's so, please add tickets for amoc_arsena/amoc_arsenal_xmpp

send_and_wait(Name, _) ->
amoc_throttle_runner:throttle(Name, wait).

%% @doc Blocks the caller until the throttle mechanism allows.
-spec wait(name()) -> ok | {error, any()}.
wait(Name) ->
Expand Down
Loading
Loading