From 019d694d2aa77d5cf9ed38697aab8086c651184e Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 24 Mar 2022 12:51:23 +0100 Subject: [PATCH] Clear all ics/feed caches when modifying events/posts/actors Closes #1059 Signed-off-by: Thomas Citharel --- lib/mobilizon/events/events.ex | 15 ++++------ lib/mobilizon/posts/posts.ex | 8 +++++- lib/service/actor_suspension.ex | 2 ++ lib/service/export/cachable.ex | 23 ++++++++++++++++ lib/service/export/feed.ex | 47 ++++++++++++++++++++++++++++++- lib/service/export/icalendar.ex | 49 ++++++++++++++++++++++++++++++++- 6 files changed, 132 insertions(+), 12 deletions(-) create mode 100644 lib/service/export/cachable.ex diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex index ba01ed23a..72656e1d7 100644 --- a/lib/mobilizon/events/events.ex +++ b/lib/mobilizon/events/events.ex @@ -28,6 +28,7 @@ defmodule Mobilizon.Events do Track } + alias Mobilizon.Service.Export.Cachable alias Mobilizon.Service.Workers.BuildSearch alias Mobilizon.Service.Workers.EventDelayedNotificationWorker alias Mobilizon.Share @@ -301,7 +302,7 @@ defmodule Mobilizon.Events do end) |> Repo.transaction(), %Event{} = new_event <- Repo.preload(new_event, @event_preloads, force: true) do - Cachex.del(:ics, "event_#{new_event.uuid}") + Cachable.clear_all_caches(new_event) unless new_event.draft do %{ @@ -355,14 +356,10 @@ defmodule Mobilizon.Events do Deletes an event. """ @spec delete_event(Event.t()) :: {:ok, Event.t()} | {:error, Changeset.t()} - def delete_event(%Event{} = event), do: Repo.delete(event) - - @doc """ - Deletes an event. - Raises an exception if it fails. - """ - @spec delete_event!(Event.t()) :: Event.t() - def delete_event!(%Event{} = event), do: Repo.delete!(event) + def delete_event(%Event{} = event) do + Cachable.clear_all_caches(event) + Repo.delete(event) + end @doc """ Returns the list of events. diff --git a/lib/mobilizon/posts/posts.ex b/lib/mobilizon/posts/posts.ex index 716379bf9..28271b1e7 100644 --- a/lib/mobilizon/posts/posts.ex +++ b/lib/mobilizon/posts/posts.ex @@ -5,6 +5,7 @@ defmodule Mobilizon.Posts do alias Mobilizon.Actors.Actor alias Mobilizon.Events.Tag alias Mobilizon.Posts.Post + alias Mobilizon.Service.Export.Cachable alias Mobilizon.Storage.{Page, Repo} import Ecto.Query @@ -107,6 +108,8 @@ defmodule Mobilizon.Posts do """ @spec update_post(Post.t(), map) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()} def update_post(%Post{} = post, attrs) do + Cachable.clear_all_caches(post) + post |> Repo.preload([:tags, :media]) |> Post.changeset(attrs) @@ -117,7 +120,10 @@ defmodule Mobilizon.Posts do Deletes a post """ @spec delete_post(Post.t()) :: {:ok, Post.t()} | {:error, Ecto.Changeset.t()} - def delete_post(%Post{} = post), do: Repo.delete(post) + def delete_post(%Post{} = post) do + Cachable.clear_all_caches(post) + Repo.delete(post) + end @doc """ Returns the list of tags for the post. diff --git a/lib/service/actor_suspension.ex b/lib/service/actor_suspension.ex index 4973208d0..1b6272a67 100644 --- a/lib/service/actor_suspension.ex +++ b/lib/service/actor_suspension.ex @@ -11,6 +11,7 @@ defmodule Mobilizon.Service.ActorSuspension do alias Mobilizon.Medias.File alias Mobilizon.Posts.Post alias Mobilizon.Resources.Resource + alias Mobilizon.Service.Export.Cachable alias Mobilizon.Storage.Repo alias Mobilizon.Users.User alias Mobilizon.Web.Email.Actor, as: ActorEmail @@ -66,6 +67,7 @@ defmodule Mobilizon.Service.ActorSuspension do case Repo.transaction(multi) do {:ok, %{actor: %Actor{} = actor}} -> {:ok, true} = Cachex.del(:activity_pub, "actor_#{actor.preferred_username}") + Cachable.clear_all_caches(actor) Logger.info("Deleted actor #{actor.url}") {:ok, actor} diff --git a/lib/service/export/cachable.ex b/lib/service/export/cachable.ex new file mode 100644 index 000000000..4558de9ce --- /dev/null +++ b/lib/service/export/cachable.ex @@ -0,0 +1,23 @@ +defmodule Mobilizon.Service.Export.Cachable do + @moduledoc """ + Behavior that export modules that use caching should implement + """ + + alias Mobilizon.Actors.Actor + alias Mobilizon.Events.Event + alias Mobilizon.Posts.Post + alias Mobilizon.Service.Export.{Feed, ICalendar} + + @callback create_cache(String.t()) :: any() + + @callback clear_caches(Event.t() | Post.t() | Actor.t()) :: any() + + @spec clear_all_caches(%{ + :__struct__ => Mobilizon.Actors.Actor | Mobilizon.Events.Event | Mobilizon.Posts.Post, + optional(any) => any + }) :: {:error, boolean} | {:ok, boolean} + def clear_all_caches(entity) do + Feed.clear_caches(entity) + ICalendar.clear_caches(entity) + end +end diff --git a/lib/service/export/feed.ex b/lib/service/export/feed.ex index 106d0a320..96f5644af 100644 --- a/lib/service/export/feed.ex +++ b/lib/service/export/feed.ex @@ -11,7 +11,7 @@ defmodule Mobilizon.Service.Export.Feed do alias Mobilizon.Config alias Mobilizon.Events.Event alias Mobilizon.Posts.Post - alias Mobilizon.Service.Export.Common + alias Mobilizon.Service.Export.{Cachable, Common} alias Mobilizon.Users.User alias Mobilizon.Web.Endpoint @@ -19,11 +19,14 @@ defmodule Mobilizon.Service.Export.Feed do require Logger + @behaviour Cachable + @item_limit 500 @spec version :: String.t() defp version, do: Config.instance_version() + @impl Cachable @spec create_cache(String.t()) :: {:commit, String.t()} | {:ignore, :actor_not_found | :actor_not_public | :bad_token | :token_not_found} @@ -37,6 +40,7 @@ defmodule Mobilizon.Service.Export.Feed do end end + @impl Cachable def create_cache("token_" <> token) do case fetch_events_from_token(token) do {:ok, res} -> @@ -47,6 +51,7 @@ defmodule Mobilizon.Service.Export.Feed do end end + @impl Cachable def create_cache("instance") do {:ok, res} = fetch_instance_feed() {:commit, res} @@ -227,4 +232,44 @@ defmodule Mobilizon.Service.Export.Feed do |> Feed.build() |> Atomex.generate_document() end + + @impl Cachable + def clear_caches(%Event{attributed_to: %Actor{} = actor} = event) do + clear_actor_feed(actor) + clear_caches(%{event | attributed_to: nil}) + end + + @impl Cachable + def clear_caches(%Event{}) do + # TODO: It would be nice to clear feed token cache based on participations as well, + # but that's harder, as it would require loading all participations + clear_instance() + end + + @impl Cachable + def clear_caches(%Post{attributed_to: %Actor{} = actor} = post) do + clear_actor_feed(actor) + clear_caches(%{post | attributed_to: nil}) + end + + @impl Cachable + def clear_caches(%Post{}) do + clear_instance() + end + + @impl Cachable + def clear_caches(%Actor{} = actor) do + clear_actor_feed(actor) + clear_instance() + end + + defp clear_instance do + Cachex.del(:feed, "instance") + end + + defp clear_actor_feed(%Actor{preferred_username: preferred_username} = actor) do + if Actor.is_public_visibility?(actor) do + Cachex.del(:feed, "actor_#{preferred_username}") + end + end end diff --git a/lib/service/export/icalendar.ex b/lib/service/export/icalendar.ex index 7ba32cc48..5bcd67401 100644 --- a/lib/service/export/icalendar.ex +++ b/lib/service/export/icalendar.ex @@ -7,14 +7,17 @@ defmodule Mobilizon.Service.Export.ICalendar do alias Mobilizon.Addresses.Address alias Mobilizon.{Config, Events} alias Mobilizon.Events.{Event, EventOptions} - alias Mobilizon.Service.Export.Common + alias Mobilizon.Service.Export.{Cachable, Common} alias Mobilizon.Service.Formatter.HTML + @behaviour Cachable + @item_limit 500 @doc """ Create cache for an actor, an event or an user token """ + @impl Cachable @spec create_cache(String.t()) :: {:commit, String.t()} | {:ignore, atom()} def create_cache("actor_" <> name) do case export_public_actor(name) do @@ -26,6 +29,7 @@ defmodule Mobilizon.Service.Export.ICalendar do end end + @impl Cachable def create_cache("event_" <> uuid) do with %Event{} = event <- Events.get_public_event_by_uuid_with_preload(uuid), {:ok, res} <- export_public_event(event) do @@ -39,6 +43,7 @@ defmodule Mobilizon.Service.Export.ICalendar do end end + @impl Cachable def create_cache("token_" <> token) do case fetch_events_from_token(token) do {:ok, res} -> @@ -49,6 +54,7 @@ defmodule Mobilizon.Service.Export.ICalendar do end end + @impl Cachable def create_cache("instance") do {:ok, res} = fetch_instance_feed() {:commit, res} @@ -172,4 +178,45 @@ defmodule Mobilizon.Service.Export.ICalendar do defp organizer(%Event{organizer_actor: %Actor{} = profile}) do Actor.display_name(profile) end + + @impl Cachable + @spec clear_caches(%{ + :__struct__ => Mobilizon.Actors.Actor | Mobilizon.Events.Event | Mobilizon.Posts.Post, + optional(any) => any + }) :: {:error, boolean} | {:ok, boolean} + def clear_caches(%Event{attributed_to: %Actor{} = actor} = event) do + clear_actor_feed(actor) + clear_caches(%{event | attributed_to: nil}) + end + + @impl Cachable + def clear_caches(%Event{uuid: uuid}) do + # TODO: It would be nice to clear feed token cache based on participations as well, + # but that's harder, as it would require loading all participations + Cachex.del(:ics, "event_#{uuid}") + clear_instance() + end + + # Not applicable for posts + @impl Cachable + def clear_caches(%Mobilizon.Posts.Post{}) do + {:ok, true} + end + + @impl Cachable + def clear_caches(%Actor{} = actor) do + clear_actor_feed(actor) + + clear_instance() + end + + defp clear_instance do + Cachex.del(:ics, "instance") + end + + defp clear_actor_feed(%Actor{preferred_username: preferred_username} = actor) do + if Actor.is_public_visibility?(actor) do + Cachex.del(:ics, "actor_#{preferred_username}") + end + end end