From e93f05a79f440fc284d3d8f702b9a6ee5a16b8b6 Mon Sep 17 00:00:00 2001 From: Lain Soykaf Date: Fri, 25 Sep 2026 08:50:41 +0400 Subject: [PATCH] fix(retention): keep threads whose post a local user quoted A quote has a context of its own, so the quoted remote thread counted as untouched and was evicted, breaking the local quote. On lain.com 45 of 134 quoted posts went this way. The objects_quote_url index keeps the lookup at one probe per post. --- changelog.d/retention-mode.add | 2 +- docs/configuration/cheatsheet.md | 5 +---- lib/pleroma/retention.ex | 13 +++++++++++++ test/mix/tasks/pleroma/database_test.exs | 23 +++++++++++++++++++++++ test/pleroma/retention_test.exs | 15 +++++++++++++++ 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/changelog.d/retention-mode.add b/changelog.d/retention-mode.add index 898acb401..3e5625fb8 100644 --- a/changelog.d/retention-mode.add +++ b/changelog.d/retention-mode.add @@ -1 +1 @@ -Add a bounded retention mode (`config :pleroma, :retention`): a cron worker walks old activities in id order and evicts remote threads nobody local interacted with, in bounded batches, so the database plateaus instead of growing forever. Adds a `retention_cursors` table. `prune_objects --keep-threads` now also keeps threads that are addressed to a local user, still have a notification for one, or contain a reported post. +Add a bounded retention mode (`config :pleroma, :retention`): a cron worker walks old activities in id order and evicts remote threads nobody local interacted with, in bounded batches, so the database plateaus instead of growing forever. Adds a `retention_cursors` table. `prune_objects --keep-threads` now also keeps threads that are addressed to a local user, still have a notification for one, contain a post quoted by a local post, or contain a reported post. diff --git a/docs/configuration/cheatsheet.md b/docs/configuration/cheatsheet.md index b299ef245..1bae9f966 100644 --- a/docs/configuration/cheatsheet.md +++ b/docs/configuration/cheatsheet.md @@ -1189,10 +1189,7 @@ Control favicons for instances. Bounded retention of remote content. Local posts are your data; remote posts are a cache of somebody else's, and this treats them that way. When enabled, a cron worker (`Pleroma.Workers.Cron.RetentionWorker`, hourly by default) evicts a batch of remote threads that nobody local has interacted with and that have been quiet for longer than `remote_post_retention_days` (see [`:instance`](#instance)). The database then plateaus instead of growing without limit. -A thread is never evicted if it contains a local post, reply, favourite, repeat or reaction, a post bookmarked by a local user, a post addressed to a local user (direct messages and mentions), a post a local user still has a notification for, or a post that was reported. Relationship activities (follows, blocks) and chat messages are never touched. Evicted posts are refetched on demand when somebody opens them again, as with `prune_objects`. - -!!! warning - A local quote post does not pin the post it quotes, since quotes do not share the quoted thread's context. Old quoted remote posts can be evicted and will be refetched when the quote is viewed. +A thread is never evicted if it contains a local post, reply, favourite, repeat or reaction, a post bookmarked by a local user, a post addressed to a local user (direct messages and mentions), a post quoted by a local post, a post a local user still has a notification for, or a post that was reported. Relationship activities (follows, blocks) and chat messages are never touched. Evicted posts are refetched on demand when somebody opens them again, as with `prune_objects`. * `enabled`: Run the worker. Defaults to `false`. * `max_objects`: Optional watermark. When the `objects` table is estimated to hold more rows than this, the oldest unpinned remote threads that have been quiet for more than a day are evicted, whatever `remote_post_retention_days` says. Defaults to `nil` (age only). diff --git a/lib/pleroma/retention.ex b/lib/pleroma/retention.ex index 9fe15d0b8..563667bb1 100644 --- a/lib/pleroma/retention.ex +++ b/lib/pleroma/retention.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Retention do users all carry the thread's context), * is bookmarked by a local user, * is addressed to a local user (direct messages and mentions), + * is a post quoted by a local post, * still has a notification for a local user, * is a report (Flag), @@ -245,6 +246,18 @@ defmodule Pleroma.Retention do ^local_prefix() ) ) + # A local quote has a context of its own, so look up quotes of the posts + # here; the objects_quote_url index is on the jsonb value. + |> having( + [a], + not fragment( + "bool_or(? ->> 'type' = 'Create' AND EXISTS (SELECT 1 FROM objects q WHERE q.data -> 'quoteUrl' = to_jsonb(associated_object_id(?)) AND left(q.data ->> 'actor', ?) = ?))", + a.data, + a.data, + ^local_prefix_length(), + ^local_prefix() + ) + ) |> maybe_keep_non_public(Keyword.get(opts, :keep_non_public, false) == true) end diff --git a/test/mix/tasks/pleroma/database_test.exs b/test/mix/tasks/pleroma/database_test.exs index 8f48d02de..31b414cc8 100644 --- a/test/mix/tasks/pleroma/database_test.exs +++ b/test/mix/tasks/pleroma/database_test.exs @@ -200,6 +200,29 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do refute Object.get_by_ap_id(old_remote_post2_id) end + test "with the --keep-threads option it keeps old threads a local user quoted", %{ + old_insert_date: old_insert_date + } do + remote_user = insert(:user, local: false) + local_user = insert(:user) + + {:ok, quoted} = CommonAPI.post(remote_user, %{status: "quote me"}) + + quoted + |> Ecto.Changeset.change(%{local: false, updated_at: old_insert_date}) + |> Repo.update!() + + {:ok, quote} = CommonAPI.post(local_user, %{status: "look", quote_id: quoted.id}) + + quote + |> Ecto.Changeset.change(%{updated_at: old_insert_date}) + |> Repo.update!() + + Mix.Tasks.Pleroma.Database.run(["prune_objects", "--keep-threads"]) + + assert Object.get_by_ap_id(quoted.data["object"]) + end + test "with the --keep-threads option it keeps old threads addressed to a local user", %{ old_insert_date: old_insert_date } do diff --git a/test/pleroma/retention_test.exs b/test/pleroma/retention_test.exs index e6f037d4c..3bd7e115a 100644 --- a/test/pleroma/retention_test.exs +++ b/test/pleroma/retention_test.exs @@ -244,6 +244,21 @@ defmodule Pleroma.RetentionTest do assert Object.get_by_ap_id(post.data["object"]) end + test "keeps an old remote thread whose post a local user quoted", %{old_date: old_date} do + remote_user = insert(:user, local: false) + local_user = insert(:user) + + quoted = old_remote_post(remote_user, old_date) + {:ok, quote} = CommonAPI.post(local_user, %{status: "look at this", quote_id: quoted.id}) + make_old(quote, old_date) + + # The quote has a context of its own and does not pin the quoted thread. + refute quote.data["context"] == quoted.data["context"] + + assert %{contexts: 0} = Retention.run() + assert Object.get_by_ap_id(quoted.data["object"]) + end + test "keeps an old remote post that was reported", %{old_date: old_date} do remote_user = insert(:user, local: false) local_user = insert(:user)