# `AtomicBucket.MultiRateLimiter`
[🔗](https://github.com/a3kov/atomic_bucket/blob/main/lib/atomic_bucket/multi_rate_limiter.ex#L1)

Applies multiple rate limits at the same time, in a single atomic
operation.

## Options

See `__using__/1`.

## Examples

    defmodule MyRateLimiter do
      use AtomicBucket.MultiRateLimiter
    end

    # application.ex
    children = [.., MyRateLimiter, ..]

    defmodule CallerModule do
      require MyRateLimiter

      @sub_buckets %{
        second: {330, 2},
        minute: {3_000, 7},
        hour: {60_000, 30}
      }

      MyRateLimiter.request(:mybucket, @sub_buckets)

      # Or with details.
      MyRateLimiter.request_details(:mybucket, @sub_buckets)
    end

# `__using__`
*since 0.5.0* *macro* 

Converts the current module to a multi-rate limiter:
  - generates rate limiter API

  - adds AtomicBucket server child spec so that the module can be added
    to a supervision tree

This macro does only basic validation of the cleanup parameters.
Developers must ensure that buckets idling for more than ~24 days
are deleted: longer periods are not supported by the wrapping timer
used by the library.

## Options:
  - `:table` ETS table name atom. By default is implementing module name.

  - `:cleanup_interval` interval in ms defining how often the server will try
    to delete idle buckets. It is applied on completion of a cleanup.
    Default is 1 hour.

  - `:max_idle_period` max period in ms since last bucket update before
    it is deleted by the server. Default is 24 hours.

  - `persistent` if true, bucket references will be cached in
    `:persistent_term`. Default is false.

# `request`
*since 0.5.0* 

```elixir
@macrocallback request(
  bucket_id :: any(),
  sub_buckets :: %{
    required(name :: atom()) =&gt;
      {request_interval :: pos_integer(), burst :: pos_integer()}
  },
  cost_factor :: integer(),
  opts :: keyword()
) :: {AtomicBucket.verdict(), :atomics.atomics_ref()}
```

Checks if the request is allowed according to multiple rate limits.

Uses simplified algorithm, where each rate is represented as
request interval in milliseconds instead of window and requests.
The bucket is updated in a single atomic operation. By default fixed
request cost is assumed, but variable cost is also supported via
cost factor.

Multiple buckets are initialized in full state. Every request will
refill each bucket if needed and check if all buckets have enough
tokens to make the request. If true, the request tokens are
removed from each bucket and the call returns `{:allow, bucket_ref}`.
Otherwise, each bucket is left untouched and the call returns
`{:deny, bucket_ref}`. `bucket_ref` is a reference to the bucket
atomic.

There must be no duplicate intervals inside the sub-buckets, and lower
rate buckets must have bigger bursts (otherwise they kick in too soon).

## Arguments:
  - `bucket_id` any id unique within the bucket table

  - `sub_buckets` a map describing sub-buckets, with sub-bucket
    names as keys and `{request interval in milliseconds, burst requests}`
    tuples as values

  - `cost_factor` integer multiplier for the request cost

## Options:
  - `ref` bucket atomic reference. If provided, the call will try
    to use it instead of refetching.

# `request_details`
*since 0.5.0* 

```elixir
@macrocallback request_details(
  bucket_id :: any(),
  sub_buckets :: %{
    required(name :: atom()) =&gt;
      {request_interval :: pos_integer(), burst :: pos_integer()}
  },
  cost_factor :: integer(),
  opts :: keyword()
) ::
  {:allow, %{required(name :: any()) =&gt; non_neg_integer()},
   :atomics.atomics_ref()}
  | {:deny,
     %{required(name :: any()) =&gt; {AtomicBucket.verdict(), non_neg_integer()}},
     :atomics.atomics_ref()}
```

Checks if the request is allowed according to multiple rate limits and
returns results of each bucket check.

Unless you *really* need the results, consider using `request/4`
instead, which skips unnecessary calculations.

Multiple buckets are initialized in full state. Every request will
refill each bucket if needed and check if all buckets have enough
tokens to make the request.
If true, the call returns `{:allow, requests, bucket_ref}`,
where `requests` is a map with remaining requests of each sub-bucket.
Otherwise, each bucket is left untouched and the call returns
`{:deny, results, bucket_ref}`, where `results` is a map with sub-bucket
name keys and result tuples (`{:allow, remaining requests}` or
`{:deny, timeout}`) as values.

Note that remaining requests in the result tuple reflect final number
of available requests in the sub-bucket after the call.

There must be no duplicate intervals inside the sub-buckets, and lower
rate buckets must have bigger bursts (otherwise they kick in too soon).

## Arguments:
  - `bucket_id` any id unique within the bucket table

  - `sub_buckets` a map describing sub-buckets, with sub-bucket
    names as keys and `{request interval in milliseconds, burst requests}`
    tuples as values.

  - `cost_factor` integer multiplier for the request cost

## Options:
  - `ref` bucket atomic reference. If provided, the call will try
    to use it instead of refetching.

# `start_link`
*since 0.5.0* 

```elixir
@callback start_link() :: GenServer.on_start()
```

Starts AtomicBucket server managing buckets for the limiter.

Normally users don't need to call this function directly - instead
the implementing module can be added to a supervision tree and
the server is then started by a supervisor.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
