defmodule Plausible.Site.Cache do @moduledoc """ The cache allows lookups by both `domain` and `domain_changed_from` fields - this is to allow traffic from sites whose domains changed within a certain grace period (see: `Plausible.Site.Transfer`). To differentiate cached Site structs from those retrieved directly from the database, a virtual schema field `from_cache?: true` is set. This indicates the `Plausible.Site` struct is incomplete in comparison to its database counterpart -- to spare bandwidth and query execution time, only selected database columns are retrieved and cached. The `@cached_schema_fields` attribute defines the list of DB columns queried on each cache refresh. Also see tests for more comprehensive examples. """ require Logger import Ecto.Query alias Plausible.Site use Plausible.Cache @cache_name :sites_by_domain @cached_schema_fields ~w( id domain domain_changed_from ingest_rate_limit_scale_seconds ingest_rate_limit_threshold )a @impl true def name(), do: @cache_name @impl true def child_id(), do: :cache_sites @impl true def count_all() do from(s in Site.regular()) |> Plausible.Repo.aggregate(:count) end @impl true def base_db_query() do from s in Site.regular(), left_join: rg in assoc(s, :revenue_goals), inner_join: team in assoc(s, :team), select: { s.domain, s.domain_changed_from, %{struct(s, ^@cached_schema_fields) | from_cache?: true} }, preload: [revenue_goals: rg, team: team] end @impl true def get_from_source(domain) do query = from s in base_db_query(), where: s.domain == ^domain case Plausible.Repo.one(query) do {_, _, site = %Site{}} -> %Site{site | from_cache?: false} _any -> nil end end @spec get_site_id(String.t(), Keyword.t()) :: pos_integer() | nil def get_site_id(domain, opts \\ []) do case get(domain, opts) do %{id: site_id} -> site_id nil -> nil end end @spec touch_site!(Site.t(), DateTime.t()) :: Site.t() def touch_site!(site, now) do now = now |> DateTime.truncate(:second) |> DateTime.to_naive() site |> Ecto.Changeset.change(updated_at: now) |> Plausible.Repo.update!() end @impl true def unwrap_cache_keys(items) do Enum.reduce(items, [], fn {domain, nil, object}, acc -> [{domain, object} | acc] {domain, domain_changed_from, object}, acc -> [{domain, object}, {domain_changed_from, object} | acc] end) end end