feat(http): back off from hosts that answer 429 or 503
Object fetches and WebFinger stop sending requests to a host until Retry-After or X-RateLimit-Reset (default 5 minutes, max 1 hour). Ported from Akkoma's Pleroma.HTTP.Backoff. Co-authored-by: FloatingGhost <hannah@coffee-and-dreams.uk> Co-authored-by: Oneric <oneric@oneric.stub>
This commit is contained in:
parent
3e9dc477c9
commit
8ce283f966
10 changed files with 330 additions and 5 deletions
|
|
@ -17,7 +17,7 @@ defmodule Mix.Pleroma do
|
|||
:oban,
|
||||
:logger_backends
|
||||
]
|
||||
@cachex_children ["object", "user", "scrubber", "web_resp"]
|
||||
@cachex_children ["object", "user", "scrubber", "web_resp", "http_backoff"]
|
||||
@doc "Common functions to be reused in mix tasks"
|
||||
def start_pleroma do
|
||||
Pleroma.Config.Holder.save_default()
|
||||
|
|
|
|||
|
|
@ -198,6 +198,7 @@ defmodule Pleroma.Application do
|
|||
),
|
||||
build_cachex("rel_me", limit: 2500),
|
||||
build_cachex("host_meta", default_ttl: :timer.minutes(120), limit: 5_000),
|
||||
build_cachex("http_backoff", limit: 10_000),
|
||||
build_cachex("translations", default_ttl: :timer.hours(24), limit: 5_000)
|
||||
]
|
||||
end
|
||||
|
|
|
|||
101
lib/pleroma/http/backoff.ex
Normal file
101
lib/pleroma/http/backoff.ex
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.HTTP.Backoff do
|
||||
@moduledoc """
|
||||
GET wrapper that stops sending requests to a host after it answers
|
||||
429 or 503, until the time given by `Retry-After` or `X-RateLimit-Reset`.
|
||||
|
||||
Based on Akkoma's `Pleroma.HTTP.Backoff`.
|
||||
"""
|
||||
|
||||
alias Pleroma.HTTP
|
||||
|
||||
require Logger
|
||||
|
||||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
@cache :http_backoff_cache
|
||||
|
||||
@default_seconds 5 * 60
|
||||
@max_seconds 60 * 60
|
||||
|
||||
@spec get(String.t(), list(), keyword()) :: {:ok, Tesla.Env.t()} | {:error, any()}
|
||||
def get(url, headers \\ [], options \\ []) when is_binary(url) do
|
||||
host = URI.parse(url).host
|
||||
|
||||
case @cachex.get(@cache, host) do
|
||||
{:ok, true} ->
|
||||
{:error, :ratelimit}
|
||||
|
||||
_ ->
|
||||
url
|
||||
|> HTTP.get(headers, options)
|
||||
|> check_backoff(host)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_backoff({:ok, %{status: status, headers: headers}}, host)
|
||||
when status in [429, 503] do
|
||||
seconds = backoff_seconds(headers, DateTime.utc_now())
|
||||
Logger.warning("#{host} answered #{status}, backing off for #{seconds} seconds")
|
||||
@cachex.put(@cache, host, true, ttl: :timer.seconds(seconds))
|
||||
{:error, :ratelimit}
|
||||
end
|
||||
|
||||
defp check_backoff(response, _host), do: response
|
||||
|
||||
@doc """
|
||||
Seconds to back off for, from the first valid `Retry-After` or
|
||||
`X-RateLimit-Reset` header. Missing, invalid or past values give the
|
||||
default of #{@default_seconds} seconds; values are capped at #{@max_seconds}.
|
||||
"""
|
||||
@spec backoff_seconds(list(), DateTime.t()) :: pos_integer()
|
||||
def backoff_seconds(headers, now) do
|
||||
[
|
||||
parse_retry_after(header(headers, "retry-after"), now),
|
||||
parse_timestamp(header(headers, "x-ratelimit-reset"), now)
|
||||
]
|
||||
|> Enum.find(@default_seconds, &is_integer/1)
|
||||
|> min(@max_seconds)
|
||||
end
|
||||
|
||||
defp header(headers, name) do
|
||||
Enum.find_value(headers, fn
|
||||
{key, value} when is_binary(key) and is_binary(value) ->
|
||||
if String.downcase(key) == name, do: String.trim(value)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
end
|
||||
|
||||
# Retry-After is either delta-seconds or an HTTP-date (RFC 9110, 10.2.3)
|
||||
defp parse_retry_after(nil, _now), do: nil
|
||||
|
||||
defp parse_retry_after(value, now) do
|
||||
case Integer.parse(value) do
|
||||
{seconds, ""} when seconds > 0 -> seconds
|
||||
{_, ""} -> nil
|
||||
_ -> parse_timestamp(value, now)
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_timestamp(nil, _now), do: nil
|
||||
|
||||
defp parse_timestamp(value, now) do
|
||||
with {:ok, %DateTime{} = datetime} <- parse_datetime(value),
|
||||
seconds when seconds > 0 <- DateTime.diff(datetime, now) do
|
||||
seconds
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_datetime(value) do
|
||||
with {:error, _} <- Timex.parse(value, "{RFC1123}"),
|
||||
{:ok, datetime, _offset} <- DateTime.from_iso8601(value) do
|
||||
{:ok, datetime}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -216,7 +216,7 @@ defmodule Pleroma.Object.Fetcher do
|
|||
|> maybe_date_fetch(date)
|
||||
|> sign_fetch(id, date)
|
||||
|
||||
case HTTP.get(id, headers) do
|
||||
case HTTP.Backoff.get(id, headers) do
|
||||
{:ok, %{body: body, status: code, headers: headers, url: final_url}}
|
||||
when code in 200..299 ->
|
||||
remote_host = if final_url, do: URI.parse(final_url).host, else: nil
|
||||
|
|
|
|||
|
|
@ -158,7 +158,11 @@ defmodule Pleroma.Web.WebFinger do
|
|||
@cachex Pleroma.Config.get([:cachex, :provider], Cachex)
|
||||
def find_lrdd_template(domain) do
|
||||
@cachex.fetch!(:host_meta_cache, domain, fn _ ->
|
||||
{:commit, fetch_lrdd_template(domain)}
|
||||
case fetch_lrdd_template(domain) do
|
||||
# retry once the backoff expires instead of caching the miss
|
||||
{:error, :ratelimit} = error -> {:ignore, error}
|
||||
result -> {:commit, result}
|
||||
end
|
||||
end)
|
||||
rescue
|
||||
e -> {:error, "Cachex error: #{inspect(e)}"}
|
||||
|
|
@ -168,9 +172,13 @@ defmodule Pleroma.Web.WebFinger do
|
|||
# WebFinger is restricted to HTTPS - https://tools.ietf.org/html/rfc7033#section-9.1
|
||||
meta_url = "https://#{domain}/.well-known/host-meta"
|
||||
|
||||
with {:ok, %{status: status, body: body}} when status in 200..299 <- HTTP.get(meta_url) do
|
||||
with {:ok, %{status: status, body: body}} when status in 200..299 <-
|
||||
HTTP.Backoff.get(meta_url) do
|
||||
get_template_from_xml(body)
|
||||
else
|
||||
{:error, :ratelimit} = error ->
|
||||
error
|
||||
|
||||
error ->
|
||||
Logger.warning("Can't find LRDD template in #{inspect(meta_url)}: #{inspect(error)}")
|
||||
{:error, :lrdd_not_found}
|
||||
|
|
@ -210,7 +218,7 @@ defmodule Pleroma.Web.WebFinger do
|
|||
|
||||
with address when is_binary(address) <- get_address_from_domain(domain, account),
|
||||
{:ok, %{status: status, body: body, headers: headers}} when status in 200..299 <-
|
||||
HTTP.get(
|
||||
HTTP.Backoff.get(
|
||||
address,
|
||||
[{"accept", "application/xrd+xml,application/jrd+json"}]
|
||||
) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue