diff options
author | Jordan Bracco <href@random.sh> | 2020-05-14 22:14:09 +0200 |
---|---|---|
committer | Jordan Bracco <href@random.sh> | 2020-05-14 22:14:09 +0200 |
commit | 9f95ba8f6502d442e89f96b856ebf77fecda9ef4 (patch) | |
tree | 07c4db7ae6fbd981f59f63d36035fc2312e4c61e | |
parent | minor fixes (diff) |
Rename to ConcurrentLimiter
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | lib/concurrent_limiter.ex (renamed from lib/limiter.ex) | 26 | ||||
-rw-r--r-- | mix.exs | 4 | ||||
-rw-r--r-- | test/concurrent_limiter_test.exs (renamed from test/limiter_test.exs) | 10 | ||||
-rw-r--r-- | test/samples/limiter.exs | 24 | ||||
-rw-r--r-- | test/samples/multi_limiter.exs | 88 | ||||
-rw-r--r-- | test/samples/results_multi_limiter.txt | 60 | ||||
-rw-r--r-- | test/samples/update_counter.exs | 27 |
8 files changed, 132 insertions, 111 deletions
@@ -1,3 +1,3 @@ -# Limiter +# Concurrent Limiter -See the docs in `lib/limiter.ex`. +See the docs in `lib/concurrent_limiter.ex`. diff --git a/lib/limiter.ex b/lib/concurrent_limiter.ex index f8789a4..9776377 100644 --- a/lib/limiter.ex +++ b/lib/concurrent_limiter.ex @@ -1,21 +1,23 @@ -defmodule Limiter do +defmodule ConcurrentLimiter do require Logger @moduledoc """ - # Limiter + # Concurrent Limiter 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. As it internally uses `persistent_term` to store metadata, and can fallback to ETS tables, it is however not made for a large number of limiters and cannot be used for things like a per-user rate limiter. + It supports two storage methods: * **[atomics](https://erlang.org/doc/man/atomics.html)** recommended and default if your OTP is > 21.2. - * **[ets](https://erlang.org/doc/man/ets.html)** either with a single table per Limiter (faster) or a shared table. + * **[ets](https://erlang.org/doc/man/ets.html)** either with a single table per limiter (faster) or a shared table. - You would however always want to use atomics, ets is mostly there for backwards compatibility. + You would almost always want to use atomics, ets is mostly there for backwards compatibility. """ @doc """ - Initializes a `Limiter`. + Initializes a `ConcurrentLimiter`. """ @spec new(name, max_running, max_waiting, options) :: :ok | {:error, :existing} when name: atom(), @@ -24,9 +26,9 @@ defmodule Limiter do options: [option], option: {:wait, non_neg_integer()} | backend, backend: :atomics | ets_backend, - ets_backend: :ets | {:ets, atom()} | {:ets, ets_name :: atom(), ets_options :: []} + ets_backend: :ets | {:ets, atom()} | {:ets, atom(), ets_options :: []} def new(name, max_running, max_waiting, options \\ []) do - name = atom_name(name) + name = prefix_name(name) if defined?(name) do {:error, :existing} else @@ -45,7 +47,7 @@ defmodule Limiter do option: {:wait, non_neg_integer()} @doc "Adjust the limiter limits at runtime" def set(name, new_max_running, new_max_waiting, options \\ []) do - name = atom_name(name) + name = prefix_name(name) if defined?(name) do new_wait = Keyword.get(options, :wait) {__MODULE__, max_running, max_waiting, backend, wait} = :persistent_term.get(name) @@ -60,7 +62,7 @@ defmodule Limiter do @spec limit(atom(), function()) :: {:error, :overload} | any() @doc "Limits invocation of `fun`." def limit(name, fun) do - do_limit(atom_name(name), fun) + do_limit(prefix_name(name), fun) end defp do_limit(name, fun) do @@ -107,7 +109,7 @@ defmodule Limiter do :atomics.sub_get(ref, 1, 1) end - defp atom_name(suffix), do: Module.concat(__MODULE__, suffix) + defp prefix_name(suffix), do: Module.concat(__MODULE__, suffix) defp defined?(name) do {__MODULE__, _, _, _, _, _} = :persistent_term.get(name) @@ -120,7 +122,7 @@ defmodule Limiter do if Code.ensure_loaded?(:atomics) do :atomics else - Logger.debug("Limiter: atomics not available, using ETS backend") + Logger.debug("ConcurrentLimiter: atomics not available, using ETS backend") :ets end end @@ -134,7 +136,7 @@ defmodule Limiter do end defp setup_backend({:ets, name, options}) do - ets_name = atom_name(name) + ets_name = prefix_name(name) case :ets.whereis(ets_name) do :undefined -> :ets.new(ets_name, [:public, :named_table] ++ options) @@ -1,9 +1,9 @@ -defmodule Limiter.MixProject do +defmodule ConcurrentLimiter.MixProject do use Mix.Project def project do [ - app: :limiter, + app: :concurrent_limiter, version: "0.1.0", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, diff --git a/test/limiter_test.exs b/test/concurrent_limiter_test.exs index 1b81ead..e1d281e 100644 --- a/test/limiter_test.exs +++ b/test/concurrent_limiter_test.exs @@ -1,16 +1,16 @@ -defmodule LimiterTest do +defmodule ConcurrentLimiterTest do use ExUnit.Case - doctest Limiter + doctest ConcurrentLimiter test "limiter ets is atomic" do name = "test1" - Limiter.new(name, 2, 2) + ConcurrentLimiter.new(name, 2, 2) atomic_test(name) end test "limiter atomics is atomic" do name = "test2" - Limiter.new(name, 2, 2, backend: :atomics) + ConcurrentLimiter.new(name, 2, 2, backend: :atomics) atomic_test(name) end @@ -18,7 +18,7 @@ defmodule LimiterTest do self = self() sleepy = fn sleep -> - case Limiter.limit(name, fn -> + case ConcurrentLimiter.limit(name, fn -> send(self, :ok) Process.sleep(sleep) :ok diff --git a/test/samples/limiter.exs b/test/samples/limiter.exs index 785c85f..f903658 100644 --- a/test/samples/limiter.exs +++ b/test/samples/limiter.exs @@ -1,24 +1,24 @@ infinite = 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000 -Limiter.new(:bench, infinite, 0) -Limiter.new(:bench_s, infinite, 0, ets: LimiterTest) +ConcurrentLimiter.new(:bench, infinite, 0) +ConcurrentLimiter.new(:bench_s, infinite, 0, ets: ConcurrentLimiterTest) concurrent = [{:read_concurrency, true}, {:write_concurrency, true}] -Limiter.new(:bench_rw, infinite, 0) -Limiter.new(:bench_s_rw, infinite, 0, ets: LimiterTest, ets_opts: concurrent) +ConcurrentLimiter.new(:bench_rw, infinite, 0) +ConcurrentLimiter.new(:bench_s_rw, infinite, 0, ets: ConcurrentLimiterTest, ets_opts: concurrent) single = %{ - "Limiter.limit/2" => fn -> - Limiter.limit(:bench, fn -> :ok end) + "ConcurrentLimiter.limit/2" => fn -> + ConcurrentLimiter.limit(:bench, fn -> :ok end) end, - "Limiter.limit/2 with concurrency" => fn -> - Limiter.limit(:bench_rw, fn -> :ok end) + "ConcurrentLimiter.limit/2 with concurrency" => fn -> + ConcurrentLimiter.limit(:bench_rw, fn -> :ok end) end, - "Limiter:limit/2 with shared ets" => fn -> - Limiter.limit(:bench_s, fn -> :ok end) + "ConcurrentLimiter:limit/2 with shared ets" => fn -> + ConcurrentLimiter.limit(:bench_s, fn -> :ok end) end, - "Limiter:limit/2 with shared ets and concurrency" => fn -> - Limiter.limit(:bench_s_rw, fn -> :ok end) + "ConcurrentLimiter:limit/2 with shared ets and concurrency" => fn -> + ConcurrentLimiter.limit(:bench_s_rw, fn -> :ok end) end } diff --git a/test/samples/multi_limiter.exs b/test/samples/multi_limiter.exs index abec65d..f56cb6c 100644 --- a/test/samples/multi_limiter.exs +++ b/test/samples/multi_limiter.exs @@ -1,52 +1,70 @@ infinite = 1_000_000_000_000_000_000_000_000_000_000_000_000_000_000_000 +parallel = case Integer.parse(System.get_env("PARALLEL", "")) do + {int, _} -> int + _ -> System.schedulers_online()/2 +end -Limiter.new(:bench_u_0, infinite, 0, backend: {:ets, LimiterTest0, []}) -Limiter.new(:bench_u_1, infinite, 0, backend: {:ets, LimiterTest1, []}) -Limiter.new(:bench_u_2, infinite, 0, backend: {:ets, LimiterTest2, []}) -Limiter.new(:bench_u_3, infinite, 0, backend: {:ets, LimiterTest3, []}) +multi_count = case Integer.parse(System.get_env("MULTI", "")) do + {int, _} -> int + _ -> parallel +end -Limiter.new(:bench_a_0, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_1, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_2, infinite, 0, backend: :atomics) -Limiter.new(:bench_a_3, infinite, 0, backend: :atomics) +names = fn(prefix) -> + for i <- 1..multi_count do + Module.concat(MultiConcurrentLimiterBenchmark, "#{prefix}#{i}") + end +end + + +bench_unique = for name <- names.("u") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, name, []}) + name +end + +IO.inspect(bench_unique) + +bench_atomics = for name <- names.("a") do + ConcurrentLimiter.new(name, infinite, 0, backend: :atomics) + name +end -Limiter.new(:bench_s_0, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_1, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_2, infinite, 0, backend: {:ets, LimiterTest, []}) -Limiter.new(:bench_s_3, infinite, 0, backend: {:ets, LimiterTest, []}) +bench_shared = for name <- names.("s") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, ConcurrentLimiterTest, []}) + name +end rw = [{:read_concurrency, true}, {:write_concurrency, true}] -Limiter.new(:bench_u_rw0, infinite, 0, backend: {:ets, LimiterTestRW0, rw}) -Limiter.new(:bench_u_rw1, infinite, 0, backend: {:ets, LimiterTestRW1, rw}) -Limiter.new(:bench_u_rw2, infinite, 0, backend: {:ets, LimiterTestRW2, rw}) -Limiter.new(:bench_u_rw3, infinite, 0, backend: {:ets, LimiterTestRW3, rw}) +bench_unique_rw = for name <- names.("u_rw") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, name, rw}) + name +end -Limiter.new(:bench_s_rw0, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw1, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw2, infinite, 0, backend: {:ets, LimiterTestRW, rw}) -Limiter.new(:bench_s_rw3, infinite, 0, backend: {:ets, LimiterTestRW, rw}) +bench_shared_rw = for name <- names.("s_rw") do + ConcurrentLimiter.new(name, infinite, 0, backend: {:ets, ConcurrentLimiterTestRW, rw}) + name +end multiple = %{ - "Limiter.limit/2 unique ets" => fn -> - limiter = Enum.random([:bench_u_0, :bench_u_1, :bench_u_2, :bench_u_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter.limit/2 unique ets" => fn -> + limiter = Enum.random(bench_unique) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 shared ets" => fn -> - limiter = Enum.random([:bench_s_0, :bench_s_1, :bench_s_2, :bench_s_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 shared ets" => fn -> + limiter = Enum.random(bench_shared) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter.limit/2 unique ets, concurrency" => fn -> - limiter = Enum.random([:bench_u_rw0, :bench_u_rw1, :bench_u_rw2, :bench_u_rw3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter.limit/2 unique ets, concurrency" => fn -> + limiter = Enum.random(bench_unique_rw) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 shared ets, concurrency" => fn -> - limiter = Enum.random([:bench_s_rw0, :bench_s_rw1, :bench_s_rw2, :bench_s_rw3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 shared ets, concurrency" => fn -> + limiter = Enum.random(bench_shared_rw) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end, - "Limiter:limit/2 atomics" => fn -> - limiter = Enum.random([:bench_a_0, :bench_a_1, :bench_a_2, :bench_a_3]) - Limiter.limit(limiter, fn -> :ok end) + "ConcurrentLimiter:limit/2 atomics" => fn -> + limiter = Enum.random(bench_atomics) + ConcurrentLimiter.limit(limiter, fn -> :ok end) end } diff --git a/test/samples/results_multi_limiter.txt b/test/samples/results_multi_limiter.txt index 112c325..75704da 100644 --- a/test/samples/results_multi_limiter.txt +++ b/test/samples/results_multi_limiter.txt @@ -22,25 +22,25 @@ parallel: 1 inputs: none specified Estimated total run time: 35 s -Benchmarking Limiter.limit/2 unique ets... -Benchmarking Limiter.limit/2 unique ets, concurrency... -Benchmarking Limiter:limit/2 atomics... -Benchmarking Limiter:limit/2 shared ets... -Benchmarking Limiter:limit/2 shared ets, concurrency... +Benchmarking ConcurrentLimiter.limit/2 unique ets... +Benchmarking ConcurrentLimiter.limit/2 unique ets, concurrency... +Benchmarking ConcurrentLimiter:limit/2 atomics... +Benchmarking ConcurrentLimiter:limit/2 shared ets... +Benchmarking ConcurrentLimiter:limit/2 shared ets, concurrency... Name ips average deviation median 99th % -Limiter:limit/2 atomics 491.88 K 2.03 μs ±1506.30% 1.55 μs 3.48 μs -Limiter.limit/2 unique ets 414.63 K 2.41 μs ±1169.34% 1.97 μs 4.53 μs -Limiter:limit/2 shared ets 411.43 K 2.43 μs ±1286.95% 1.96 μs 3.66 μs -Limiter.limit/2 unique ets, concurrency 406.50 K 2.46 μs ±1006.31% 2.06 μs 4.34 μs -Limiter:limit/2 shared ets, concurrency 384.04 K 2.60 μs ±1293.25% 2.12 μs 4.37 μs +ConcurrentLimiter:limit/2 atomics 491.88 K 2.03 μs ±1506.30% 1.55 μs 3.48 μs +ConcurrentLimiter.limit/2 unique ets 414.63 K 2.41 μs ±1169.34% 1.97 μs 4.53 μs +ConcurrentLimiter:limit/2 shared ets 411.43 K 2.43 μs ±1286.95% 1.96 μs 3.66 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 406.50 K 2.46 μs ±1006.31% 2.06 μs 4.34 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 384.04 K 2.60 μs ±1293.25% 2.12 μs 4.37 μs Comparison: -Limiter:limit/2 atomics 491.88 K -Limiter.limit/2 unique ets 414.63 K - 1.19x slower +0.38 μs -Limiter:limit/2 shared ets 411.43 K - 1.20x slower +0.40 μs -Limiter.limit/2 unique ets, concurrency 406.50 K - 1.21x slower +0.43 μs -Limiter:limit/2 shared ets, concurrency 384.04 K - 1.28x slower +0.57 μs +ConcurrentLimiter:limit/2 atomics 491.88 K +ConcurrentLimiter.limit/2 unique ets 414.63 K - 1.19x slower +0.38 μs +ConcurrentLimiter:limit/2 shared ets 411.43 K - 1.20x slower +0.40 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 406.50 K - 1.21x slower +0.43 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 384.04 K - 1.28x slower +0.57 μs @@ -65,22 +65,22 @@ parallel: 8 inputs: none specified Estimated total run time: 35 s -Benchmarking Limiter.limit/2 unique ets... -Benchmarking Limiter.limit/2 unique ets, concurrency... -Benchmarking Limiter:limit/2 atomics... -Benchmarking Limiter:limit/2 shared ets... -Benchmarking Limiter:limit/2 shared ets, concurrency... +Benchmarking ConcurrentLimiter.limit/2 unique ets... +Benchmarking ConcurrentLimiter.limit/2 unique ets, concurrency... +Benchmarking ConcurrentLimiter:limit/2 atomics... +Benchmarking ConcurrentLimiter:limit/2 shared ets... +Benchmarking ConcurrentLimiter:limit/2 shared ets, concurrency... Name ips average deviation median 99th % -Limiter:limit/2 atomics 307.84 K 3.25 μs ±1113.62% 2.09 μs 10.24 μs -Limiter.limit/2 unique ets, concurrency 95.56 K 10.46 μs ±391.37% 2.93 μs 163.02 μs -Limiter:limit/2 shared ets, concurrency 92.39 K 10.82 μs ±374.36% 2.92 μs 158.97 μs -Limiter.limit/2 unique ets 80.68 K 12.39 μs ±362.74% 2.85 μs 160.66 μs -Limiter:limit/2 shared ets 6.04 K 165.66 μs ±17.23% 167.48 μs 237.96 μs +ConcurrentLimiter:limit/2 atomics 307.84 K 3.25 μs ±1113.62% 2.09 μs 10.24 μs +ConcurrentLimiter.limit/2 unique ets, concurrency 95.56 K 10.46 μs ±391.37% 2.93 μs 163.02 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 92.39 K 10.82 μs ±374.36% 2.92 μs 158.97 μs +ConcurrentLimiter.limit/2 unique ets 80.68 K 12.39 μs ±362.74% 2.85 μs 160.66 μs +ConcurrentLimiter:limit/2 shared ets 6.04 K 165.66 μs ±17.23% 167.48 μs 237.96 μs Comparison: -Limiter:limit/2 atomics 307.84 K -Limiter.limit/2 unique ets, concurrency 95.56 K - 3.22x slower +7.22 μs -Limiter:limit/2 shared ets, concurrency 92.39 K - 3.33x slower +7.57 μs -Limiter.limit/2 unique ets 80.68 K - 3.82x slower +9.15 μs -Limiter:limit/2 shared ets 6.04 K - 51.00x slower +162.41 μs +ConcurrentLimiter:limit/2 atomics 307.84 K +ConcurrentLimiter.limit/2 unique ets, concurrency 95.56 K - 3.22x slower +7.22 μs +ConcurrentLimiter:limit/2 shared ets, concurrency 92.39 K - 3.33x slower +7.57 μs +ConcurrentLimiter.limit/2 unique ets 80.68 K - 3.82x slower +9.15 μs +ConcurrentLimiter:limit/2 shared ets 6.04 K - 51.00x slower +162.41 μs diff --git a/test/samples/update_counter.exs b/test/samples/update_counter.exs index 1768735..6310c57 100644 --- a/test/samples/update_counter.exs +++ b/test/samples/update_counter.exs @@ -1,19 +1,20 @@ :ets.new(:limiter_bench, [:public, :named_table]) +:ets.new(:limiter_bench_concurrent, [:public, :named_table, {:read_concurrency, false}, {:write_concurrency, true}]) +atomics = :atomics.new(1, []) -Benchee.run( +update_counter = %{ "ets:update_counter" => fn -> :ets.update_counter(:limiter_bench, "bench", {2, 1}, {"bench", 0}) - end - }, - parallel: 1 -) - -Benchee.run( - %{ - "ets:update_counter" => fn -> + end, + "ets:update_counter concurrent" => fn -> :ets.update_counter(:limiter_bench, "bench", {2, 1}, {"bench", 0}) - end - }, - parallel: System.schedulers_online() -) + end, + "atomics:add_get" => fn -> + :atomics.add_get(atomics, 1, 1) + end, + } + +Benchee.run(update_counter, parallel: 1) +Benchee.run(update_counter, parallel: System.schedulers_online()) + |