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() | :auto} | {: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