lib/cache/con_cache.ex

defmodule Cache.ConCache do
  @opts_definition [
    acquire_lock_timeout: [type: :pos_integer, default: 5000],
    touch_on_read: [type: :boolean, default: false],

    global_ttl: [type: {:or, [:pos_integer, {:in, [:infinity]}]}, default: :timer.minutes(30)],
    ttl_check_interval: [type: {:or, [:pos_integer, {:in, [false]}]}, default: :timer.minutes(1)],

    dirty?: [type: :boolean, default: true, doc: "Use dirty_put instead of locking to put, enabled by default"],

    ets_options: [
      type: {:custom, Cache.ETS, :opts_definition, []},
      doc: "https://www.erlang.org/doc/man/ets.html#new-2"
    ]
  ]
  @moduledoc """
  ETS Based cache https://github.com/sasa1977/con_cache

  Takes the following options:

   #{NimbleOptions.docs(@opts_definition)}
  """

  @behaviour Cache

  @type opts :: [
    name: atom,
    pid: pid,
    global_ttl: non_neg_integer | :infinity,
    acquire_lock_timeout: pos_integer,
    touch_on_read: boolean | nil,
    ttl_check_interval: non_neg_integer() | false,
    ets_options: [ets_option()]
  ]

  @type ets_option :: :named_table
                    | :compressed
                    | {:heir, pid()}
                    | {:write_concurrency, boolean()}
                    | {:read_concurrency, boolean()}
                    | :ordered_set
                    | :set
                    | :bag
                    | :duplicate_bag
                    | {:name, atom()}

  defmacro __using__(_opts) do
    quote do
      def get_or_store(key, ttl, store_fun) do
        @cache_adapter.get_or_store(@cache_name, key, ttl, store_fun)
      end

      def dirty_get_or_store(key, store_fun) do
        @cache_adapter.dirty_get_or_store(@cache_name, key, store_fun)
      end
    end
  end

  @impl Cache
  def opts_definition, do: @opts_definition

  @impl Cache
  def start_link(opts) do
    cache_opts =
      opts
        |> Keyword.delete(:dirty?)
        |> Keyword.update(
          :ets_options,
          [:named_table, name: opts[:name]],
          &[:named_table, {:name, opts[:name]} | &1]
        )

    ConCache.start_link(cache_opts)
  end

  @impl Cache
  def child_spec({cache_name, opts}) do
    %{
      id: :"con_cache_#{cache_name}",
      start: {Cache.ConCache, :start_link, [Keyword.put(opts, :name, cache_name)]},
      type: :supervisor
    }
  end

  @impl Cache
  def put(cache_name, key, _ttl \\ nil, value, _opts \\ [])

  def put(cache_name, key, nil, value, opts) do
    if is_nil(opts[:dirty?]) or opts[:dirty?] do
      ConCache.dirty_put(cache_name, key, value)
    else
      ConCache.put(cache_name, key, value)
    end
  end

  def put(cache_name, key, ttl, value, opts) do
    item = %ConCache.Item{value: value, ttl: ttl}

    if is_nil(opts[:dirty?]) or opts[:dirty?] do
      ConCache.dirty_put(cache_name, key, item)
    else
      ConCache.put(cache_name, key, item)
    end
  end

  @impl Cache
  def get(cache_name, key, _opts \\ []) do
    {:ok, ConCache.get(cache_name, key)}
  end

  @impl Cache
  def delete(cache_name, key, _opts \\ []) do
    ConCache.delete(cache_name, key)
  end

  @doc """
  Implements a version of get_or_store that locks locally
  so only one process runs `store_fun` at a time.

  Any other processes that miss cache will wait for the first
  caller to finish `store_fun` then will read the result from cache.
  """
  def get_or_store(cache_name, key, ttl, store_fun) do
    ConCache.get_or_store(cache_name, key, fn ->
      %ConCache.Item{value: store_fun.(), ttl: ttl}
    end)
  end

  def dirty_get_or_store(cache_name, key, store_fun) do
    ConCache.dirty_get_or_store(cache_name, key, store_fun)
  end
end