Skip to content

Commit

Permalink
Merge pull request #184 from esl/throttle_api
Browse files Browse the repository at this point in the history
Throttle api
  • Loading branch information
DenysGonchar authored Nov 29, 2024
2 parents 360c3a3 + 92ef315 commit d842d1f
Show file tree
Hide file tree
Showing 15 changed files with 1,065 additions and 604 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
otp_vsn: ['27', '26', '25']
rebar_vsn: ['3.23.0']
rebar_vsn: ['3.24.0']
test-type: ['regular', 'integration']
runs-on: 'ubuntu-24.04'
steps:
Expand Down
2 changes: 1 addition & 1 deletion guides/coordinator.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## API

See `amoc_coordinator`.
See `m:amoc_coordinator`.

## Description

Expand Down
2 changes: 1 addition & 1 deletion guides/telemetry.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ This event is raised only on the master node.

```erlang
event_name: [amoc, throttle, rate]
measurements: #{rate := non_neg_integer()}
measurements: #{rate := rate(), interval := interval()}
metadata: #{monotonic_time := integer(), name := atom(), msg => binary()}
```

Expand Down
39 changes: 23 additions & 16 deletions guides/throttle.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
## API

See `amoc_throttle`
See `m: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 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.
Amoc throttle allows to:

- Setting the execution `Rate` per `Interval`, or inversely, the `Interarrival` time between actions.
- Limiting the number of parallel executions when `interval` is set to `0`.

Each throttle is identified with a `Name`.
The rate limiting mechanism allows responding to a request only when it does not exceed the given throttle.
Amoc throttle makes sure that the given throttle is maintained on a constant level.
It prevents bursts of executions which could blurry the results, as they technically produce a desired rate in a given interval.
Because of that, it may happen that the actual `Rate` would be slightly below the demanded rate. However, it will never be exceeded.
Because of that, it may happen that the actual throttle rate would be slightly below the demanded rate. However, it will never be exceeded.

## Examples

Expand Down Expand Up @@ -42,18 +46,21 @@ user_loop(Id) ->
user_loop(Id).
```
Here a system should be under a continuous load of 100 messages per minute.
Note that if we used something like `amoc_throttle:run(messages_rate, fun() -> send_message(Id) end)` instead of `amoc_throttle:send_and_wait/2` the system would be flooded with requests.
Note that if we used something like `amoc_throttle:run(messages_rate, fun() -> send_message(Id) end)` instead of `amoc_throttle:wait/1` the system would be flooded with requests.

A test may of course be much more complicated.
For example it can have the load changing in time.
A plan for that can be set for the whole test in `init/1`:
```erlang
init() ->
%% init metrics
amoc_throttle:start(messages_rate, 100),
%% 9 steps of 100 increases in Rate, each lasting one minute
amoc_throttle:change_rate_gradually(messages_rate, 100, 1000, 60000, 60000, 9),
ok.
Gradual = #{from_rate => 100,
to_rate => 1000,
step_count => 9,
step_size => 100,
step_interval => timer:minutes(1)},
amoc_throttle:change_rate_gradually(messages_rate, Gradual).
```

Normal Erlang messages can be used to schedule tasks for users by themselves or by some controller process.
Expand Down Expand Up @@ -97,13 +104,13 @@ For a more comprehensive example please refer to the `throttle_test` scenario, w
- `amoc_throttle_controller.erl` - a gen_server which is responsible for reacting to requests, and managing `throttle_processes`.
In a distributed environment an instance of `throttle_controller` runs on every node, and the one running on the master Amoc node stores the state for all nodes.
- `amoc_throttle_process.erl` - gen_server module, implements the logic responsible for limiting the rate.
For every `Name`, a `NoOfProcesses` are created, each responsible for keeping executions at a level proportional to their part of `Rate`.
For every `Name`, a number of processes are created, each responsible for keeping executions at a level proportional to their part of the throttle.

### 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 All @@ -112,12 +119,12 @@ Then a runner process is spawned on the same node.
Its task will be to execute `Fun` asynchronously.
A random throttle process which is assigned to the `Name` is asked for a permission for asynchronous runner to execute `Fun`.
When the request reaches the master node, where throttle processes reside, the request metric on the master node is updated and the throttle process which got the request starts monitoring the asynchronous runner process.
Then, depending on the system's load and the current rate of executions, the asynchronous runner is allowed to run the `Fun` or compelled to wait, because executing the function would exceed the calculated `Rate` in an `Interval`.
Then, depending on the system's load and the current rate of executions, the asynchronous runner is allowed to run the `Fun` or compelled to wait, because executing the function would exceed the calculated throttle.
When the rate finally allows it, the asynchronous runner gets the permission to run the function from the throttle process.
Both processes increase the metrics which count executions, but for each the metric is assigned to their own node.
Then the asynchronous runner tries to execute `Fun`.
It may succeed or fail, either way it dies and an `'EXIT'` signal is sent to the throttle process.
This way it knows that the execution of a task has ended, and can allow a different process to run its task connected to the same `Name` if the current `Rate` allows it.
This way it knows that the execution of a task has ended, and can allow a different process to run its task connected to the same `Name` if the current throttle allows it.

Below is a graph showing the communication between processes on different nodes described above.
![amoc_throttle_dist](assets/amoc_throttle_dist.svg)
8 changes: 4 additions & 4 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
]}.

{deps, [
{telemetry, "1.2.1"}
{telemetry, "1.3.0"}
]}.

{profiles, [
Expand All @@ -14,10 +14,10 @@
{meck, "0.9.2"},
{proper, "1.4.0"},
{bbmustache, "1.12.2"},
{wait_helper, "0.2.0"}
{wait_helper, "0.2.1"}
]}
]},
{elvis, [{plugins, [{rebar3_lint, "3.2.3"}]}]}
{elvis, [{plugins, [{rebar3_lint, "3.2.6"}]}]}
]}.

{relx, [
Expand Down Expand Up @@ -62,7 +62,7 @@
{'guides/amoc_livebook.livemd', #{title => <<"Livebook tutorial">>}},
{'LICENSE', #{title => <<"License">>}}
]},
{assets, <<"guides/assets">>},
{assets, #{<<"guides/assets">> => <<"assets">>}},
{main, <<"readme">>}
]}.

Expand Down
6 changes: 3 additions & 3 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{"1.2.0",
[{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.2.1">>},0}]}.
[{<<"telemetry">>,{pkg,<<"telemetry">>,<<"1.3.0">>},0}]}.
[
{pkg_hash,[
{<<"telemetry">>, <<"68FDFE8D8F05A8428483A97D7AAB2F268AAFF24B49E0F599FAA091F1D4E7F61C">>}]},
{<<"telemetry">>, <<"FEDEBBAE410D715CF8E7062C96A1EF32EC22E764197F70CDA73D82778D61E7A2">>}]},
{pkg_hash_ext,[
{<<"telemetry">>, <<"DAD9CE9D8EFFC621708F99EAC538EF1CBE05D6A874DD741DE2E689C47FEAFED5">>}]}
{<<"telemetry">>, <<"7015FC8919DBE63764F4B4B87A95B7C0996BD539E0D499BE6EC9D7F3875B79E6">>}]}
].
Loading

0 comments on commit d842d1f

Please sign in to comment.