From 635bc19ac8bcb470579d71c23310eb476f3460dc Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Wed, 2 Sep 2026 13:42:48 +0400 Subject: [PATCH] fix(notifications): cost the followed-accounts subselect once Wrap the followed ap_ids subselect in keep_following_or_not_domain_blocked in ARRAY(...) so it becomes an InitPlan. As a plain subselect the planner charged its startup per rescan of the actor lookup, which could make Notification.for_user pick a plan that scans every active user per row. --- changelog.d/domain-block-subplan-cost.fix | 1 + lib/pleroma/following_relationship.ex | 10 ++++++++-- test/pleroma/notification_test.exs | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 changelog.d/domain-block-subplan-cost.fix diff --git a/changelog.d/domain-block-subplan-cost.fix b/changelog.d/domain-block-subplan-cost.fix new file mode 100644 index 000000000..7ae866ad1 --- /dev/null +++ b/changelog.d/domain-block-subplan-cost.fix @@ -0,0 +1 @@ +Fix notification and timeline queries for users with domain blocks choosing a plan that scans every active user per row; the followed-accounts exception is now an `ARRAY(SELECT ...)` InitPlan that the planner costs once. diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index e27f78d33..b803c07b4 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -258,10 +258,16 @@ defmodule Pleroma.FollowingRelationship do [_, user_actor: user_actor], fragment( # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)" + # + # ARRAY(SELECT ...) makes the followed-ids lookup an InitPlan that is + # evaluated and costed once. As a plain subselect, the planner charges + # its startup cost on every rescan of the actor lookup, which inflates + # the cost of the index plan enough for it to lose to a plan that + # materialises every active user for every candidate row. """ NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR - ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr - ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?) + ? = ANY(ARRAY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr + ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?)) """, user_actor.ap_id, ^user.domain_blocks, diff --git a/test/pleroma/notification_test.exs b/test/pleroma/notification_test.exs index a2be2ae49..992b15f8b 100644 --- a/test/pleroma/notification_test.exs +++ b/test/pleroma/notification_test.exs @@ -1028,6 +1028,21 @@ defmodule Pleroma.NotificationTest do assert Notification.for_user(user) == [] end + test "it doesn't return notifications from a blocked domain unless the actor is followed", + %{user: user} do + blocked_domain = "blocked.example" + stranger = insert(:user, local: false, domain: blocked_domain) + followed = insert(:user, local: false, domain: blocked_domain) + {:ok, user, followed} = User.follow(user, followed) + {:ok, user} = User.block_domain(user, blocked_domain) + + {:ok, _} = CommonAPI.post(stranger, %{status: "hey @#{user.nickname}"}) + {:ok, followed_activity} = CommonAPI.post(followed, %{status: "hey @#{user.nickname}"}) + + assert [%{activity: %{id: followed_id}}] = Notification.for_user(user) + assert followed_id == followed_activity.id + end + test "it doesn't return notifications for domain-blocked non-followed user", %{user: user} do blocked = insert(:user, ap_id: "http://some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com")