fix(workers): snooze incoming jobs while a host is backed off

With 5 attempts and Oban's short retry delays, an incoming activity from
an unknown actor on a backed-off host was discarded without a request.
ReceiverWorker and SignatureRetryWorker now snooze for up to 2 hours.
Also lowercase backoff host keys and log backed-off user fetches at debug.
This commit is contained in:
Lain Soykaf 2026-10-31 14:35:58 +04:00
commit b29d9d0981
No known key found for this signature in database
8 changed files with 97 additions and 13 deletions

View file

@ -19,10 +19,11 @@ defmodule Pleroma.HTTP.Backoff do
@default_seconds 5 * 60
@max_seconds 60 * 60
@max_job_age 2 * 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
host = url |> URI.parse() |> Map.get(:host) |> downcase()
case @cachex.get(@cache, host) do
{:ok, true} ->
@ -35,6 +36,9 @@ defmodule Pleroma.HTTP.Backoff do
end
end
defp downcase(nil), do: nil
defp downcase(host), do: String.downcase(host)
defp check_backoff({:ok, %{status: status, headers: headers}}, host)
when status in [429, 503] do
seconds = backoff_seconds(headers, DateTime.utc_now())
@ -45,6 +49,23 @@ defmodule Pleroma.HTTP.Backoff do
defp check_backoff(response, _host), do: response
@doc """
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
backed off. Jobs inserted more than #{@max_job_age} seconds ago get the
error back and fail as usual.
"""
@spec snooze_ratelimited(any(), Oban.Job.t()) :: any()
def snooze_ratelimited({:error, :ratelimit} = error, %Oban.Job{inserted_at: inserted_at}) do
if is_nil(inserted_at) or DateTime.diff(DateTime.utc_now(), inserted_at) < @max_job_age do
{:snooze, @default_seconds}
else
error
end
end
def snooze_ratelimited(result, _job), do: result
@doc """
Seconds to back off for, from the first valid `Retry-After` or
`X-RateLimit-Reset` header. Missing, invalid or past values give the

View file

@ -1949,6 +1949,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
Logger.info("Rejected user #{ap_id}: #{inspect(reason)}")
{:error, e}
{:error, :ratelimit = e} ->
Logger.debug("Backing off from fetching user #{ap_id}")
{:error, e}
{:error, e} ->
Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}")
{:error, e}

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.HTTP.Backoff
alias Pleroma.Instances
alias Pleroma.Web.Federator
alias Pleroma.Workers.SignatureRetryWorker
@ -14,7 +15,9 @@ defmodule Pleroma.Workers.ReceiverWorker do
if signature_retry_job?(args) do
perform_signature_retry(job)
else
perform_incoming(params)
params
|> perform_incoming()
|> Backoff.snooze_ratelimited(job)
end
end

View file

@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.SignatureRetryWorker do
alias Pleroma.HTTP.Backoff
alias Pleroma.Instances
alias Pleroma.Signature
alias Pleroma.User
@ -16,16 +17,18 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true
def perform(%Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => method,
"params" => params,
"req_headers" => req_headers,
"request_path" => request_path,
"query_string" => query_string
}
})
def perform(
%Job{
args: %{
"op" => "incoming_failed_signature_ap_doc",
"method" => method,
"params" => params,
"req_headers" => req_headers,
"request_path" => request_path,
"query_string" => query_string
}
} = job
)
when is_binary(method) and is_map(params) and is_list(req_headers) and
is_binary(request_path) and is_binary(query_string) do
case normalize_req_headers(req_headers) do
@ -58,7 +61,10 @@ defmodule Pleroma.Workers.SignatureRetryWorker do
{:ok, res}
else
e -> process_errors(e, retry_log_context(params, request_path, signature_actor_result))
e ->
e
|> process_errors(retry_log_context(params, request_path, signature_actor_result))
|> Backoff.snooze_ratelimited(job)
end
e ->