From 3d422ef3256e9eeef79d0c78743e19d8435dc352 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Thu, 5 Jun 2025 16:38:40 -0700 Subject: [PATCH 01/21] Reachability refactor The result of Oban jobs determine the reachability status. Publisher jobs will cancel themselves at execution time if the target server is now unreachable. Receiving activities does not immediately mark a server as reachable, but creates a ReachabilityWorker job to validate. A Cron will execute daily to test all unreachable servers. --- changelog.d/reachabililty.change | 1 + config/config.exs | 4 +- lib/pleroma/instances.ex | 20 +--- lib/pleroma/instances/instance.ex | 18 +-- lib/pleroma/object/fetcher.ex | 5 - .../activity_pub/activity_pub_controller.ex | 10 -- lib/pleroma/web/activity_pub/publisher.ex | 22 +--- .../controllers/instances_controller.ex | 2 +- .../cron/schedule_reachability_worker.ex | 33 ++++++ lib/pleroma/workers/publisher_worker.ex | 26 +++- lib/pleroma/workers/reachability_worker.ex | 31 +++++ lib/pleroma/workers/receiver_worker.ex | 11 ++ lib/pleroma/workers/remote_fetcher_worker.ex | 10 ++ test/pleroma/instances/instance_test.exs | 9 +- test/pleroma/instances_test.exs | 78 ++---------- test/pleroma/object/fetcher_test.exs | 12 -- .../activity_pub_controller_test.exs | 36 ------ .../web/activity_pub/publisher_test.exs | 111 ------------------ test/pleroma/web/federator_test.exs | 9 +- .../controllers/instances_controller_test.exs | 7 +- .../schedule_reachability_worker_test.exs | 52 ++++++++ .../pleroma/workers/publisher_worker_test.exs | 83 +++++++++++++ test/pleroma/workers/receiver_worker_test.exs | 61 +++++++++- .../workers/remote_fetcher_worker_test.exs | 2 +- 24 files changed, 341 insertions(+), 312 deletions(-) create mode 100644 changelog.d/reachabililty.change create mode 100644 lib/pleroma/workers/cron/schedule_reachability_worker.ex create mode 100644 lib/pleroma/workers/reachability_worker.ex create mode 100644 test/pleroma/workers/cron/schedule_reachability_worker_test.exs diff --git a/changelog.d/reachabililty.change b/changelog.d/reachabililty.change new file mode 100644 index 000000000..71b9514be --- /dev/null +++ b/changelog.d/reachabililty.change @@ -0,0 +1 @@ +Improved the logic of how we determine if a server is unreachable. diff --git a/config/config.exs b/config/config.exs index a231c5ba0..d164f8389 100644 --- a/config/config.exs +++ b/config/config.exs @@ -194,7 +194,6 @@ config :pleroma, :instance, account_approval_required: false, federating: true, federation_incoming_replies_max_depth: 100, - federation_reachability_timeout_days: 7, allow_relay: true, public: true, quarantined_instances: [], @@ -603,7 +602,8 @@ config :pleroma, Oban, crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, - {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker} + {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}, + {"0 0 * * *", Pleroma.Workers.Cron.ScheduleReachabilityWorker} ] config :pleroma, Pleroma.Formatter, diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index b6d83f591..9237e0944 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -15,25 +15,7 @@ defmodule Pleroma.Instances do defdelegate set_unreachable(url_or_host, unreachable_since \\ nil), to: Instance - defdelegate get_consistently_unreachable, to: Instance - - def set_consistently_unreachable(url_or_host), - do: set_unreachable(url_or_host, reachability_datetime_threshold()) - - def reachability_datetime_threshold do - federation_reachability_timeout_days = - Pleroma.Config.get([:instance, :federation_reachability_timeout_days], 0) - - if federation_reachability_timeout_days > 0 do - NaiveDateTime.add( - NaiveDateTime.utc_now(), - -federation_reachability_timeout_days * 24 * 3600, - :second - ) - else - ~N[0000-01-01 00:00:00] - end - end + defdelegate get_unreachable, to: Instance def host(url_or_host) when is_binary(url_or_host) do if url_or_host =~ ~r/^http/i do diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 33f1229d0..baccc314c 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -51,7 +51,7 @@ defmodule Pleroma.Instances.Instance do |> cast(params, [:software_name, :software_version, :software_repository]) end - def filter_reachable([]), do: %{} + def filter_reachable([]), do: [] def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do hosts = @@ -68,19 +68,15 @@ defmodule Pleroma.Instances.Instance do ) |> Map.new(& &1) - reachability_datetime_threshold = Instances.reachability_datetime_threshold() - for entry <- Enum.filter(urls_or_hosts, &is_binary/1) do host = host(entry) unreachable_since = unreachable_since_by_host[host] - if !unreachable_since || - NaiveDateTime.compare(unreachable_since, reachability_datetime_threshold) == :gt do - {entry, unreachable_since} + if is_nil(unreachable_since) do + entry end end |> Enum.filter(& &1) - |> Map.new(& &1) end def reachable?(url_or_host) when is_binary(url_or_host) do @@ -88,7 +84,7 @@ defmodule Pleroma.Instances.Instance do from(i in Instance, where: i.host == ^host(url_or_host) and - i.unreachable_since <= ^Instances.reachability_datetime_threshold(), + i.unreachable_since <= ^NaiveDateTime.utc_now(), select: true ) ) @@ -132,11 +128,9 @@ defmodule Pleroma.Instances.Instance do def set_unreachable(_, _), do: {:error, nil} - def get_consistently_unreachable do - reachability_datetime_threshold = Instances.reachability_datetime_threshold() - + def get_unreachable do from(i in Instance, - where: ^reachability_datetime_threshold > i.unreachable_since, + where: not is_nil(i.unreachable_since), order_by: i.unreachable_since, select: {i.host, i.unreachable_since} ) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index b54ef9ce5..ea5480a41 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Object.Fetcher do alias Pleroma.HTTP - alias Pleroma.Instances alias Pleroma.Maps alias Pleroma.Object alias Pleroma.Object.Containment @@ -152,10 +151,6 @@ defmodule Pleroma.Object.Fetcher do {:ok, body} <- get_object(id), {:ok, data} <- safe_json_decode(body), :ok <- Containment.contain_origin_from_id(id, data) do - if not Instances.reachable?(id) do - Instances.set_reachable(id) - end - {:ok, data} else {:scheme, _} -> diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index 7ac0bbab4..2ee72c49a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -53,7 +53,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do ) plug(:log_inbox_metadata when action in [:inbox]) - plug(:set_requester_reachable when action in [:inbox]) plug(:relay_active? when action in [:relay]) defp relay_active?(conn, _) do @@ -520,15 +519,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(dgettext("errors", "error")) end - defp set_requester_reachable(%Plug.Conn{} = conn, _) do - with actor <- conn.params["actor"], - true <- is_binary(actor) do - Pleroma.Instances.set_reachable(actor) - end - - conn - end - defp log_inbox_metadata(%{params: %{"actor" => actor, "type" => type}} = conn, _) do Logger.metadata(actor: actor, type: type) conn diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 0de3a0d43..78312b771 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -148,17 +148,9 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {"digest", p.digest} ] ) do - if not is_nil(p.unreachable_since) do - Instances.set_reachable(p.inbox) - end - result else {_post_result, %{status: code} = response} = e -> - if is_nil(p.unreachable_since) do - Instances.set_unreachable(p.inbox) - end - Logger.metadata(activity: p.activity_id, inbox: p.inbox, status: code) Logger.error("Publisher failed to inbox #{p.inbox} with status #{code}") @@ -179,10 +171,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do connection_pool_snooze() e -> - if is_nil(p.unreachable_since) do - Instances.set_unreachable(p.inbox) - end - Logger.metadata(activity: p.activity_id, inbox: p.inbox) Logger.error("Publisher failed to inbox #{p.inbox} #{inspect(e)}") {:error, e} @@ -308,7 +296,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do Repo.checkout(fn -> Enum.each(inboxes, fn inboxes -> - Enum.each(inboxes, fn {inbox, unreachable_since} -> + Enum.each(inboxes, fn inbox -> %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) # Get all the recipients on the same host and add them to cc. Otherwise, a remote @@ -318,8 +306,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do __MODULE__.enqueue_one(%{ inbox: inbox, cc: cc, - activity_id: activity.id, - unreachable_since: unreachable_since + activity_id: activity.id }) end) end) @@ -352,12 +339,11 @@ defmodule Pleroma.Web.ActivityPub.Publisher do |> Enum.each(fn {inboxes, priority} -> inboxes |> Instances.filter_reachable() - |> Enum.each(fn {inbox, unreachable_since} -> + |> Enum.each(fn inbox -> __MODULE__.enqueue_one( %{ inbox: inbox, - activity_id: activity.id, - unreachable_since: unreachable_since + activity_id: activity.id }, priority: priority ) diff --git a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex index 6257e3153..85cfe9f00 100644 --- a/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/instances_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.PleromaAPI.InstancesController do def show(conn, _params) do unreachable = - Instances.get_consistently_unreachable() + Instances.get_unreachable() |> Map.new(fn {host, date} -> {host, to_string(date)} end) json(conn, %{"unreachable" => unreachable}) diff --git a/lib/pleroma/workers/cron/schedule_reachability_worker.ex b/lib/pleroma/workers/cron/schedule_reachability_worker.ex new file mode 100644 index 000000000..a0b8e261c --- /dev/null +++ b/lib/pleroma/workers/cron/schedule_reachability_worker.ex @@ -0,0 +1,33 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.ScheduleReachabilityWorker do + use Oban.Worker, + queue: :background, + max_attempts: 2 + + alias Pleroma.Instances + alias Pleroma.Repo + + @impl true + def perform(_job) do + unreachable_servers = Instances.get_unreachable() + + jobs = + unreachable_servers + |> Enum.map(fn {domain, _} -> + Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) + end) + + case Repo.transaction(fn -> + Enum.each(jobs, &Oban.insert/1) + end) do + {:ok, _} -> + :ok + + {:error, reason} -> + {:error, reason} + end + end +end diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex index 7d9b022de..10736bef5 100644 --- a/lib/pleroma/workers/publisher_worker.ex +++ b/lib/pleroma/workers/publisher_worker.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Workers.PublisherWorker do alias Pleroma.Activity + alias Pleroma.Instances alias Pleroma.Web.Federator use Oban.Worker, queue: :federator_outgoing, max_attempts: 5 @@ -14,9 +15,30 @@ defmodule Pleroma.Workers.PublisherWorker do Federator.perform(:publish, activity) end - def perform(%Job{args: %{"op" => "publish_one", "params" => params}}) do + def perform(%Job{args: %{"op" => "publish_one", "params" => params}} = job) do params = Map.new(params, fn {k, v} -> {String.to_atom(k), v} end) - Federator.perform(:publish_one, params) + + # Cancel / skip the job if this server believed to be unreachable now + if not Instances.reachable?(params.inbox) do + {:cancel, :unreachable} + else + case Federator.perform(:publish_one, params) do + {:ok, _} -> + :ok + + {:error, _} = error -> + # Only mark as unreachable on final failure + if job.attempt == job.max_attempts do + Instances.set_unreachable(params.inbox) + end + + error + + error -> + # Unexpected error, may have been client side + error + end + end end @impl true diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex new file mode 100644 index 000000000..3a11dfe2a --- /dev/null +++ b/lib/pleroma/workers/reachability_worker.ex @@ -0,0 +1,31 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ReachabilityWorker do + use Oban.Worker, + queue: :background, + max_attempts: 3, + unique: [period: :infinity, states: [:available, :scheduled]] + + alias Pleroma.HTTP + alias Pleroma.Instances + + @impl true + def perform(%Oban.Job{args: %{"domain" => domain}}) do + case HTTP.get("https://#{domain}/.well-known/nodeinfo") do + {:ok, %{status: status}} when status in 200..299 -> + Instances.set_reachable("https://#{domain}") + :ok + + {:ok, %{status: _status}} -> + {:error, :unreachable} + + {:error, _} = error -> + error + end + end + + @impl true + def timeout(_job), do: :timer.seconds(5) +end diff --git a/lib/pleroma/workers/receiver_worker.ex b/lib/pleroma/workers/receiver_worker.ex index 11b672bef..e2c950967 100644 --- a/lib/pleroma/workers/receiver_worker.ex +++ b/lib/pleroma/workers/receiver_worker.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorker do + alias Pleroma.Instances alias Pleroma.Signature alias Pleroma.User alias Pleroma.Web.Federator @@ -37,6 +38,11 @@ defmodule Pleroma.Workers.ReceiverWorker do {:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + {:ok, res} else e -> process_errors(e) @@ -45,6 +51,11 @@ defmodule Pleroma.Workers.ReceiverWorker do def perform(%Job{args: %{"op" => "incoming_ap_doc", "params" => params}}) do with {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do + unless Instances.reachable?(params["actor"]) do + domain = URI.parse(params["actor"]).host + Oban.insert(Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain})) + end + {:ok, res} else e -> process_errors(e) diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index aa09362f5..5f57ec2d7 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -3,6 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.RemoteFetcherWorker do + alias Pleroma.Instances alias Pleroma.Object.Fetcher use Oban.Worker, queue: :background, unique: [period: :infinity] @@ -11,6 +12,15 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do {:ok, _object} -> + # Mark the server as reachable since we successfully fetched an object + case URI.parse(id) do + %URI{host: host} when not is_nil(host) -> + Instances.set_reachable("https://#{host}") + + _ -> + :ok + end + :ok {:allowed_depth, false} -> diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 6a718be21..f195d9bd6 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Instances.InstanceTest do - alias Pleroma.Instances alias Pleroma.Instances.Instance alias Pleroma.Repo alias Pleroma.Tests.ObanHelpers @@ -14,8 +13,6 @@ defmodule Pleroma.Instances.InstanceTest do import ExUnit.CaptureLog import Pleroma.Factory - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - describe "set_reachable/1" do test "clears `unreachable_since` of existing matching Instance record having non-nil `unreachable_since`" do unreachable_since = NaiveDateTime.to_iso8601(NaiveDateTime.utc_now()) @@ -145,7 +142,11 @@ defmodule Pleroma.Instances.InstanceTest do end test "Doesn't scrapes unreachable instances" do - instance = insert(:instance, unreachable_since: Instances.reachability_datetime_threshold()) + instance = + insert(:instance, + unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.add(-:timer.hours(24)) + ) + url = "https://" <> instance.host assert capture_log(fn -> assert nil == Instance.get_or_update_favicon(URI.parse(url)) end) =~ diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs index 96fa9cffe..cbafbfa44 100644 --- a/test/pleroma/instances_test.exs +++ b/test/pleroma/instances_test.exs @@ -7,73 +7,40 @@ defmodule Pleroma.InstancesTest do use Pleroma.DataCase - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - describe "reachable?/1" do test "returns `true` for host / url with unknown reachability status" do assert Instances.reachable?("unknown.site") assert Instances.reachable?("http://unknown.site") end - - test "returns `false` for host / url marked unreachable for at least `reachability_datetime_threshold()`" do - host = "consistently-unreachable.name" - Instances.set_consistently_unreachable(host) - - refute Instances.reachable?(host) - refute Instances.reachable?("http://#{host}/path") - end - - test "returns `true` for host / url marked unreachable for less than `reachability_datetime_threshold()`" do - url = "http://eventually-unreachable.name/path" - - Instances.set_unreachable(url) - - assert Instances.reachable?(url) - assert Instances.reachable?(URI.parse(url).host) - end - - test "raises FunctionClauseError exception on non-binary input" do - assert_raise FunctionClauseError, fn -> Instances.reachable?(nil) end - assert_raise FunctionClauseError, fn -> Instances.reachable?(1) end - end end describe "filter_reachable/1" do setup do - host = "consistently-unreachable.name" - url1 = "http://eventually-unreachable.com/path" - url2 = "http://domain.com/path" + unreachable_host = "consistently-unreachable.name" + reachable_host = "http://domain.com/path" - Instances.set_consistently_unreachable(host) - Instances.set_unreachable(url1) + Instances.set_unreachable(unreachable_host) - result = Instances.filter_reachable([host, url1, url2, nil]) - %{result: result, url1: url1, url2: url2} + result = Instances.filter_reachable([unreachable_host, reachable_host, nil]) + %{result: result, reachable_host: reachable_host, unreachable_host: unreachable_host} end - test "returns a map with keys containing 'not marked consistently unreachable' elements of supplied list", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert Enum.sort([url1, url2]) == result |> Map.keys() |> Enum.sort() + test "returns a list of only reachable elements", + %{result: result, reachable_host: reachable_host} do + assert is_list(result) + assert [reachable_host] == result end - test "returns a map with `unreachable_since` values for keys", - %{result: result, url1: url1, url2: url2} do - assert is_map(result) - assert %NaiveDateTime{} = result[url1] - assert is_nil(result[url2]) - end - - test "returns an empty map for empty list or list containing no hosts / url" do - assert %{} == Instances.filter_reachable([]) - assert %{} == Instances.filter_reachable([nil]) + test "returns an empty list when provided no data" do + assert [] == Instances.filter_reachable([]) + assert [] == Instances.filter_reachable([nil]) end end describe "set_reachable/1" do test "sets unreachable url or host reachable" do host = "domain.com" - Instances.set_consistently_unreachable(host) + Instances.set_unreachable(host) refute Instances.reachable?(host) Instances.set_reachable(host) @@ -102,23 +69,4 @@ defmodule Pleroma.InstancesTest do assert {:error, _} = Instances.set_unreachable(1) end end - - describe "set_consistently_unreachable/1" do - test "sets reachable url or host unreachable" do - url = "http://domain.com?q=" - assert Instances.reachable?(url) - - Instances.set_consistently_unreachable(url) - refute Instances.reachable?(url) - end - - test "keeps unreachable url or host unreachable" do - host = "site.name" - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - - Instances.set_consistently_unreachable(host) - refute Instances.reachable?(host) - end - end end diff --git a/test/pleroma/object/fetcher_test.exs b/test/pleroma/object/fetcher_test.exs index 7ba5090e1..9afa34fa2 100644 --- a/test/pleroma/object/fetcher_test.exs +++ b/test/pleroma/object/fetcher_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.Object.FetcherTest do use Pleroma.DataCase alias Pleroma.Activity - alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Object.Fetcher alias Pleroma.Web.ActivityPub.ObjectValidator @@ -250,17 +249,6 @@ defmodule Pleroma.Object.FetcherTest do result = Fetcher.fetch_object_from_id("https://example.com/objects/no-content-type") assert {:fetch, {:error, nil}} = result end - - test "it resets instance reachability on successful fetch" do - id = "http://mastodon.example.org/@admin/99541947525187367" - Instances.set_consistently_unreachable(id) - refute Instances.reachable?(id) - - {:ok, _object} = - Fetcher.fetch_object_from_id("http://mastodon.example.org/@admin/99541947525187367") - - assert Instances.reachable?(id) - end end describe "implementation quirks" do diff --git a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs index 46b3d5f0d..d9be82e64 100644 --- a/test/pleroma/web/activity_pub/activity_pub_controller_test.exs +++ b/test/pleroma/web/activity_pub/activity_pub_controller_test.exs @@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do alias Pleroma.Activity alias Pleroma.Delivery - alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Tests.ObanHelpers alias Pleroma.User @@ -601,23 +600,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert Activity.get_by_ap_id(data["id"]) end - test "it clears `unreachable` federation status of the sender", %{conn: conn} do - data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() - - sender_url = data["actor"] - Instances.set_consistently_unreachable(sender_url) - refute Instances.reachable?(sender_url) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_url) - end - test "accept follow activity", %{conn: conn} do clear_config([:instance, :federating], true) relay = Relay.get_actor() @@ -1108,24 +1090,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do assert response(conn, 200) =~ note_object.data["content"] end - test "it clears `unreachable` federation status of the sender", %{conn: conn, data: data} do - user = insert(:user) - data = Map.put(data, "bcc", [user.ap_id]) - - sender_host = URI.parse(data["actor"]).host - Instances.set_consistently_unreachable(sender_host) - refute Instances.reachable?(sender_host) - - conn = - conn - |> assign(:valid_signature, true) - |> put_req_header("content-type", "application/activity+json") - |> post("/users/#{user.nickname}/inbox", data) - - assert "ok" == json_response(conn, 200) - assert Instances.reachable?(sender_host) - end - test "it removes all follower collections but actor's", %{conn: conn} do [actor, recipient] = insert_pair(:user) diff --git a/test/pleroma/web/activity_pub/publisher_test.exs b/test/pleroma/web/activity_pub/publisher_test.exs index 99ed42877..a6f25c9a7 100644 --- a/test/pleroma/web/activity_pub/publisher_test.exs +++ b/test/pleroma/web/activity_pub/publisher_test.exs @@ -6,13 +6,11 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do use Oban.Testing, repo: Pleroma.Repo use Pleroma.Web.ConnCase - import ExUnit.CaptureLog import Pleroma.Factory import Tesla.Mock import Mock alias Pleroma.Activity - alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Tests.ObanHelpers alias Pleroma.Web.ActivityPub.Publisher @@ -167,115 +165,6 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do }) |> Publisher.publish_one() end - - test_with_mock "calls `Instances.set_reachable` on successful federation if `unreachable_since` is set", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string() - }) - |> Publisher.publish_one() - - assert called(Instances.set_reachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_reachable` on successful federation if `unreachable_since` is nil", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: nil - }) - |> Publisher.publish_one() - - refute called(Instances.set_reachable(inbox)) - end - - test_with_mock "calls `Instances.set_unreachable` on target inbox on non-2xx HTTP response code", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://404.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert {:cancel, _} = - Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id}) - |> Publisher.publish_one() - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "it calls `Instances.set_unreachable` on target inbox on request error of any kind", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert capture_log(fn -> - assert {:error, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id - }) - |> Publisher.publish_one() - end) =~ "connrefused" - - assert called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target is reachable", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://200.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert {:ok, _} = - Publisher.prepare_one(%{inbox: inbox, activity_id: activity.id}) - |> Publisher.publish_one() - - refute called(Instances.set_unreachable(inbox)) - end - - test_with_mock "does NOT call `Instances.set_unreachable` if target instance has non-nil `unreachable_since`", - Instances, - [:passthrough], - [] do - _actor = insert(:user) - inbox = "http://connrefused.site/users/nick1/inbox" - activity = insert(:note_activity) - - assert capture_log(fn -> - assert {:error, _} = - Publisher.prepare_one(%{ - inbox: inbox, - activity_id: activity.id, - unreachable_since: NaiveDateTime.utc_now() |> NaiveDateTime.to_string() - }) - |> Publisher.publish_one() - end) =~ "connrefused" - - refute called(Instances.set_unreachable(inbox)) - end end describe "publish/2" do diff --git a/test/pleroma/web/federator_test.exs b/test/pleroma/web/federator_test.exs index 4a398f239..16fe1066a 100644 --- a/test/pleroma/web/federator_test.exs +++ b/test/pleroma/web/federator_test.exs @@ -126,22 +126,17 @@ defmodule Pleroma.Web.FederatorTest do inbox: inbox2 }) - dt = NaiveDateTime.utc_now() - Instances.set_unreachable(inbox1, dt) - - Instances.set_consistently_unreachable(URI.parse(inbox2).host) + Instances.set_unreachable(URI.parse(inbox2).host) {:ok, _activity} = CommonAPI.post(user, %{status: "HI @nick1@domain.com, @nick2@domain2.com!"}) - expected_dt = NaiveDateTime.to_iso8601(dt) - ObanHelpers.perform(all_enqueued(worker: PublisherWorker)) assert ObanHelpers.member?( %{ "op" => "publish_one", - "params" => %{"inbox" => inbox1, "unreachable_since" => expected_dt} + "params" => %{"inbox" => inbox1} }, all_enqueued(worker: PublisherWorker) ) diff --git a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs index 0d4951a73..702c05504 100644 --- a/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs +++ b/test/pleroma/web/pleroma_api/controllers/instances_controller_test.exs @@ -7,16 +7,11 @@ defmodule Pleroma.Web.PleromaApi.InstancesControllerTest do alias Pleroma.Instances - setup_all do: clear_config([:instance, :federation_reachability_timeout_days], 1) - setup do constant = "http://consistently-unreachable.name/" - eventual = "http://eventually-unreachable.com/path" {:ok, %Pleroma.Instances.Instance{unreachable_since: constant_unreachable}} = - Instances.set_consistently_unreachable(constant) - - _eventual_unreachable = Instances.set_unreachable(eventual) + Instances.set_unreachable(constant) %{constant_unreachable: constant_unreachable, constant: constant} end diff --git a/test/pleroma/workers/cron/schedule_reachability_worker_test.exs b/test/pleroma/workers/cron/schedule_reachability_worker_test.exs new file mode 100644 index 000000000..310c2e61a --- /dev/null +++ b/test/pleroma/workers/cron/schedule_reachability_worker_test.exs @@ -0,0 +1,52 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.Cron.ScheduleReachabilityWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + alias Pleroma.Instances + alias Pleroma.Workers.Cron.ScheduleReachabilityWorker + + describe "perform/1" do + test "schedules reachability checks for unreachable servers" do + # Mark some servers as unreachable + Instances.set_unreachable("https://example.com") + Instances.set_unreachable("https://test.com") + Instances.set_unreachable("https://another.com") + + # Verify they are marked as unreachable + refute Instances.reachable?("https://example.com") + refute Instances.reachable?("https://test.com") + refute Instances.reachable?("https://another.com") + + # Run the worker + assert :ok = ScheduleReachabilityWorker.perform(%Oban.Job{}) + + # Verify ReachabilityWorker jobs were scheduled for each server + # Note: domains in get_unreachable/0 are without the https:// prefix + assert_enqueued( + worker: Pleroma.Workers.ReachabilityWorker, + args: %{"domain" => "example.com"} + ) + + assert_enqueued( + worker: Pleroma.Workers.ReachabilityWorker, + args: %{"domain" => "test.com"} + ) + + assert_enqueued( + worker: Pleroma.Workers.ReachabilityWorker, + args: %{"domain" => "another.com"} + ) + end + + test "handles empty list of unreachable servers" do + # Ensure no servers are marked as unreachable + assert [] = Instances.get_unreachable() + assert :ok = ScheduleReachabilityWorker.perform(%Oban.Job{}) + refute_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + end + end +end diff --git a/test/pleroma/workers/publisher_worker_test.exs b/test/pleroma/workers/publisher_worker_test.exs index 13372bf49..ca432d9bf 100644 --- a/test/pleroma/workers/publisher_worker_test.exs +++ b/test/pleroma/workers/publisher_worker_test.exs @@ -7,7 +7,9 @@ defmodule Pleroma.Workers.PublisherWorkerTest do use Oban.Testing, repo: Pleroma.Repo import Pleroma.Factory + import Mock + alias Pleroma.Instances alias Pleroma.Object alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Builder @@ -37,4 +39,85 @@ defmodule Pleroma.Workers.PublisherWorkerTest do assert {:ok, %Oban.Job{priority: 0}} = Federator.publish(post) end end + + describe "Server reachability:" do + setup do + user = insert(:user) + remote_user = insert(:user, local: false, inbox: "https://example.com/inbox") + {:ok, _, _} = Pleroma.User.follow(remote_user, user) + {:ok, activity} = CommonAPI.post(user, %{status: "Test post"}) + + %{ + user: user, + remote_user: remote_user, + activity: activity + } + end + + test "marks server as unreachable only on final failure", %{activity: activity} do + with_mock Pleroma.Web.Federator, + perform: fn :publish_one, _params -> {:error, :connection_error} end do + # First attempt + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job) + assert Instances.reachable?("https://example.com/inbox") + + # Final attempt + job = %{job | attempt: 5} + assert {:error, :connection_error} = Pleroma.Workers.PublisherWorker.perform(job) + refute Instances.reachable?("https://example.com/inbox") + end + end + + test "does not mark server as unreachable on successful publish", %{activity: activity} do + with_mock Pleroma.Web.Federator, + perform: fn :publish_one, _params -> {:ok, %{status: 200}} end do + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert :ok = Pleroma.Workers.PublisherWorker.perform(job) + assert Instances.reachable?("https://example.com/inbox") + end + end + + test "cancels job if server is unreachable", %{activity: activity} do + # First mark the server as unreachable + Instances.set_unreachable("https://example.com/inbox") + refute Instances.reachable?("https://example.com/inbox") + + job = %Oban.Job{ + args: %{ + "op" => "publish_one", + "params" => %{ + "inbox" => "https://example.com/inbox", + "activity_id" => activity.id + } + }, + attempt: 1, + max_attempts: 5 + } + + assert {:cancel, :unreachable} = Pleroma.Workers.PublisherWorker.perform(job) + end + end end diff --git a/test/pleroma/workers/receiver_worker_test.exs b/test/pleroma/workers/receiver_worker_test.exs index 4d53c44ed..7f4789f91 100644 --- a/test/pleroma/workers/receiver_worker_test.exs +++ b/test/pleroma/workers/receiver_worker_test.exs @@ -3,13 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.ReceiverWorkerTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true use Oban.Testing, repo: Pleroma.Repo import Mock import Pleroma.Factory alias Pleroma.User + alias Pleroma.Web.CommonAPI alias Pleroma.Web.Federator alias Pleroma.Workers.ReceiverWorker @@ -243,4 +244,62 @@ defmodule Pleroma.Workers.ReceiverWorkerTest do assert {:cancel, _} = ReceiverWorker.perform(oban_job) end + + describe "Server reachability:" do + setup do + user = insert(:user) + remote_user = insert(:user, local: false, ap_id: "https://example.com/users/remote") + {:ok, _, _} = Pleroma.User.follow(user, remote_user) + {:ok, activity} = CommonAPI.post(remote_user, %{status: "Test post"}) + + %{ + user: user, + remote_user: remote_user, + activity: activity + } + end + + test "schedules ReachabilityWorker if host is unreachable", %{activity: activity} do + with_mocks [ + {Pleroma.Web.ActivityPub.Transmogrifier, [], + [handle_incoming: fn _ -> {:ok, activity} end]}, + {Pleroma.Instances, [], [reachable?: fn _ -> false end]}, + {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]} + ] do + job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => activity.data + } + } + + Pleroma.Workers.ReceiverWorker.perform(job) + + assert_enqueued( + worker: Pleroma.Workers.ReachabilityWorker, + args: %{"domain" => "example.com"} + ) + end + end + + test "does not schedule ReachabilityWorker if host is reachable", %{activity: activity} do + with_mocks [ + {Pleroma.Web.ActivityPub.Transmogrifier, [], + [handle_incoming: fn _ -> {:ok, activity} end]}, + {Pleroma.Instances, [], [reachable?: fn _ -> true end]}, + {Pleroma.Web.Federator, [], [perform: fn :incoming_ap_doc, _params -> {:ok, nil} end]} + ] do + job = %Oban.Job{ + args: %{ + "op" => "incoming_ap_doc", + "params" => activity.data + } + } + + Pleroma.Workers.ReceiverWorker.perform(job) + + refute_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + end + end + end end diff --git a/test/pleroma/workers/remote_fetcher_worker_test.exs b/test/pleroma/workers/remote_fetcher_worker_test.exs index 9caddb600..6eb6932cb 100644 --- a/test/pleroma/workers/remote_fetcher_worker_test.exs +++ b/test/pleroma/workers/remote_fetcher_worker_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Workers.RemoteFetcherWorkerTest do - use Pleroma.DataCase + use Pleroma.DataCase, async: true use Oban.Testing, repo: Pleroma.Repo alias Pleroma.Workers.RemoteFetcherWorker From b87ec4997244fc23d803948eccc778a603cf566f Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 6 Jun 2025 12:55:21 -0700 Subject: [PATCH 02/21] Nodeinfo is not universally implemented --- lib/pleroma/workers/reachability_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index 3a11dfe2a..d9f764322 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.ReachabilityWorker do @impl true def perform(%Oban.Job{args: %{"domain" => domain}}) do - case HTTP.get("https://#{domain}/.well-known/nodeinfo") do + case HTTP.get("https://#{domain}/") do {:ok, %{status: status}} when status in 200..299 -> Instances.set_reachable("https://#{domain}") :ok From 0f667761a9349a852c549c0bfb846b793607e397 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 6 Jun 2025 13:00:54 -0700 Subject: [PATCH 03/21] The ap_id is a URL, so we can just pass that to set_reachable/1 Also only bother attempting to mark reachable if it was known to be unreachable --- lib/pleroma/workers/remote_fetcher_worker.ex | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/workers/remote_fetcher_worker.ex b/lib/pleroma/workers/remote_fetcher_worker.ex index 5f57ec2d7..0cc480c02 100644 --- a/lib/pleroma/workers/remote_fetcher_worker.ex +++ b/lib/pleroma/workers/remote_fetcher_worker.ex @@ -12,13 +12,9 @@ defmodule Pleroma.Workers.RemoteFetcherWorker do def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do case Fetcher.fetch_object_from_id(id, depth: args["depth"]) do {:ok, _object} -> - # Mark the server as reachable since we successfully fetched an object - case URI.parse(id) do - %URI{host: host} when not is_nil(host) -> - Instances.set_reachable("https://#{host}") - - _ -> - :ok + unless Instances.reachable?(id) do + # Mark the server as reachable since we successfully fetched an object + Instances.set_reachable(id) end :ok From 0fe03fc4eef0159e3015d68d75ec42ea11f649cf Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 6 Jun 2025 13:44:24 -0700 Subject: [PATCH 04/21] Revert "Nodeinfo is not universally implemented" This reverts commit b87ec4997244fc23d803948eccc778a603cf566f. --- lib/pleroma/workers/reachability_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index d9f764322..3a11dfe2a 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.ReachabilityWorker do @impl true def perform(%Oban.Job{args: %{"domain" => domain}}) do - case HTTP.get("https://#{domain}/") do + case HTTP.get("https://#{domain}/.well-known/nodeinfo") do {:ok, %{status: status}} when status in 200..299 -> Instances.set_reachable("https://#{domain}") :ok From 83c97568259d5bf34f2117f37c5ec61495f9bc5b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 6 Jun 2025 17:10:33 -0700 Subject: [PATCH 05/21] Remove unncessary NaiveDateTime call. Every non-nil entry in the database is considered unreachable. --- lib/pleroma/instances/instance.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index baccc314c..7b7127973 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -84,7 +84,7 @@ defmodule Pleroma.Instances.Instance do from(i in Instance, where: i.host == ^host(url_or_host) and - i.unreachable_since <= ^NaiveDateTime.utc_now(), + not is_nil(i.unreachable_since), select: true ) ) From 3984ba87217e2a9fdc89c22ff2357c49563c5ad2 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Sat, 7 Jun 2025 22:51:26 +0400 Subject: [PATCH 06/21] Fix typo in changelog filename --- changelog.d/fix-public-url-addressing.fix | 1 + changelog.d/{reachabililty.change => reachability.change} | 0 2 files changed, 1 insertion(+) create mode 100644 changelog.d/fix-public-url-addressing.fix rename changelog.d/{reachabililty.change => reachability.change} (100%) diff --git a/changelog.d/fix-public-url-addressing.fix b/changelog.d/fix-public-url-addressing.fix new file mode 100644 index 000000000..810b76905 --- /dev/null +++ b/changelog.d/fix-public-url-addressing.fix @@ -0,0 +1 @@ +- Fixed an issue where the ActivityStreams Public collection URL was being removed from incoming activities' cc fields diff --git a/changelog.d/reachabililty.change b/changelog.d/reachability.change similarity index 100% rename from changelog.d/reachabililty.change rename to changelog.d/reachability.change From 2748891e124ede3619cb2a77e27b83fbb8a724f8 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 7 Jun 2025 12:23:47 -0700 Subject: [PATCH 07/21] Change the inboxes assignment in the Publisher to better indicate it's a list containing two lists This clarifies what is really going on here and removes confusion about the nested Enum.each |> Enum.each which both were using an assignment called "inboxes" --- lib/pleroma/web/activity_pub/publisher.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 78312b771..4a5cbd64e 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -282,7 +282,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do [priority_recipients, recipients] = recipients(actor, activity) - inboxes = + [priority_inboxes, other_inboxes] = [priority_recipients, recipients] |> Enum.map(fn recipients -> recipients @@ -295,7 +295,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do end) Repo.checkout(fn -> - Enum.each(inboxes, fn inboxes -> + Enum.each([priority_inboxes, other_inboxes], fn inboxes -> Enum.each(inboxes, fn inbox -> %User{ap_id: ap_id} = Enum.find(recipients, fn actor -> actor.inbox == inbox end) From 8383584d692f56dfdd4b88d328d5d54962f82ed1 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 7 Jun 2025 14:57:34 -0700 Subject: [PATCH 08/21] Reapply "Nodeinfo is not universally implemented" This reverts commit 0fe03fc4eef0159e3015d68d75ec42ea11f649cf. --- lib/pleroma/workers/reachability_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index 3a11dfe2a..d9f764322 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Workers.ReachabilityWorker do @impl true def perform(%Oban.Job{args: %{"domain" => domain}}) do - case HTTP.get("https://#{domain}/.well-known/nodeinfo") do + case HTTP.get("https://#{domain}/") do {:ok, %{status: status}} when status in 200..299 -> Instances.set_reachable("https://#{domain}") :ok From a46a48fb3f508da67b171b54c91bb027c13aa22b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 7 Jun 2025 15:13:45 -0700 Subject: [PATCH 09/21] PublisherWorker: change max_attempts to 13 which extends the last delivery attempt to ~4.3 days --- lib/pleroma/workers/publisher_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/publisher_worker.ex b/lib/pleroma/workers/publisher_worker.ex index 10736bef5..f799af77a 100644 --- a/lib/pleroma/workers/publisher_worker.ex +++ b/lib/pleroma/workers/publisher_worker.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Workers.PublisherWorker do alias Pleroma.Instances alias Pleroma.Web.Federator - use Oban.Worker, queue: :federator_outgoing, max_attempts: 5 + use Oban.Worker, queue: :federator_outgoing, max_attempts: 13 @impl true def perform(%Job{args: %{"op" => "publish", "activity_id" => activity_id}}) do From 59bfa83c9ce372b6413ff5498cad030b97a7af2d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 16:04:08 -0700 Subject: [PATCH 10/21] Remove daily reachability scheduling for unreachable instances --- config/config.exs | 3 +- .../cron/schedule_reachability_worker.ex | 33 ------------------- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 lib/pleroma/workers/cron/schedule_reachability_worker.ex diff --git a/config/config.exs b/config/config.exs index 805cd0d62..372852a7b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -603,8 +603,7 @@ config :pleroma, Oban, crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, - {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}, - {"0 0 * * *", Pleroma.Workers.Cron.ScheduleReachabilityWorker} + {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker} ] config :pleroma, Pleroma.Formatter, diff --git a/lib/pleroma/workers/cron/schedule_reachability_worker.ex b/lib/pleroma/workers/cron/schedule_reachability_worker.ex deleted file mode 100644 index a0b8e261c..000000000 --- a/lib/pleroma/workers/cron/schedule_reachability_worker.ex +++ /dev/null @@ -1,33 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.ScheduleReachabilityWorker do - use Oban.Worker, - queue: :background, - max_attempts: 2 - - alias Pleroma.Instances - alias Pleroma.Repo - - @impl true - def perform(_job) do - unreachable_servers = Instances.get_unreachable() - - jobs = - unreachable_servers - |> Enum.map(fn {domain, _} -> - Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) - end) - - case Repo.transaction(fn -> - Enum.each(jobs, &Oban.insert/1) - end) do - {:ok, _} -> - :ok - - {:error, reason} -> - {:error, reason} - end - end -end From 77dca7c3e59053505abb4fa757b2d97e227fa4f4 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 16:35:10 -0700 Subject: [PATCH 11/21] Refactor ReachabilityWorker to use a 5-phase reachability testing approach It will check reachability for an instance deemed unreachable at the following intervals: 4 attempts, once a minute 4 attempts, once every 15 minutes 4 attempts, once every 60 minutes 4 attempts, once every 8 hours 4 attempts, once every 24 hours This should be effective and respectful of the resources of instances on the fediverse. We have the Oban Pruner plugin enabled to keep the Oban Jobs table from growing indefinitely. It prunes every 15 minutes, but this will interfere with our ability to enforce uniqueness on the ReachabilityWorker jobs for a time period longer than 15 minutes. The solution is to exclude the ReachabilityWorker from the pruning operation and instead schedule a custom job that will prune the table for us once a day. The ReachabilityPruner cron task will clean up the history of the ReachabilityWorker jobs older than 6 days. --- config/config.exs | 5 +- .../workers/cron/reachability_pruner.ex | 26 +++ lib/pleroma/workers/reachability_worker.ex | 71 +++++- .../schedule_reachability_worker_test.exs | 52 ----- .../workers/reachability_worker_test.exs | 202 ++++++++++++++++++ 5 files changed, 296 insertions(+), 60 deletions(-) create mode 100644 lib/pleroma/workers/cron/reachability_pruner.ex delete mode 100644 test/pleroma/workers/cron/schedule_reachability_worker_test.exs create mode 100644 test/pleroma/workers/reachability_worker_test.exs diff --git a/config/config.exs b/config/config.exs index 372852a7b..f58dfb1af 100644 --- a/config/config.exs +++ b/config/config.exs @@ -599,11 +599,12 @@ config :pleroma, Oban, search_indexing: [limit: 10, paused: true], slow: 5 ], - plugins: [{Oban.Plugins.Pruner, max_age: 900}], + plugins: [{Oban.Plugins.Pruner, max_age: 900, exclude: [Pleroma.Workers.ReachabilityWorker]}], crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, - {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker} + {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}, + {"0 2 * * *", Pleroma.Workers.Cron.ReachabilityPruner} ] config :pleroma, Pleroma.Formatter, diff --git a/lib/pleroma/workers/cron/reachability_pruner.ex b/lib/pleroma/workers/cron/reachability_pruner.ex new file mode 100644 index 000000000..6eb671e0e --- /dev/null +++ b/lib/pleroma/workers/cron/reachability_pruner.ex @@ -0,0 +1,26 @@ +defmodule Pleroma.Workers.Cron.ReachabilityPruner do + use Oban.Worker, queue: :background, max_attempts: 1 + + import Ecto.Query + require Logger + + @reachability_worker "Elixir.Pleroma.Workers.ReachabilityWorker" + @prune_days 6 + + @impl true + def perform(_job) do + cutoff = DateTime.utc_now() |> DateTime.add(-@prune_days * 24 * 60 * 60, :second) + + {count, _} = + from(j in Oban.Job, + where: j.worker == @reachability_worker and j.inserted_at < ^cutoff + ) + |> Pleroma.Repo.delete_all() + + if count > 0 do + Logger.debug(fn -> "Pruned #{count} old ReachabilityWorker jobs." end) + end + + :ok + end +end diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index d9f764322..ba6928dee 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -5,17 +5,31 @@ defmodule Pleroma.Workers.ReachabilityWorker do use Oban.Worker, queue: :background, - max_attempts: 3, - unique: [period: :infinity, states: [:available, :scheduled]] + max_attempts: 1, + unique: [period: :infinity, states: [:available, :scheduled], keys: [:domain]] alias Pleroma.HTTP alias Pleroma.Instances @impl true - def perform(%Oban.Job{args: %{"domain" => domain}}) do + def perform(%Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}}) do + case check_reachability(domain) do + :ok -> + Instances.set_reachable("https://#{domain}") + :ok + + {:error, _} = error -> + handle_failed_attempt(domain, phase, attempt) + error + end + end + + @impl true + def timeout(_job), do: :timer.seconds(5) + + defp check_reachability(domain) do case HTTP.get("https://#{domain}/") do {:ok, %{status: status}} when status in 200..299 -> - Instances.set_reachable("https://#{domain}") :ok {:ok, %{status: _status}} -> @@ -26,6 +40,51 @@ defmodule Pleroma.Workers.ReachabilityWorker do end end - @impl true - def timeout(_job), do: :timer.seconds(5) + defp handle_failed_attempt(_domain, "final", _attempt), do: :ok + + defp handle_failed_attempt(domain, phase, attempt) do + {interval_minutes, max_attempts, next_phase} = get_phase_config(phase) + + if attempt >= max_attempts do + # Move to next phase + schedule_next_phase(domain, next_phase) + else + # Retry same phase with incremented attempt + schedule_retry(domain, phase, attempt + 1, interval_minutes) + end + end + + defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"} + defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"} + defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"} + defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"} + defp get_phase_config("phase_24hour"), do: {1440, 4, "final"} + defp get_phase_config("final"), do: {nil, 0, nil} + + defp schedule_next_phase(_domain, "final"), do: :ok + + defp schedule_next_phase(domain, next_phase) do + {interval_minutes, _max_attempts, _next_phase} = get_phase_config(next_phase) + scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second) + + %{ + "domain" => domain, + "phase" => next_phase, + "attempt" => 1 + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + end + + def schedule_retry(domain, phase, attempt, interval_minutes) do + scheduled_at = DateTime.add(DateTime.utc_now(), interval_minutes * 60, :second) + + %{ + "domain" => domain, + "phase" => phase, + "attempt" => attempt + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + end end diff --git a/test/pleroma/workers/cron/schedule_reachability_worker_test.exs b/test/pleroma/workers/cron/schedule_reachability_worker_test.exs deleted file mode 100644 index 310c2e61a..000000000 --- a/test/pleroma/workers/cron/schedule_reachability_worker_test.exs +++ /dev/null @@ -1,52 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2022 Pleroma Authors -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Workers.Cron.ScheduleReachabilityWorkerTest do - use Pleroma.DataCase, async: true - use Oban.Testing, repo: Pleroma.Repo - - alias Pleroma.Instances - alias Pleroma.Workers.Cron.ScheduleReachabilityWorker - - describe "perform/1" do - test "schedules reachability checks for unreachable servers" do - # Mark some servers as unreachable - Instances.set_unreachable("https://example.com") - Instances.set_unreachable("https://test.com") - Instances.set_unreachable("https://another.com") - - # Verify they are marked as unreachable - refute Instances.reachable?("https://example.com") - refute Instances.reachable?("https://test.com") - refute Instances.reachable?("https://another.com") - - # Run the worker - assert :ok = ScheduleReachabilityWorker.perform(%Oban.Job{}) - - # Verify ReachabilityWorker jobs were scheduled for each server - # Note: domains in get_unreachable/0 are without the https:// prefix - assert_enqueued( - worker: Pleroma.Workers.ReachabilityWorker, - args: %{"domain" => "example.com"} - ) - - assert_enqueued( - worker: Pleroma.Workers.ReachabilityWorker, - args: %{"domain" => "test.com"} - ) - - assert_enqueued( - worker: Pleroma.Workers.ReachabilityWorker, - args: %{"domain" => "another.com"} - ) - end - - test "handles empty list of unreachable servers" do - # Ensure no servers are marked as unreachable - assert [] = Instances.get_unreachable() - assert :ok = ScheduleReachabilityWorker.perform(%Oban.Job{}) - refute_enqueued(worker: Pleroma.Workers.ReachabilityWorker) - end - end -end diff --git a/test/pleroma/workers/reachability_worker_test.exs b/test/pleroma/workers/reachability_worker_test.exs new file mode 100644 index 000000000..32c39e869 --- /dev/null +++ b/test/pleroma/workers/reachability_worker_test.exs @@ -0,0 +1,202 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2022 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Workers.ReachabilityWorkerTest do + use Pleroma.DataCase, async: true + use Oban.Testing, repo: Pleroma.Repo + + import Mock + + alias Pleroma.Tests.ObanHelpers + alias Pleroma.Workers.ReachabilityWorker + + setup do + ObanHelpers.wipe_all() + :ok + end + + describe "progressive backoff phases" do + test "starts with phase_1min and progresses through phases on failure" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Start with phase_1min + job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1} + } + + # First attempt fails + assert {:error, :timeout} = ReachabilityWorker.perform(job) + + # Should schedule retry for phase_1min (attempt 2) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 2 + + # Clear jobs and simulate second attempt failure + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 2} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule retry for phase_1min (attempt 3) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 3 + + # Clear jobs and simulate third attempt failure (final attempt for phase_1min) + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 3} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule retry for phase_1min (attempt 4) + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == "phase_1min" + assert retry_job.args["attempt"] == 4 + + # Clear jobs and simulate fourth attempt failure (final attempt for phase_1min) + ObanHelpers.wipe_all() + + retry_job = %Oban.Job{ + args: %{"domain" => domain, "phase" => "phase_1min", "attempt" => 4} + } + + assert {:error, :timeout} = ReachabilityWorker.perform(retry_job) + + # Should schedule next phase (phase_15min) + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 1 + [next_phase_job] = next_phase_jobs + assert next_phase_job.args["phase"] == "phase_15min" + assert next_phase_job.args["attempt"] == 1 + end + end + + test "progresses through all phases correctly" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:error, :timeout} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Simulate all phases failing + phases = ["phase_1min", "phase_15min", "phase_1hour", "phase_8hour", "phase_24hour"] + + Enum.each(phases, fn phase -> + {_interval, max_attempts, next_phase} = get_phase_config(phase) + + # Simulate all attempts failing for this phase + Enum.each(1..max_attempts, fn attempt -> + job = %Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}} + assert {:error, :timeout} = ReachabilityWorker.perform(job) + + if attempt < max_attempts do + # Should schedule retry for same phase + retry_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(retry_jobs) == 1 + [retry_job] = retry_jobs + assert retry_job.args["phase"] == phase + assert retry_job.args["attempt"] == attempt + 1 + ObanHelpers.wipe_all() + else + # Should schedule next phase (except for final phase) + if next_phase != "final" do + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 1 + [next_phase_job] = next_phase_jobs + assert next_phase_job.args["phase"] == next_phase + assert next_phase_job.args["attempt"] == 1 + ObanHelpers.wipe_all() + else + # Final phase - no more jobs should be scheduled + next_phase_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_phase_jobs) == 0 + end + end + end) + end) + end + end + + test "succeeds and stops progression when instance becomes reachable" do + domain = "example.com" + + with_mocks([ + {Pleroma.HTTP, [], [get: fn _ -> {:ok, %{status: 200}} end]}, + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + job = %Oban.Job{args: %{"domain" => domain, "phase" => "phase_1hour", "attempt" => 2}} + + # Should succeed and not schedule any more jobs + assert :ok = ReachabilityWorker.perform(job) + + # Verify set_reachable was called + assert_called(Pleroma.Instances.set_reachable("https://#{domain}")) + + # No more jobs should be scheduled + next_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(next_jobs) == 0 + end + end + + test "enforces uniqueness per domain using Oban's conflict detection" do + domain = "example.com" + + # Insert first job for the domain + job1 = + %{ + "domain" => domain, + "phase" => "phase_1min", + "attempt" => 1 + } + |> ReachabilityWorker.new() + |> Oban.insert() + + assert {:ok, _} = job1 + + # Try to insert a second job for the same domain with different phase/attempt + job2 = + %{ + "domain" => domain, + "phase" => "phase_15min", + "attempt" => 1 + } + |> ReachabilityWorker.new() + |> Oban.insert() + + # Should fail due to uniqueness constraint (conflict) + assert {:ok, %Oban.Job{conflict?: true}} = job2 + + # Verify only one job exists for this domain + jobs = all_enqueued(worker: ReachabilityWorker) + assert length(jobs) == 1 + [existing_job] = jobs + assert existing_job.args["domain"] == domain + assert existing_job.args["phase"] == "phase_1min" + end + end + + defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"} + defp get_phase_config("phase_15min"), do: {15, 4, "phase_1hour"} + defp get_phase_config("phase_1hour"), do: {60, 4, "phase_8hour"} + defp get_phase_config("phase_8hour"), do: {480, 4, "phase_24hour"} + defp get_phase_config("phase_24hour"), do: {1440, 4, "final"} + defp get_phase_config("final"), do: {nil, 0, nil} +end From 6e4b5edc257aa7555dd7c82d2884e0beac0c60ac Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 17:13:34 -0700 Subject: [PATCH 12/21] Reduce pruning of history to anything older than 2 days --- lib/pleroma/workers/cron/reachability_pruner.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/workers/cron/reachability_pruner.ex b/lib/pleroma/workers/cron/reachability_pruner.ex index 6eb671e0e..51cfdad3c 100644 --- a/lib/pleroma/workers/cron/reachability_pruner.ex +++ b/lib/pleroma/workers/cron/reachability_pruner.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Workers.Cron.ReachabilityPruner do require Logger @reachability_worker "Elixir.Pleroma.Workers.ReachabilityWorker" - @prune_days 6 + @prune_days 2 @impl true def perform(_job) do From a5e11ad1101dc86309152666b855e1c065a6eabc Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 17:24:02 -0700 Subject: [PATCH 13/21] Custom pruning is not actually needed because an old job cannot exist in the table due to our use of [replace: true] when retrying jobs or walking it through the different phases --- config/config.exs | 5 ++-- .../workers/cron/reachability_pruner.ex | 26 ------------------- 2 files changed, 2 insertions(+), 29 deletions(-) delete mode 100644 lib/pleroma/workers/cron/reachability_pruner.ex diff --git a/config/config.exs b/config/config.exs index f58dfb1af..372852a7b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -599,12 +599,11 @@ config :pleroma, Oban, search_indexing: [limit: 10, paused: true], slow: 5 ], - plugins: [{Oban.Plugins.Pruner, max_age: 900, exclude: [Pleroma.Workers.ReachabilityWorker]}], + plugins: [{Oban.Plugins.Pruner, max_age: 900}], crontab: [ {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker}, - {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}, - {"0 2 * * *", Pleroma.Workers.Cron.ReachabilityPruner} + {"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker} ] config :pleroma, Pleroma.Formatter, diff --git a/lib/pleroma/workers/cron/reachability_pruner.ex b/lib/pleroma/workers/cron/reachability_pruner.ex deleted file mode 100644 index 51cfdad3c..000000000 --- a/lib/pleroma/workers/cron/reachability_pruner.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Pleroma.Workers.Cron.ReachabilityPruner do - use Oban.Worker, queue: :background, max_attempts: 1 - - import Ecto.Query - require Logger - - @reachability_worker "Elixir.Pleroma.Workers.ReachabilityWorker" - @prune_days 2 - - @impl true - def perform(_job) do - cutoff = DateTime.utc_now() |> DateTime.add(-@prune_days * 24 * 60 * 60, :second) - - {count, _} = - from(j in Oban.Job, - where: j.worker == @reachability_worker and j.inserted_at < ^cutoff - ) - |> Pleroma.Repo.delete_all() - - if count > 0 do - Logger.debug(fn -> "Pruned #{count} old ReachabilityWorker jobs." end) - end - - :ok - end -end From 13db730659c7abd902cd7d59aecaf1bb36ab58d2 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 17:52:00 -0700 Subject: [PATCH 14/21] Update Oban to 2.19 which gives us the delete_all_jobs/1 and delete_job/1 functions --- mix.exs | 2 +- mix.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index 971084f94..b137802e4 100644 --- a/mix.exs +++ b/mix.exs @@ -136,7 +136,7 @@ defmodule Pleroma.Mixfile do {:telemetry_poller, "~> 1.0"}, {:tzdata, "~> 1.0.3"}, {:plug_cowboy, "~> 2.5"}, - {:oban, "~> 2.18.0"}, + {:oban, "~> 2.19.0"}, {:gettext, "~> 0.20"}, {:bcrypt_elixir, "~> 2.2"}, {:trailing_format_plug, "~> 0.0.7"}, diff --git a/mix.lock b/mix.lock index f7f37b7e1..7e86b5683 100644 --- a/mix.lock +++ b/mix.lock @@ -26,8 +26,8 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, + "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"}, @@ -92,7 +92,7 @@ "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, - "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"}, + "oban": {:hex, :oban, "2.19.4", "045adb10db1161dceb75c254782f97cdc6596e7044af456a59decb6d06da73c1", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fcc6219e6464525b808d97add17896e724131f498444a292071bf8991c99f97"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, "open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"}, From ff5f88aae314a61f4c766762056094216e00b89d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 18:07:46 -0700 Subject: [PATCH 15/21] Instance.set_reachable/1 should delete any existing ReachabilityWorker jobs for that instance --- lib/pleroma/instances/instance.ex | 17 +++++++++++++--- test/pleroma/instances/instance_test.exs | 26 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index fb0b9d7f0..620544134 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -92,9 +92,20 @@ defmodule Pleroma.Instances.Instance do def reachable?(url_or_host) when is_binary(url_or_host), do: true def set_reachable(url_or_host) when is_binary(url_or_host) do - %Instance{host: host(url_or_host)} - |> changeset(%{unreachable_since: nil}) - |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host) + host = host(url_or_host) + + result = + %Instance{host: host} + |> changeset(%{unreachable_since: nil}) + |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host) + + # Delete any existing reachability testing jobs for this instance + Oban.Job + |> Ecto.Query.where(worker: "Pleroma.Workers.ReachabilityWorker") + |> Ecto.Query.where([j], j.args["domain"] == ^host) + |> Oban.delete_all_jobs() + + result end def set_reachable(_), do: {:error, nil} diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index ed536c55c..354ba139a 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -27,6 +27,32 @@ defmodule Pleroma.Instances.InstanceTest do assert {:ok, instance} = Instance.set_reachable(instance.host) refute instance.unreachable_since end + + test "cancels all ReachabilityWorker jobs for the domain" do + domain = "cancelme.example.org" + insert(:instance, host: domain, unreachable_since: NaiveDateTime.utc_now()) + + # Insert a ReachabilityWorker job for this domain, scheduled 5 minutes in the future + scheduled_at = DateTime.add(DateTime.utc_now(), 300, :second) + + {:ok, job} = + Pleroma.Workers.ReachabilityWorker.new( + %{"domain" => domain, "phase" => "phase_1min", "attempt" => 1}, + scheduled_at: scheduled_at + ) + |> Oban.insert() + + # Ensure the job is present + job = Pleroma.Repo.get(Oban.Job, job.id) + assert job + + # Call set_reachable, which should delete the job + assert {:ok, _} = Instance.set_reachable(domain) + + # Reload the job and assert it is deleted + job = Pleroma.Repo.get(Oban.Job, job.id) + refute job + end end describe "set_unreachable/1" do From 2267ace10687d9289750932a7809fb7e5c4cc496 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 18:12:18 -0700 Subject: [PATCH 16/21] Ensure ReachabilityWorker jobs can be scheduled without needing awareness of the phase design --- lib/pleroma/workers/reachability_worker.ex | 16 +++++++++++++ .../workers/reachability_worker_test.exs | 24 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index ba6928dee..badfa476c 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -24,6 +24,22 @@ defmodule Pleroma.Workers.ReachabilityWorker do end end + # New jobs enter here and are immediately re-scheduled for the first phase + @impl true + def perform(%Oban.Job{args: %{"domain" => domain}}) do + scheduled_at = DateTime.add(DateTime.utc_now(), 60, :second) + + %{ + "domain" => domain, + "phase" => "phase_1min", + "attempt" => 1 + } + |> new(scheduled_at: scheduled_at, replace: true) + |> Oban.insert() + + :ok + end + @impl true def timeout(_job), do: :timer.seconds(5) diff --git a/test/pleroma/workers/reachability_worker_test.exs b/test/pleroma/workers/reachability_worker_test.exs index 32c39e869..4854aff77 100644 --- a/test/pleroma/workers/reachability_worker_test.exs +++ b/test/pleroma/workers/reachability_worker_test.exs @@ -191,6 +191,30 @@ defmodule Pleroma.Workers.ReachabilityWorkerTest do assert existing_job.args["domain"] == domain assert existing_job.args["phase"] == "phase_1min" end + + test "handles new jobs with only domain argument and transitions them to the first phase" do + domain = "legacy.example.com" + + with_mocks([ + {Pleroma.Instances, [], [set_reachable: fn _ -> :ok end]} + ]) do + # Create a job with only domain (legacy format) + job = %Oban.Job{ + args: %{"domain" => domain} + } + + # Should reschedule with phase_1min and attempt 1 + assert :ok = ReachabilityWorker.perform(job) + + # Check that a new job was scheduled with the correct format + scheduled_jobs = all_enqueued(worker: ReachabilityWorker) + assert length(scheduled_jobs) == 1 + [scheduled_job] = scheduled_jobs + assert scheduled_job.args["domain"] == domain + assert scheduled_job.args["phase"] == "phase_1min" + assert scheduled_job.args["attempt"] == 1 + end + end end defp get_phase_config("phase_1min"), do: {1, 4, "phase_15min"} From 8a0551686238af40ac21a2a9152f2a218c69d04e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Fri, 27 Jun 2025 18:23:24 -0700 Subject: [PATCH 17/21] Remove changelog entry that leaked in via 3984ba87217e2a9fdc89c22ff2357c49563c5ad2 --- changelog.d/fix-public-url-addressing.fix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/fix-public-url-addressing.fix diff --git a/changelog.d/fix-public-url-addressing.fix b/changelog.d/fix-public-url-addressing.fix deleted file mode 100644 index 810b76905..000000000 --- a/changelog.d/fix-public-url-addressing.fix +++ /dev/null @@ -1 +0,0 @@ -- Fixed an issue where the ActivityStreams Public collection URL was being removed from incoming activities' cc fields From 29f76079107f12e14b87e58c804ff10550381478 Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 28 Jun 2025 12:51:10 -0700 Subject: [PATCH 18/21] Add Instances.check_all_unreachable/0 and Instance.check_unreachable/1 --- lib/pleroma/instances.ex | 9 ++++++ lib/pleroma/instances/instance.ex | 6 ++++ test/pleroma/instances/instance_test.exs | 30 ++++++++++++++++++ test/pleroma/instances_test.exs | 40 ++++++++++++++++++++++++ 4 files changed, 85 insertions(+) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 9237e0944..a69554ada 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -24,4 +24,13 @@ defmodule Pleroma.Instances do url_or_host end end + + @doc "Schedules reachability checks for all unreachable instances" + def check_all_unreachable do + get_unreachable() + |> Enum.each(fn {domain, _} -> + Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) + |> Oban.insert() + end) + end end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 620544134..dca30275b 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -304,4 +304,10 @@ defmodule Pleroma.Instances.Instance do DeleteWorker.new(%{"op" => "delete_instance", "host" => host}) |> Oban.insert() end + + @doc "Schedules reachability check for instance" + def check_unreachable(domain) when is_binary(domain) do + Pleroma.Workers.ReachabilityWorker.new(%{"domain" => domain}) + |> Oban.insert() + end end diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 354ba139a..83e70ac38 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -249,4 +249,34 @@ defmodule Pleroma.Instances.InstanceTest do args: %{"op" => "delete_instance", "host" => "mushroom.kingdom"} ) end + + describe "check_unreachable/1" do + test "schedules a ReachabilityWorker job for the given domain" do + domain = "test.example.com" + + # Call check_unreachable + assert {:ok, _job} = Instance.check_unreachable(domain) + + # Verify that a ReachabilityWorker job was scheduled + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == domain + end + + test "handles multiple calls for the same domain (uniqueness enforced)" do + domain = "duplicate.example.com" + + assert {:ok, _job1} = Instance.check_unreachable(domain) + + # Second call for the same domain + assert {:ok, %Oban.Job{conflict?: true}} = Instance.check_unreachable(domain) + + # Should only have one job due to uniqueness + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == domain + end + end end diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs index cbafbfa44..8e23fd096 100644 --- a/test/pleroma/instances_test.exs +++ b/test/pleroma/instances_test.exs @@ -6,6 +6,7 @@ defmodule Pleroma.InstancesTest do alias Pleroma.Instances use Pleroma.DataCase + use Oban.Testing, repo: Pleroma.Repo describe "reachable?/1" do test "returns `true` for host / url with unknown reachability status" do @@ -69,4 +70,43 @@ defmodule Pleroma.InstancesTest do assert {:error, _} = Instances.set_unreachable(1) end end + + describe "check_all_unreachable/0" do + test "schedules ReachabilityWorker jobs for all unreachable instances" do + domain1 = "unreachable1.example.com" + domain2 = "unreachable2.example.com" + domain3 = "unreachable3.example.com" + + Instances.set_unreachable(domain1) + Instances.set_unreachable(domain2) + Instances.set_unreachable(domain3) + + Instances.check_all_unreachable() + + # Verify that ReachabilityWorker jobs were scheduled for all unreachable domains + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 3 + + domains = Enum.map(jobs, & &1.args["domain"]) + assert domain1 in domains + assert domain2 in domains + assert domain3 in domains + end + + test "does not schedule jobs for reachable instances" do + unreachable_domain = "unreachable.example.com" + reachable_domain = "reachable.example.com" + + Instances.set_unreachable(unreachable_domain) + Instances.set_reachable(reachable_domain) + + Instances.check_all_unreachable() + + # Verify that only one job was scheduled (for the unreachable domain) + jobs = all_enqueued(worker: Pleroma.Workers.ReachabilityWorker) + assert length(jobs) == 1 + [job] = jobs + assert job.args["domain"] == unreachable_domain + end + end end From f06f0bedd305706ba8dd7cc38d421e2831f43d0b Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 28 Jun 2025 13:14:01 -0700 Subject: [PATCH 19/21] Clean up ReachabilityWorker jobs and delete from Instances table when deleting all users and activities for an instance --- lib/pleroma/instances/instance.ex | 6 +----- lib/pleroma/workers/delete_worker.ex | 12 ++++++++++++ lib/pleroma/workers/reachability_worker.ex | 10 ++++++++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index dca30275b..cf896ca08 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -99,11 +99,7 @@ defmodule Pleroma.Instances.Instance do |> changeset(%{unreachable_since: nil}) |> Repo.insert(on_conflict: {:replace, [:unreachable_since]}, conflict_target: :host) - # Delete any existing reachability testing jobs for this instance - Oban.Job - |> Ecto.Query.where(worker: "Pleroma.Workers.ReachabilityWorker") - |> Ecto.Query.where([j], j.args["domain"] == ^host) - |> Oban.delete_all_jobs() + Pleroma.Workers.ReachabilityWorker.delete_jobs_for_host(host) result end diff --git a/lib/pleroma/workers/delete_worker.ex b/lib/pleroma/workers/delete_worker.ex index 4f52edd28..b83185fff 100644 --- a/lib/pleroma/workers/delete_worker.ex +++ b/lib/pleroma/workers/delete_worker.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Workers.DeleteWorker do end def perform(%Job{args: %{"op" => "delete_instance", "host" => host}}) do + # Schedule the per-user deletion jobs Pleroma.Repo.transaction(fn -> User.Query.build(%{nickname: "@#{host}"}) |> Pleroma.Repo.all() @@ -22,6 +23,17 @@ defmodule Pleroma.Workers.DeleteWorker do |> __MODULE__.new() |> Oban.insert() end) + + # Delete the instance from the Instances table + case Pleroma.Repo.get_by(Pleroma.Instances.Instance, host: host) do + nil -> :ok + instance -> Pleroma.Repo.delete(instance) + end + + # Delete any pending ReachabilityWorker jobs for this domain + Pleroma.Workers.ReachabilityWorker.delete_jobs_for_host(host) + + :ok end) end diff --git a/lib/pleroma/workers/reachability_worker.ex b/lib/pleroma/workers/reachability_worker.ex index badfa476c..41981a2e4 100644 --- a/lib/pleroma/workers/reachability_worker.ex +++ b/lib/pleroma/workers/reachability_worker.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Workers.ReachabilityWorker do alias Pleroma.HTTP alias Pleroma.Instances + import Ecto.Query + @impl true def perform(%Oban.Job{args: %{"domain" => domain, "phase" => phase, "attempt" => attempt}}) do case check_reachability(domain) do @@ -43,6 +45,14 @@ defmodule Pleroma.Workers.ReachabilityWorker do @impl true def timeout(_job), do: :timer.seconds(5) + @doc "Deletes scheduled jobs to check reachability for specified instance" + def delete_jobs_for_host(host) do + Oban.Job + |> where(worker: "Pleroma.Workers.ReachabilityWorker") + |> where([j], j.args["domain"] == ^host) + |> Oban.delete_all_jobs() + end + defp check_reachability(domain) do case HTTP.get("https://#{domain}/") do {:ok, %{status: status}} when status in 200..299 -> From df0880d8d12e557ca79161bc9a942bc8b3655d4e Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 28 Jun 2025 13:23:37 -0700 Subject: [PATCH 20/21] Add Instances.delete_all_unreachable/0 --- lib/pleroma/instances.ex | 8 ++++++++ test/pleroma/instances_test.exs | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index a69554ada..79fbd538f 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -33,4 +33,12 @@ defmodule Pleroma.Instances do |> Oban.insert() end) end + + @doc "Deletes all users and activities for unreachable instances" + def delete_all_unreachable do + get_unreachable() + |> Enum.each(fn {domain, _} -> + Instance.delete_users_and_activities(domain) + end) + end end diff --git a/test/pleroma/instances_test.exs b/test/pleroma/instances_test.exs index 8e23fd096..c8618b748 100644 --- a/test/pleroma/instances_test.exs +++ b/test/pleroma/instances_test.exs @@ -109,4 +109,30 @@ defmodule Pleroma.InstancesTest do assert job.args["domain"] == unreachable_domain end end + + test "delete_all_unreachable/0 schedules DeleteWorker jobs for all unreachable instances" do + domain1 = "unreachable1.example.com" + domain2 = "unreachable2.example.com" + domain3 = "unreachable3.example.com" + + Instances.set_unreachable(domain1) + Instances.set_unreachable(domain2) + Instances.set_unreachable(domain3) + + Instances.delete_all_unreachable() + + # Verify that DeleteWorker jobs were scheduled for all unreachable domains + jobs = all_enqueued(worker: Pleroma.Workers.DeleteWorker) + assert length(jobs) == 3 + + domains = Enum.map(jobs, & &1.args["host"]) + assert domain1 in domains + assert domain2 in domains + assert domain3 in domains + + # Verify all jobs are delete_instance operations + Enum.each(jobs, fn job -> + assert job.args["op"] == "delete_instance" + end) + end end From 59844d020212b1df85101a142b2102d62bdccaef Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Sat, 28 Jun 2025 13:37:06 -0700 Subject: [PATCH 21/21] Rename Instance.delete_users_and_activities/1 to Instance.delete/1 --- lib/pleroma/instances.ex | 2 +- lib/pleroma/instances/instance.ex | 2 +- lib/pleroma/web/admin_api/controllers/instance_controller.ex | 2 +- test/pleroma/instances/instance_test.exs | 4 ++-- test/pleroma/workers/delete_worker_test.exs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/instances.ex b/lib/pleroma/instances.ex index 79fbd538f..52dbba8ad 100644 --- a/lib/pleroma/instances.ex +++ b/lib/pleroma/instances.ex @@ -38,7 +38,7 @@ defmodule Pleroma.Instances do def delete_all_unreachable do get_unreachable() |> Enum.each(fn {domain, _} -> - Instance.delete_users_and_activities(domain) + Instance.delete(domain) end) end end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index cf896ca08..3695e0b75 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -296,7 +296,7 @@ defmodule Pleroma.Instances.Instance do Deletes all users from an instance in a background task, thus also deleting all of those users' activities and notifications. """ - def delete_users_and_activities(host) when is_binary(host) do + def delete(host) when is_binary(host) do DeleteWorker.new(%{"op" => "delete_instance", "host" => host}) |> Oban.insert() end diff --git a/lib/pleroma/web/admin_api/controllers/instance_controller.ex b/lib/pleroma/web/admin_api/controllers/instance_controller.ex index 117a72280..40d4d812e 100644 --- a/lib/pleroma/web/admin_api/controllers/instance_controller.ex +++ b/lib/pleroma/web/admin_api/controllers/instance_controller.ex @@ -49,7 +49,7 @@ defmodule Pleroma.Web.AdminAPI.InstanceController do end def delete(conn, %{"instance" => instance}) do - with {:ok, _job} <- Instance.delete_users_and_activities(instance) do + with {:ok, _job} <- Instance.delete(instance) do json(conn, instance) end end diff --git a/test/pleroma/instances/instance_test.exs b/test/pleroma/instances/instance_test.exs index 83e70ac38..bc3e7993e 100644 --- a/test/pleroma/instances/instance_test.exs +++ b/test/pleroma/instances/instance_test.exs @@ -239,10 +239,10 @@ defmodule Pleroma.Instances.InstanceTest do end end - test "delete_users_and_activities/1 schedules a job to delete the instance and users" do + test "delete/1 schedules a job to delete the instance and users" do insert(:user, nickname: "mario@mushroom.kingdom", name: "Mario") - {:ok, _job} = Instance.delete_users_and_activities("mushroom.kingdom") + {:ok, _job} = Instance.delete("mushroom.kingdom") assert_enqueued( worker: Pleroma.Workers.DeleteWorker, diff --git a/test/pleroma/workers/delete_worker_test.exs b/test/pleroma/workers/delete_worker_test.exs index b914aaee2..1becd0c03 100644 --- a/test/pleroma/workers/delete_worker_test.exs +++ b/test/pleroma/workers/delete_worker_test.exs @@ -17,7 +17,7 @@ defmodule Pleroma.Workers.DeleteWorkerTest do user1 = insert(:user, nickname: "alice@example.com", name: "Alice") user2 = insert(:user, nickname: "bob@example.com", name: "Bob") - {:ok, job} = Instance.delete_users_and_activities("example.com") + {:ok, job} = Instance.delete("example.com") assert_enqueued( worker: DeleteWorker,