fix(http): back off from the host that answered, and read Unix resets
After a redirect the 429/503 came from the final host, but the backoff was recorded for the requested one. X-RateLimit-Reset is also accepted as a Unix timestamp or as seconds.
This commit is contained in:
parent
b29d9d0981
commit
4070b7be21
2 changed files with 65 additions and 2 deletions
|
|
@ -39,8 +39,9 @@ defmodule Pleroma.HTTP.Backoff do
|
||||||
defp downcase(nil), do: nil
|
defp downcase(nil), do: nil
|
||||||
defp downcase(host), do: String.downcase(host)
|
defp downcase(host), do: String.downcase(host)
|
||||||
|
|
||||||
defp check_backoff({:ok, %{status: status, headers: headers}}, host)
|
defp check_backoff({:ok, %{status: status, headers: headers} = env}, host)
|
||||||
when status in [429, 503] do
|
when status in [429, 503] do
|
||||||
|
host = response_host(env, host)
|
||||||
seconds = backoff_seconds(headers, DateTime.utc_now())
|
seconds = backoff_seconds(headers, DateTime.utc_now())
|
||||||
Logger.warning("#{host} answered #{status}, backing off for #{seconds} seconds")
|
Logger.warning("#{host} answered #{status}, backing off for #{seconds} seconds")
|
||||||
@cachex.put(@cache, host, true, ttl: :timer.seconds(seconds))
|
@cachex.put(@cache, host, true, ttl: :timer.seconds(seconds))
|
||||||
|
|
@ -49,6 +50,13 @@ defmodule Pleroma.HTTP.Backoff do
|
||||||
|
|
||||||
defp check_backoff(response, _host), do: response
|
defp check_backoff(response, _host), do: response
|
||||||
|
|
||||||
|
# After a redirect, the 429/503 came from the last host, not the requested one.
|
||||||
|
defp response_host(%{url: url}, host) when is_binary(url) do
|
||||||
|
URI.parse(url).host |> downcase() || host
|
||||||
|
end
|
||||||
|
|
||||||
|
defp response_host(_env, host), do: host
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Turns an Oban job's `{:error, :ratelimit}` result into a snooze for the
|
Turns an Oban job's `{:error, :ratelimit}` result into a snooze for the
|
||||||
default backoff, so the job's attempts aren't all spent while the host is
|
default backoff, so the job's attempts aren't all spent while the host is
|
||||||
|
|
@ -75,7 +83,7 @@ defmodule Pleroma.HTTP.Backoff do
|
||||||
def backoff_seconds(headers, now) do
|
def backoff_seconds(headers, now) do
|
||||||
[
|
[
|
||||||
parse_retry_after(header(headers, "retry-after"), now),
|
parse_retry_after(header(headers, "retry-after"), now),
|
||||||
parse_timestamp(header(headers, "x-ratelimit-reset"), now)
|
parse_ratelimit_reset(header(headers, "x-ratelimit-reset"), now)
|
||||||
]
|
]
|
||||||
|> Enum.find(@default_seconds, &is_integer/1)
|
|> Enum.find(@default_seconds, &is_integer/1)
|
||||||
|> min(@max_seconds)
|
|> min(@max_seconds)
|
||||||
|
|
@ -102,6 +110,29 @@ defmodule Pleroma.HTTP.Backoff do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Servers send X-RateLimit-Reset as a timestamp (ISO 8601 like Mastodon, or
|
||||||
|
# Unix seconds) or as seconds until the reset.
|
||||||
|
@unix_timestamp_min 1_000_000_000
|
||||||
|
|
||||||
|
defp parse_ratelimit_reset(nil, _now), do: nil
|
||||||
|
|
||||||
|
defp parse_ratelimit_reset(value, now) do
|
||||||
|
case Integer.parse(value) do
|
||||||
|
{timestamp, ""} when timestamp >= @unix_timestamp_min ->
|
||||||
|
timestamp - DateTime.to_unix(now)
|
||||||
|
|
||||||
|
{seconds, ""} ->
|
||||||
|
seconds
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
parse_timestamp(value, now)
|
||||||
|
end
|
||||||
|
|> positive()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp positive(seconds) when is_integer(seconds) and seconds > 0, do: seconds
|
||||||
|
defp positive(_), do: nil
|
||||||
|
|
||||||
defp parse_timestamp(nil, _now), do: nil
|
defp parse_timestamp(nil, _now), do: nil
|
||||||
|
|
||||||
defp parse_timestamp(value, now) do
|
defp parse_timestamp(value, now) do
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,23 @@ defmodule Pleroma.HTTP.BackoffTest do
|
||||||
mock_response(200)
|
mock_response(200)
|
||||||
assert {:ok, _} = Backoff.get("https://other.example.com/users/bob")
|
assert {:ok, _} = Backoff.get("https://other.example.com/users/bob")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "backs off from the host that answered after a redirect" do
|
||||||
|
test_pid = self()
|
||||||
|
|
||||||
|
Tesla.Mock.mock(fn
|
||||||
|
%{url: "https://redirector.example.com/users/alice"} = env ->
|
||||||
|
%{env | status: 302, headers: [{"location", @url}], body: ""}
|
||||||
|
|
||||||
|
%{url: @url} = env ->
|
||||||
|
send(test_pid, :request)
|
||||||
|
%{env | status: 429, headers: [], body: ""}
|
||||||
|
end)
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get("https://redirector.example.com/users/alice")
|
||||||
|
assert {:ok, true} = Cachex.get(@cache, @host)
|
||||||
|
assert {:ok, nil} = Cachex.get(@cache, "redirector.example.com")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "backoff headers" do
|
describe "backoff headers" do
|
||||||
|
|
@ -121,6 +138,21 @@ defmodule Pleroma.HTTP.BackoffTest do
|
||||||
assert_ttl_about(600)
|
assert_ttl_about(600)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "parses X-RateLimit-Reset as a Unix timestamp" do
|
||||||
|
reset = DateTime.utc_now() |> DateTime.add(600) |> DateTime.to_unix() |> to_string()
|
||||||
|
mock_response(429, [{"x-ratelimit-reset", reset}])
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||||
|
assert_ttl_about(600)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "parses X-RateLimit-Reset in seconds" do
|
||||||
|
mock_response(429, [{"x-ratelimit-reset", "600"}])
|
||||||
|
|
||||||
|
assert {:error, :ratelimit} = Backoff.get(@url)
|
||||||
|
assert_ttl_about(600)
|
||||||
|
end
|
||||||
|
|
||||||
test "matches header names case-insensitively" do
|
test "matches header names case-insensitively" do
|
||||||
mock_response(429, [{"Retry-After", "600"}])
|
mock_response(429, [{"Retry-After", "600"}])
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue