concurrent-limiter/lib/concurrent_limiter.ex

151 lines
4.8 KiB
Elixir
Raw Normal View History

2020-05-16 10:17:40 +00:00
# ConcurrentLimiter: A concurrency limiter.
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
2020-05-14 20:14:09 +00:00
defmodule ConcurrentLimiter do
2020-05-08 18:10:33 +00:00
require Logger
2020-05-07 23:55:49 +00:00
2020-05-08 18:10:33 +00:00
@moduledoc """
A concurrency limiter. Limits the number of concurrent invocations possible, without using a worker pool or different processes.
It can be useful in cases where you don't need a worker pool but still being able to limit concurrent calls without much overhead.
2020-05-16 10:17:40 +00:00
As it internally uses `persistent_term` to store metadata, it is not made for a large number of different or dynamic limiters and
cannot be used for things like a per-user rate limiter.
2020-05-08 18:10:33 +00:00
2020-05-16 10:17:40 +00:00
```elixir
:ok = ConcurrentLimiter.new(RequestLimiter, 10, 10)
ConcurrentLimiter.limit(RequestLimiter, fn() -> something_that_can_only_run_ten_times_concurrently() end)
```
2020-05-08 18:10:33 +00:00
"""
@default_wait 150
@default_max_retries 5
2020-05-16 10:17:40 +00:00
@doc "Initializes a `ConcurrentLimiter`."
@spec new(name, max_running, max_waiting, options) :: :ok | {:error, :existing}
when name: atom(),
max_running: non_neg_integer(),
max_waiting: non_neg_integer() | :infinity,
options: [option],
option: {:wait, non_neg_integer()} | {:max_retries, non_neg_integer()}
2020-05-08 18:10:33 +00:00
def new(name, max_running, max_waiting, options \\ []) do
2020-05-14 20:14:09 +00:00
name = prefix_name(name)
2020-05-16 10:17:40 +00:00
2020-05-08 18:10:33 +00:00
if defined?(name) do
{:error, :existing}
else
wait = Keyword.get(options, :wait, @default_wait)
max_retries = Keyword.get(options, :max_retries, @default_max_retries)
2020-05-16 10:17:40 +00:00
atomics = :atomics.new(1, signed: true)
:persistent_term.put(
name,
{__MODULE__, max_running, max_waiting, atomics, wait, max_retries}
)
2020-05-08 18:10:33 +00:00
:ok
end
end
2020-05-16 10:17:40 +00:00
@doc "Adjust the limits at runtime."
@spec set(name, new_max_running, new_max_waiting, options) :: :ok | :error
when name: atom(),
new_max_running: non_neg_integer(),
new_max_waiting: non_neg_integer() | :infinity,
options: [option],
option: {:wait, non_neg_integer()}
2020-05-08 18:10:33 +00:00
def set(name, new_max_running, new_max_waiting, options \\ []) do
2020-05-14 20:14:09 +00:00
name = prefix_name(name)
2020-05-16 10:17:40 +00:00
2020-05-08 18:10:33 +00:00
if defined?(name) do
new_wait = Keyword.get(options, :wait)
new_max_retries = Keyword.get(options, :max_retries)
{__MODULE__, max_running, max_waiting, ref, wait, max_retries} = :persistent_term.get(name)
2020-05-16 10:17:40 +00:00
new =
{__MODULE__, new_max_running || max_running, new_max_waiting || max_waiting, ref,
new_wait || wait, new_max_retries || max_retries}
2020-05-08 18:10:33 +00:00
:persistent_term.put(name, new)
:ok
else
:error
end
2020-05-07 23:55:49 +00:00
end
@spec delete(name) :: :ok when name: atom()
@doc "Deletes a limiter."
def delete(name) do
if defined?(name) do
:persistent_term.put(name, nil)
end
:ok
end
2020-05-08 18:10:33 +00:00
@doc "Limits invocation of `fun`."
@spec limit(name, function(), opts) :: {:error, :overload} | any()
when name: atom(),
opts: [option],
2020-05-16 10:17:40 +00:00
option: {:wait, non_neg_integer()} | {:max_retries, non_neg_integer()}
def limit(name, fun, opts \\ []) do
do_limit(prefix_name(name), fun, opts, 0)
2020-05-08 18:10:33 +00:00
end
defp do_limit(name, fun, opts, retries) do
{__MODULE__, max_running, max_waiting, ref, wait, max_retries} = :persistent_term.get(name)
2020-05-07 23:55:49 +00:00
max = max_running + max_waiting
counter = inc(ref, name)
max_retries = Keyword.get(opts, :max_retries) || max_retries
:telemetry.execute([:concurrent_limiter, :limit], %{counter: counter}, %{limiter: name})
2020-05-07 23:55:49 +00:00
cond do
counter <= max_running ->
:telemetry.execute([:concurrent_limiter, :execution], %{counter: counter}, %{limiter: name})
2020-05-08 18:10:33 +00:00
try do
fun.()
after
dec(ref, name)
2020-05-08 18:10:33 +00:00
end
2020-05-07 23:55:49 +00:00
counter > max ->
:telemetry.execute([:concurrent_limiter, :overload], %{counter: counter}, %{limiter: name, scope: "max"})
dec(ref, name)
{:error, :overload}
retries + 1 > max_retries ->
:telemetry.execute([:concurrent_limiter, :max_retries], %{counter: counter}, %{limiter: name, retries: retries + 1})
dec(ref, name)
2020-05-07 23:55:49 +00:00
{:error, :overload}
counter > max_running ->
:telemetry.execute([:concurrent_limiter, :wait], %{counter: counter}, %{limiter: name, retries: retries + 1})
2020-05-16 10:17:40 +00:00
wait(ref, name, fun, wait, opts, retries + 1)
2020-05-07 23:55:49 +00:00
end
end
2020-05-16 10:17:40 +00:00
defp wait(ref, name, fun, wait, opts, retries) do
wait = Keyword.get(opts, :timeout) || wait
2020-05-08 18:10:33 +00:00
Process.sleep(wait)
dec(ref, name)
do_limit(name, fun, opts, retries)
2020-05-07 23:55:49 +00:00
end
defp inc(ref, _) do
2020-05-08 18:10:33 +00:00
:atomics.add_get(ref, 1, 1)
2020-05-07 23:55:49 +00:00
end
defp dec(ref, _) do
2020-05-08 18:10:33 +00:00
:atomics.sub_get(ref, 1, 1)
end
2020-05-14 20:14:09 +00:00
defp prefix_name(suffix), do: Module.concat(__MODULE__, suffix)
2020-05-08 18:10:33 +00:00
defp defined?(name) do
{__MODULE__, _, _, _, _, _} = :persistent_term.get(name)
true
rescue
_ -> false
end
2020-05-07 23:55:49 +00:00
end