| defmodule Plausible.Cache.Adapter do |
| @moduledoc """ |
| Interface for the underlying cache implementation. |
| Currently: ConCache |
| |
| Using the Adapter module directly, the user must ensure that the relevant |
| processes are available to use, which is normally done via the child specification. |
| """ |
|
|
| require Logger |
|
|
| @spec child_specs(atom(), atom(), Keyword.t()) :: [Supervisor.child_spec()] |
| def child_specs(name, child_id, opts \\ []) |
| when is_atom(name) and is_atom(child_id) and is_list(opts) do |
| partitions = partitions(name) |
|
|
| if partitions == 1 do |
| [child_spec(name, child_id, opts)] |
| else |
| Enum.map(1..partitions, fn partition -> |
| partition_name = String.to_atom("#{name}_#{partition}") |
| partition_child_id = String.to_atom("#{child_id}_#{partition}") |
|
|
| child_spec(partition_name, partition_child_id, opts) |
| end) |
| end |
| end |
|
|
| @spec child_spec(atom(), atom(), Keyword.t()) :: Supervisor.child_spec() |
| def child_spec(name, child_id, opts \\ []) |
| when is_atom(name) and is_atom(child_id) and is_list(opts) do |
| cache_name = Keyword.get(opts, :cache_name, name) |
| child_id = Keyword.get(opts, :child_id, child_id) |
| ttl_check_interval = Keyword.get(opts, :ttl_check_interval, false) |
|
|
| opts = |
| opts |
| |> Keyword.put(:name, cache_name) |
| |> Keyword.put(:ttl_check_interval, ttl_check_interval) |
|
|
| Supervisor.child_spec( |
| {ConCache, opts}, |
| id: child_id |
| ) |
| end |
|
|
| @spec size(atom()) :: non_neg_integer() | nil |
| def size(cache_name) do |
| cache_name |
| |> get_names() |
| |> Enum.map(&ConCache.size/1) |
| |> Enum.sum() |
| catch |
| :exit, _ -> nil |
| end |
|
|
| @spec get(atom(), any()) :: any() |
| def get(cache_name, key) do |
| full_cache_name = get_name(cache_name, key) |
| ConCache.get(full_cache_name, key) |
| catch |
| :exit, _ -> |
| Logger.error("Error retrieving key from '#{inspect(cache_name)}'") |
| nil |
| end |
|
|
| @spec get(atom(), any(), (-> any())) :: any() |
| def get(cache_name, key, fallback_fn) do |
| full_cache_name = get_name(cache_name, key) |
| ConCache.dirty_get_or_store(full_cache_name, key, fallback_fn) |
| catch |
| :exit, _ -> |
| Logger.error("Error retrieving key from '#{inspect(cache_name)}'") |
| nil |
| end |
|
|
| @spec fetch(atom(), any(), (-> any())) :: any() |
| def fetch(cache_name, key, fallback_fn) do |
| full_cache_name = get_name(cache_name, key) |
| ConCache.dirty_fetch_or_store(full_cache_name, key, fallback_fn) |
| catch |
| :exit, _ -> |
| Logger.error("Error fetching key from '#{inspect(cache_name)}'") |
| nil |
| end |
|
|
| @spec put(atom(), any(), any()) :: any() |
| def put(cache_name, key, value, _opts \\ []) do |
| full_cache_name = get_name(cache_name, key) |
| :ok = ConCache.dirty_put(full_cache_name, key, value) |
|
|
| value |
| catch |
| :exit, _ -> |
| Logger.error("Error putting a key to '#{cache_name}'") |
| nil |
| end |
|
|
| @spec put_many(atom(), [any()]) :: :ok |
| def put_many(cache_name, items) when is_list(items) do |
| items |
| |> Enum.group_by(fn {key, _} -> get_name(cache_name, key) end) |
| |> Enum.each(fn {full_cache_name, items} -> |
| true = :ets.insert(ConCache.ets(full_cache_name), items) |
| end) |
|
|
| :ok |
| catch |
| :exit, _ -> |
| Logger.error("Error putting keys to '#{cache_name}'") |
| :ok |
| end |
|
|
| @spec delete(atom(), any()) :: :ok |
| def delete(cache_name, key) do |
| full_cache_name = get_name(cache_name, key) |
| ConCache.dirty_delete(full_cache_name, key) |
| catch |
| :exit, _ -> |
| Logger.error("Error deleting a key in '#{cache_name}'") |
| :ok |
| end |
|
|
| @spec keys(atom()) :: Enumerable.t() |
| def keys(cache_name) do |
| cache_name |
| |> get_names() |
| |> Enum.reduce([], fn full_cache_name, stream -> |
| Stream.concat(stream, get_keys(full_cache_name)) |
| end) |
| catch |
| :exit, _ -> |
| Logger.error("Error retrieving key from '#{inspect(cache_name)}'") |
| [] |
| end |
|
|
| @spec get_names(atom()) :: [atom()] |
| def get_names(cache_name) do |
| partitions = partitions(cache_name) |
|
|
| if partitions == 1 do |
| [cache_name] |
| else |
| Enum.map(1..partitions, &String.to_existing_atom("#{cache_name}_#{&1}")) |
| end |
| end |
|
|
| defp get_keys(full_cache_name) do |
| ets = ConCache.ets(full_cache_name) |
|
|
| Stream.resource( |
| fn -> :ets.first(ets) end, |
| fn |
| :"$end_of_table" -> {:halt, nil} |
| prev_key -> {[prev_key], :ets.next(ets, prev_key)} |
| end, |
| fn _ -> :ok end |
| ) |
| end |
|
|
| defp get_name(cache_name, key) do |
| partitions = partitions(cache_name) |
|
|
| if partitions == 1 do |
| cache_name |
| else |
| chosen_partition = :erlang.phash2(key, partitions) + 1 |
| String.to_existing_atom("#{cache_name}_#{chosen_partition}") |
| end |
| end |
|
|
| defp partitions(cache_name) do |
| Application.get_env(:plausible, __MODULE__)[cache_name][:partitions] || 1 |
| end |
|
|
| def cache2list(full_cache_name) do |
| :ets.tab2list(ConCache.ets(full_cache_name)) |
| end |
| end |
|
|