From 9e84826ee96083f16757d5516f4a6820b1a77110 Mon Sep 17 00:00:00 2001 From: Adrien Moreau Date: Wed, 1 Dec 2021 16:08:02 +0800 Subject: [PATCH] Add rate limiter --- .github/workflows/elixir.yml | 14 ++-- .gitignore | 3 + README.md | 24 +++++-- lib/http_client.ex | 90 ++++++-------------------- lib/http_client/rate_limiter.ex | 50 ++++++++++++++ mix.exs | 11 ++-- mix.lock | 43 ++++++------ test/http_client/rate_limiter_test.exs | 25 +++++++ 8 files changed, 151 insertions(+), 109 deletions(-) create mode 100644 lib/http_client/rate_limiter.ex create mode 100644 test/http_client/rate_limiter_test.exs diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 5e67c8e..ae2982d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -10,14 +10,16 @@ jobs: build: runs-on: ubuntu-latest - + strategy: + matrix: + otp: ['24.1'] + elixir: ['1.12.3'] steps: - uses: actions/checkout@v2 - - name: Setup elixir - uses: actions/setup-elixir@v1 + - uses: erlef/setup-beam@v1 with: - elixir-version: 1.10.3 - otp-version: 22.2 + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} - name: deps cache uses: actions/cache@v1 with: @@ -32,7 +34,7 @@ jobs: key: ${{ runner.os }}-build-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} restore-keys: | ${{ runner.os }}-build- - - name: dializer cache + - name: dialyzer cache uses: actions/cache@v1 with: path: deps diff --git a/.gitignore b/.gitignore index 2475f14..0f730f6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ erl_crash.dump http_client-*.tar .elixir_ls/ + +*.plt +*.xml diff --git a/README.md b/README.md index ab3102e..6e18b79 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,31 @@ # HttpClient -**TODO: Add description** +Httpoison boosted with telemetry, mox and ex_rated. + +```elixir +HttpClient.get("http://mydomain.com") +``` ## Installation -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `http_client` to your list of dependencies in `mix.exs`: +The package can be installed by adding `http_client` to your list of dependencies +in `mix.exs`: ```elixir def deps do [ - {:http_client, "~> 0.1.0"} + {:http_client, "~> 0.2.3"} ] end ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at [https://hexdocs.pm/http_client](https://hexdocs.pm/http_client). +The docs can be found at [https://hexdocs.pm/http_client](https://hexdocs.pm/http_client). + +## Configuration + +```elixir +# Will rate limit calls to mydomain.com to 5 per seconds +# HttpClient.get("http://mydomain.com/any_path") +config :http_client, :rate_limits, "mydomain.com": [{:timer.seconds(1), 5}] +``` diff --git a/lib/http_client.ex b/lib/http_client.ex index 5ada143..f0bfec9 100644 --- a/lib/http_client.ex +++ b/lib/http_client.ex @@ -1,6 +1,8 @@ defmodule HttpClient do @moduledoc false alias HttpClient.Instrumenter + alias HttpClient.RateLimiter + @callback get(String.t()) :: {:ok, any()} | {:error, any()} @callback get(String.t(), list()) :: {:ok, any()} | {:error, any()} @callback get(String.t(), list(), Keyword.t()) :: {:ok, any()} | {:error, any()} @@ -17,87 +19,35 @@ defmodule HttpClient do @callback delete(String.t(), list()) :: {:ok, any()} | {:error, any()} @callback delete(String.t(), list(), Keyword.t()) :: {:ok, any()} | {:error, any()} - def get(url) do - :timer.tc(fn -> - impl().get(url) - end) - |> Instrumenter.instrument(url) - end + def get(url), do: request(:get, [url]) - def get(url, headers) do - :timer.tc(fn -> - impl().get(url, headers) - end) - |> Instrumenter.instrument(url) - end + def get(url, headers), do: request(:get, [url, headers]) - def get(url, headers, opts) do - :timer.tc(fn -> - impl().get(url, headers, opts) - end) - |> Instrumenter.instrument(url) - end + def get(url, headers, options), do: request(:get, [url, headers, options]) - def post(url, body) do - :timer.tc(fn -> - impl().post(url, body) - end) - |> Instrumenter.instrument(url) - end + def post(url, body), do: request(:post, [url, body]) - def post(url, body, headers) do - :timer.tc(fn -> - impl().post(url, body, headers) - end) - |> Instrumenter.instrument(url) - end + def post(url, body, headers), do: request(:post, [url, body, headers]) - def post(url, body, headers, opts) do - :timer.tc(fn -> - impl().post(url, body, headers, opts) - end) - |> Instrumenter.instrument(url) - end + def post(url, body, headers, options), do: request(:post, [url, body, headers, options]) - def put(url, body) do - :timer.tc(fn -> - impl().put(url, body) - end) - |> Instrumenter.instrument(url) - end + def put(url, body), do: request(:put, [url, body]) - def put(url, body, headers) do - :timer.tc(fn -> - impl().put(url, body, headers) - end) - |> Instrumenter.instrument(url) - end + def put(url, body, headers), do: request(:put, [url, body, headers]) - def put(url, body, headers, opts) do - :timer.tc(fn -> - impl().put(url, body, headers, opts) - end) - |> Instrumenter.instrument(url) - end + def put(url, body, headers, options), do: request(:put, [url, body, headers, options]) - def delete(url) do - :timer.tc(fn -> - impl().delete(url) - end) - |> Instrumenter.instrument(url) - end + def delete(url), do: request(:delete, [url]) - def delete(url, headers) do - :timer.tc(fn -> - impl().delete(url, headers) - end) - |> Instrumenter.instrument(url) - end + def delete(url, headers), do: request(:delete, [url, headers]) + + def delete(url, headers, options), do: request(:delete, [url, headers, options]) + + defp request(method, [url | _] = args) do + :ok = RateLimiter.rate_limit(url) - def delete(url, headers, options) do - :timer.tc(fn -> - impl().delete(url, headers, options) - end) + fn -> apply(impl(), method, args) end + |> :timer.tc() |> Instrumenter.instrument(url) end diff --git a/lib/http_client/rate_limiter.ex b/lib/http_client/rate_limiter.ex new file mode 100644 index 0000000..a43e9f1 --- /dev/null +++ b/lib/http_client/rate_limiter.ex @@ -0,0 +1,50 @@ +defmodule HttpClient.RateLimiter do + @moduledoc """ + Module used to rate limit calls to certain host + + In `config.exs` : + + config :http_client, :rate_limits, + "google.com": [{:timer.seconds(1), 5}, {:timer.hours(24), 500_000}] + """ + + @spec rate_limit(String.t()) :: :ok + def rate_limit(url) do + case retrieve_rate_limit_config(url) do + {:ok, {host, rate_limits}} -> + :ok = enforce_rate_limits(host, rate_limits) + + :error -> + :ok + end + end + + @spec enforce_rate_limits(atom(), list({non_neg_integer(), non_neg_integer()})) :: :ok + defp enforce_rate_limits(_host, []), do: :ok + + defp enforce_rate_limits(host, [{scale, limit} | remaining_limits] = rate_limits) do + bucket = {host, scale} + {_, _, ms_to_next_bucket, _, _} = ExRated.inspect_bucket(bucket, scale, limit) + + case ExRated.check_rate(bucket, scale, limit) do + {:ok, _} -> + enforce_rate_limits(host, remaining_limits) + + _ -> + Process.sleep(ms_to_next_bucket) + enforce_rate_limits(host, rate_limits) + end + end + + @spec retrieve_rate_limit_config(String.t()) :: + {:ok, {atom(), list({non_neg_integer(), non_neg_integer()})}} | :error + defp retrieve_rate_limit_config(url) do + %URI{host: host} = URI.parse(url) + host_atom = String.to_existing_atom(host) + {:ok, rate_limits} = Application.fetch_env(:http_client, :rate_limits) + {:ok, value} = Keyword.fetch(rate_limits, host_atom) + {:ok, {host_atom, value}} + rescue + _ -> :error + end +end diff --git a/mix.exs b/mix.exs index 8a72c84..26e34bc 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule HttpClient.MixProject do def project do [ app: :http_client, - version: "0.2.2", + version: "0.2.3", elixir: "~> 1.10", start_permanent: Mix.env() == :prod, test_coverage: [tool: ExCoveralls], @@ -20,18 +20,15 @@ defmodule HttpClient.MixProject do ] end - # Run "mix help compile.app" to learn about applications. def application do - [ - extra_applications: [:logger] - ] + [extra_applications: [:logger]] end - # Run "mix help deps" to learn about dependencies. defp deps do [ {:httpoison, "~> 1.5"}, {:telemetry, "~> 0.4.0"}, + {:ex_rated, "~> 2.0"}, {:mox, "~> 0.5", only: :test}, {:excoveralls, "~> 0.12", only: [:test]}, {:ex_unit_sonarqube, "~> 0.1.2", only: [:dev, :test]}, @@ -44,7 +41,7 @@ defmodule HttpClient.MixProject do end defp description do - "Httpoison boosted with telemetry and mox." + "Httpoison boosted with telemetry, mox and ex_rated." end defp package do diff --git a/mix.lock b/mix.lock index 087d98e..af0380c 100644 --- a/mix.lock +++ b/mix.lock @@ -1,29 +1,34 @@ %{ "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, - "credo": {:hex, :credo, "1.3.1", "082e8d9268a489becf8e7aa75671a7b9088b1277cd6c1b13f40a55554b3f5126", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "0da816ed52fa520b9ea0e5d18a0d3ca269e0bd410b1174d88d8abd94be6cce3c"}, - "dialyxir": {:hex, :dialyxir, "1.0.0", "6a1fa629f7881a9f5aaf3a78f094b2a51a0357c843871b8bc98824e7342d00a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "aeb06588145fac14ca08d8061a142d52753dbc2cf7f0d00fc1013f53f8654654"}, + "certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"}, + "credo": {:hex, :credo, "1.6.1", "7dc76dcdb764a4316c1596804c48eada9fff44bd4b733a91ccbf0c0f368be61e", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "698607fb5993720c7e93d2d8e76f2175bba024de964e160e2f7151ef3ab82ac5"}, + "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.17", "6f3c7e94170377ba45241d394389e800fb15adc5de51d0a3cd52ae766aafd63f", [:mix], [], "hexpm", "f93ac89c9feca61c165b264b5837bf82344d13bebc634cd575cb711e2e342023"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, - "ex_unit_sonarqube": {:hex, :ex_unit_sonarqube, "0.1.2", "fe849022391f47229daf40062d86410a335a8ee413d227bb0811da8989cb48f6", [:mix], [], "hexpm", "f5bbc838371371e7e07e044ecc8eb070bdc5cf5a6383cfec16c80015949e990d"}, - "excoveralls": {:hex, :excoveralls, "0.12.3", "2142be7cb978a3ae78385487edda6d1aff0e482ffc6123877bb7270a8ffbcfe0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "568a3e616c264283f5dea5b020783ae40eef3f7ee2163f7a67cbd7b35bcadada"}, - "git_hooks": {:hex, :git_hooks, "0.4.1", "8d0873c9145cce67f5c794dbf7c262a9855cf86b984d6079646b36e182d09f50", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.6.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "9765388245f10db1f76b8f29dbee08d49666c712bda699787d3a141c6db2c8a8"}, - "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, - "httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"}, - "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"}, - "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, - "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, + "ex2ms": {:hex, :ex2ms, "1.6.1", "66d472eb14da43087c156e0396bac3cc7176b4f24590a251db53f84e9a0f5f72", [:mix], [], "hexpm", "a7192899d84af03823a8ec2f306fa858cbcce2c2e7fd0f1c49e05168fb9c740e"}, + "ex_doc": {:hex, :ex_doc, "0.26.0", "1922164bac0b18b02f84d6f69cab1b93bc3e870e2ad18d5dacb50a9e06b542a3", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2775d66e494a9a48355db7867478ffd997864c61c65a47d31c4949459281c78d"}, + "ex_rated": {:hex, :ex_rated, "2.0.1", "49b4c170039fc62fa93ea28df16e3586e98c2fe0aec10f75e6717fba8039637f", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "2f675b649f74028842ae3d1f0c5090f8a664682df98c82836db6f1d321eaa42a"}, + "ex_unit_sonarqube": {:hex, :ex_unit_sonarqube, "0.1.3", "6277aaddb6caff32402f227cd8390eca8ffe00217b4bc7974a477346c84a0119", [:mix], [], "hexpm", "e41b03efe9a0019f63f2f6309b02c8e443b0a36eb4cf6c9bc9593eb5f5de4882"}, + "excoveralls": {:hex, :excoveralls, "0.14.4", "295498f1ae47bdc6dce59af9a585c381e1aefc63298d48172efaaa90c3d251db", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e3ab02f2df4c1c7a519728a6f0a747e71d7d6e846020aae338173619217931c1"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "git_hooks": {:hex, :git_hooks, "0.6.4", "a20757c39b2b9ab75d894fc3eaed8fbb1c45e207fd19f12f45fe9b5327910817", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.7.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "a44c76f3d7bdaf885d6f0bf5b3f3e86922b5edf28ee2e6235aa6e561bf172b1c"}, + "hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"}, + "httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"}, + "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, + "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm", "052346cf322311c49a0f22789f3698eea030eec09b8c47367f0686ef2634ae14"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, - "recase": {:hex, :recase, "0.6.0", "1dd2dd2f4e06603b74977630e739f08b7fedbb9420cc14de353666c2fc8b99f4", [:mix], [], "hexpm", "8712e318420a228eb2e6366ada230148ed3a4316a798319edd5512f64d78c990"}, - "sobelow": {:hex, :sobelow, "0.9.3", "fcadf0a3fb7be0809825b7cca2f3430973fa8b03d71ecc0c65b70525f636a835", [:mix], [], "hexpm", "75f91a07bfb86b5c79d026d7349884d82e2841c4e650132416b8f28f70ead806"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, + "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, + "recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, + "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm", "e9e3cacfd37c1531c0ca70ca7c0c30ce2dbb02998a4f7719de180fe63f8d41e4"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, } diff --git a/test/http_client/rate_limiter_test.exs b/test/http_client/rate_limiter_test.exs new file mode 100644 index 0000000..19bd5fa --- /dev/null +++ b/test/http_client/rate_limiter_test.exs @@ -0,0 +1,25 @@ +defmodule HttpClient.RateLimiterTest do + use ExUnit.Case + + alias HttpClient.RateLimiter + + test "rate_limit(url) | no config" do + :ok = RateLimiter.rate_limit("http://mydomain.com") + end + + test "rate_limit(url) | config available" do + Application.put_env(:http_client, :rate_limits, "mydomain2.com": [{:timer.seconds(2), 2}]) + + now_1 = now() + :ok = RateLimiter.rate_limit("http://mydomain2.com") + :ok = RateLimiter.rate_limit("http://mydomain2.com") + now_2 = now() + :ok = RateLimiter.rate_limit("http://mydomain2.com") + now_3 = now() + + assert now_2 - now_1 < 500 + assert div(now_3, 1_000) != div(now_2, 1_000) + end + + defp now, do: DateTime.to_unix(DateTime.utc_now(), :millisecond) +end