defmodule Plausible.ConsolidatedView.Cache do @moduledoc """ Caching layer for consolidated views. Because of how they're modelled (on top of "sites" table currently), we have to refresh the cache whenever any regular site changes within, as well as when the consolidating site is updated itself. """ alias Plausible.ConsolidatedView import Ecto.Query use Plausible.Cache @cache_name :consolidated_views @large_view_alert_threshold 12_000 @max_sites_per_view 14_000 @impl true def name(), do: @cache_name @impl true def child_id(), do: :cache_consolidated_views @impl true def count_all() do Plausible.Repo.aggregate( from(s in ConsolidatedView.sites()), :count ) end @impl true def base_db_query() do from sc in ConsolidatedView.sites(), inner_join: sr in ^Plausible.Site.regular(), on: sr.team_id == sc.team_id, group_by: sc.id, order_by: [desc: sc.id], select: %{ consolidated_view_id: sc.domain, site_ids: fragment("array_agg(? ORDER BY ? DESC)", sr.id, sr.id) } end @spec refresh_updated_recently(Keyword.t()) :: :ok def refresh_updated_recently(opts) do recently_updated_site_ids = from sc in ConsolidatedView.sites(), join: sr in ^Plausible.Site.regular(), on: sc.team_id == sr.team_id, where: sr.updated_at > ago(^15, "minute") or sc.updated_at > ago(^15, "minute"), select: sc.id query = from sc in ConsolidatedView.sites(), join: sr in ^Plausible.Site.regular(), on: sr.team_id == sc.team_id, where: sc.id in subquery(recently_updated_site_ids), group_by: sc.id, order_by: [desc: sc.id], select: %{ consolidated_view_id: sc.domain, site_ids: fragment("array_agg(? ORDER BY ? DESC)", sr.id, sr.id) } refresh( :updated_recently, query, Keyword.put(opts, :delete_stale_items?, false) ) end @impl true def get_from_source(consolidated_view_id) do case ConsolidatedView.site_ids(consolidated_view_id) do {:ok, some} -> some {:error, :not_found} -> nil end end @impl true def unwrap_cache_keys(items) do Enum.reduce(items, [], fn row, acc -> [{row.consolidated_view_id, row.site_ids} | acc] end) end def get(key, opts) do case super(key, opts) do nil -> [] site_ids when length(site_ids) > @large_view_alert_threshold -> Sentry.capture_message("Consolidated View crop warning", extra: %{sites: length(site_ids), key: key} ) Enum.take(site_ids, @max_sites_per_view) site_ids -> site_ids end end end